Feat: Support kruise rollout (#4243)

* Feat: support kruise rollout

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

resolve roll back

fix

add tests

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

small fix

* fix rollback

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

topology filter by owner reference

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix ci

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

add comments

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix imports

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix lint

* rollback related tests

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

rename the operator

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix bugs

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix test

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix test

* clean args before start

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix test

* remove replace go mod

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

* fix operation tests

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
wyike
2022-06-24 18:03:04 +08:00
committed by GitHub
co-authored by Somefive
parent dc660fc97d
commit c4e1f39d28
18 changed files with 1820 additions and 253 deletions
@@ -280,6 +280,9 @@ func (h *AppHandler) collectHealthStatus(ctx context.Context, wl *appfile.Worklo
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appName, wl.Name, tr.Name)
}
if status.Message == "" && traitStatus.Message != "" {
status.Message = traitStatus.Message
}
traitStatusList = append(traitStatusList, traitStatus)
namespace = appRev.GetNamespace()
wl.Ctx.SetCtx(context.WithValue(wl.Ctx.GetCtx(), multicluster.ClusterContextKey, status.Cluster))
+200
View File
@@ -0,0 +1,200 @@
/*
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"
"io"
"github.com/pkg/errors"
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
)
// ClusterRollout rollout in specified cluster
type ClusterRollout struct {
*kruisev1alpha1.Rollout
Cluster string
}
func getAssociatedRollouts(ctx context.Context, cli client.Client, app *v1beta1.Application, withHistoryRTs bool) ([]*ClusterRollout, error) {
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app)
if err != nil {
return nil, errors.Wrapf(err, "failed to list resource trackers")
}
if !withHistoryRTs {
historyRTs = []*v1beta1.ResourceTracker{}
}
var rollouts []*ClusterRollout
for _, rt := range append(historyRTs, rootRT, currentRT) {
if rt == nil {
continue
}
for _, mr := range rt.Spec.ManagedResources {
if mr.APIVersion == kruisev1alpha1.SchemeGroupVersion.String() && mr.Kind == "Rollout" {
rollout := &kruisev1alpha1.Rollout{}
if err = cli.Get(multicluster.ContextWithClusterName(ctx, mr.Cluster), k8stypes.NamespacedName{Namespace: mr.Namespace, Name: mr.Name}, rollout); err != nil {
if multicluster.IsNotFoundOrClusterNotExists(err) || velaerrors.IsCRDNotExists(err) {
continue
}
return nil, errors.Wrapf(err, "failed to get kruise rollout %s/%s in cluster %s", mr.Namespace, mr.Name, mr.Cluster)
}
rollouts = append(rollouts, &ClusterRollout{Rollout: rollout, Cluster: mr.Cluster})
}
}
}
return rollouts, nil
}
// SuspendRollout find all rollouts associated with the application (including history RTs) and resume them
func SuspendRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) error {
rollouts, err := getAssociatedRollouts(ctx, cli, app, true)
if err != nil {
return err
}
for i := range rollouts {
rollout := rollouts[i]
if rollout.Status.Phase == kruisev1alpha1.RolloutPhaseProgressing && !rollout.Spec.Strategy.Paused {
_ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster)
rolloutKey := client.ObjectKeyFromObject(rollout.Rollout)
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil {
return err
}
if rollout.Status.Phase == kruisev1alpha1.RolloutPhaseProgressing && !rollout.Spec.Strategy.Paused {
rollout.Spec.Strategy.Paused = true
if err = cli.Update(_ctx, rollout.Rollout); err != nil {
return err
}
if writer != nil {
_, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s suspended.\n", rollout.Namespace, rollout.Name, rollout.Cluster)))
}
return nil
}
return nil
}); err != nil {
return errors.Wrapf(err, "failed to suspend rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster)
}
}
}
return nil
}
// ResumeRollout find all rollouts associated with the application (in the current RT) and resume them
func ResumeRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) (bool, error) {
rollouts, err := getAssociatedRollouts(ctx, cli, app, false)
if err != nil {
return false, err
}
modified := false
for i := range rollouts {
rollout := rollouts[i]
if rollout.Spec.Strategy.Paused || (rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused) {
_ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster)
rolloutKey := client.ObjectKeyFromObject(rollout.Rollout)
resumed := false
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil {
return err
}
if rollout.Spec.Strategy.Paused {
rollout.Spec.Strategy.Paused = false
if err = cli.Update(_ctx, rollout.Rollout); err != nil {
return err
}
resumed = true
return nil
}
return nil
}); err != nil {
return false, errors.Wrapf(err, "failed to resume rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster)
}
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil {
return err
}
if rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused {
rollout.Status.CanaryStatus.CurrentStepState = kruisev1alpha1.CanaryStepStateReady
if err = cli.Status().Update(_ctx, rollout.Rollout); err != nil {
return err
}
resumed = true
return nil
}
return nil
}); err != nil {
return false, errors.Wrapf(err, "failed to resume rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster)
}
if resumed {
modified = true
if writer != nil {
_, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s resumed.\n", rollout.Namespace, rollout.Name, rollout.Cluster)))
}
}
}
}
return modified, nil
}
// RollbackRollout find all rollouts associated with the application (in the current RT) and disable the pause field.
func RollbackRollout(ctx context.Context, cli client.Client, app *v1beta1.Application, writer io.Writer) (bool, error) {
rollouts, err := getAssociatedRollouts(ctx, cli, app, false)
if err != nil {
return false, err
}
modified := false
for i := range rollouts {
rollout := rollouts[i]
if rollout.Spec.Strategy.Paused || (rollout.Status.CanaryStatus != nil && rollout.Status.CanaryStatus.CurrentStepState == kruisev1alpha1.CanaryStepStatePaused) {
_ctx := multicluster.ContextWithClusterName(ctx, rollout.Cluster)
rolloutKey := client.ObjectKeyFromObject(rollout.Rollout)
resumed := false
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(_ctx, rolloutKey, rollout.Rollout); err != nil {
return err
}
if rollout.Spec.Strategy.Paused {
rollout.Spec.Strategy.Paused = false
if err = cli.Update(_ctx, rollout.Rollout); err != nil {
return err
}
resumed = true
return nil
}
return nil
}); err != nil {
return false, errors.Wrapf(err, "failed to rollback rollout %s/%s in cluster %s", rollout.Namespace, rollout.Name, rollout.Cluster)
}
if resumed {
modified = true
if writer != nil {
_, _ = writer.Write([]byte(fmt.Sprintf("Rollout %s/%s in cluster %s rollback.\n", rollout.Namespace, rollout.Name, rollout.Cluster)))
}
}
}
}
return modified, nil
}
+155
View File
@@ -0,0 +1,155 @@
/*
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"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
var _ = Describe("Kruise rollout test", func() {
ctx := context.Background()
BeforeEach(func() {
Expect(k8sClient.Create(ctx, rollout.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, rt.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, app.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
})
It("test get associated rollout func", func() {
rollouts, err := getAssociatedRollouts(ctx, k8sClient, &app, false)
Expect(err).Should(BeNil())
Expect(len(rollouts)).Should(BeEquivalentTo(1))
})
It("Suspend rollout", func() {
r := kruisev1alpha1.Rollout{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil())
r.Status.Phase = kruisev1alpha1.RolloutPhaseProgressing
Expect(k8sClient.Status().Update(ctx, &r)).Should(BeNil())
Expect(SuspendRollout(ctx, k8sClient, &app, nil))
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r))
Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(true))
})
It("Resume rollout", func() {
r := kruisev1alpha1.Rollout{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil())
Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(true))
Expect(ResumeRollout(ctx, k8sClient, &app, nil))
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r))
Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(false))
})
It("Rollback rollout", func() {
r := kruisev1alpha1.Rollout{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r)).Should(BeNil())
r.Spec.Strategy.Paused = true
Expect(k8sClient.Update(ctx, &r)).Should(BeNil())
Expect(RollbackRollout(ctx, k8sClient, &app, nil))
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "my-rollout"}, &r))
Expect(r.Spec.Strategy.Paused).Should(BeEquivalentTo(false))
})
})
var app = v1beta1.Application{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "Application",
},
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-app",
Namespace: "default",
Generation: 1,
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{},
},
}
var rt = v1beta1.ResourceTracker{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "ResourceTracker",
},
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-app",
Labels: map[string]string{
"app.oam.dev/appRevision": "rollout-app-v1",
"app.oam.dev/name": "rollout-app",
"app.oam.dev/namespace": "default",
},
},
Spec: v1beta1.ResourceTrackerSpec{
ApplicationGeneration: 1,
Type: v1beta1.ResourceTrackerTypeVersioned,
ManagedResources: []v1beta1.ManagedResource{
{
ClusterObjectReference: common.ClusterObjectReference{
ObjectReference: v1.ObjectReference{
APIVersion: "rollouts.kruise.io/v1alpha1",
Kind: "Rollout",
Name: "my-rollout",
Namespace: "default",
},
},
OAMObjectReference: common.OAMObjectReference{
Component: "my-rollout",
},
},
},
},
}
var rollout = kruisev1alpha1.Rollout{
TypeMeta: metav1.TypeMeta{
APIVersion: "rollouts.kruise.io/v1alpha1",
Kind: "Rollout",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-rollout",
Namespace: "default",
},
Spec: kruisev1alpha1.RolloutSpec{
ObjectRef: kruisev1alpha1.ObjectRef{
WorkloadRef: &kruisev1alpha1.WorkloadRef{
APIVersion: "appsv1",
Kind: "Deployment",
Name: "canary-demo",
},
},
Strategy: kruisev1alpha1.RolloutStrategy{
Canary: &kruisev1alpha1.CanaryStrategy{
Steps: []kruisev1alpha1.CanaryStep{
{
Weight: 30,
},
},
},
Paused: false,
},
},
}
+119
View File
@@ -0,0 +1,119 @@
/*
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"
"path/filepath"
"testing"
"time"
"k8s.io/client-go/discovery"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
ocmworkv1 "open-cluster-management.io/api/work/v1"
v12 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/cue/packages"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
// +kubebuilder:scaffold:imports
)
var cfg *rest.Config
var scheme *runtime.Scheme
var k8sClient client.Client
var testEnv *envtest.Environment
var dm discoverymapper.DiscoveryMapper
var pd *packages.PackageDiscover
var testns string
var dc *discovery.DiscoveryClient
func TestAddon(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Kruise rollout Suite test",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "vela-core", "crds"), filepath.Join("", "testdata")},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
scheme = runtime.NewScheme()
Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(crdv1.AddToScheme(scheme)).NotTo(HaveOccurred())
_ = ocmclusterv1alpha1.Install(scheme)
_ = ocmclusterv1.Install(scheme)
_ = ocmworkv1.Install(scheme)
_ = kruisev1alpha1.AddToScheme(scheme)
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
dc, err = discovery.NewDiscoveryClientForConfig(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(dc).ShouldNot(BeNil())
dm, err = discoverymapper.New(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(dm).ToNot(BeNil())
pd, err = packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(pd).ToNot(BeNil())
testns = "vela-system"
Expect(k8sClient.Create(context.Background(),
&v12.Namespace{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, ObjectMeta: metav1.ObjectMeta{
Name: testns,
}}))
close(done)
}, 120)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
+290
View File
@@ -0,0 +1,290 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: rollouts.rollouts.kruise.io
spec:
group: rollouts.kruise.io
names:
kind: Rollout
listKind: RolloutList
plural: rollouts
singular: rollout
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: The rollout status phase
jsonPath: .status.phase
name: STATUS
type: string
- description: The rollout canary status step
jsonPath: .status.canaryStatus.currentStepIndex
name: CANARY_STEP
type: integer
- description: The rollout canary status step state
jsonPath: .status.canaryStatus.currentStepState
name: CANARY_STATE
type: string
- description: The rollout canary status message
jsonPath: .status.message
name: MESSAGE
type: string
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: Rollout is the Schema for the rollouts 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: RolloutSpec defines the desired state of Rollout
properties:
objectRef:
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file
ObjectRef indicates workload'
properties:
workloadRef:
description: WorkloadRef contains enough information to let you
identify a workload for Rollout Batch release of the bypass
properties:
apiVersion:
description: API Version of the referent
type: string
kind:
description: Kind of the referent
type: string
name:
description: Name of the referent
type: string
required:
- apiVersion
- kind
- name
type: object
type: object
strategy:
description: rollout strategy
properties:
canary:
description: CanaryStrategy defines parameters for a Replica Based
Canary
properties:
steps:
description: Steps define the order of phases to execute release
in batches(20%, 40%, 60%, 80%, 100%)
items:
description: CanaryStep defines a step of a canary workload.
properties:
pause:
description: Pause defines a pause stage for a rollout,
manual or auto
properties:
duration:
description: Duration the amount of time to wait
before moving to the next step.
format: int32
type: integer
type: object
replicas:
anyOf:
- type: integer
- type: string
description: 'Replicas is the number of expected canary
pods in this batch it can be an absolute number (ex:
5) or a percentage of total pods.'
x-kubernetes-int-or-string: true
weight:
description: SetWeight sets what percentage of the canary
pods should receive
format: int32
type: integer
type: object
type: array
trafficRoutings:
description: TrafficRoutings hosts all the supported service
meshes supported to enable more fine-grained traffic routing
todo current only support one
items:
description: TrafficRouting hosts all the different configuration
for supported service meshes to enable more fine-grained
traffic routing
properties:
gracePeriodSeconds:
description: Optional duration in seconds the traffic
provider(e.g. nginx ingress controller) consumes the
service, ingress configuration changes gracefully.
format: int32
type: integer
ingress:
description: Ingress holds Ingress specific configuration
to route traffic, e.g. Nginx, Alb.
properties:
name:
description: Name refers to the name of an `Ingress`
resource in the same namespace as the `Rollout`
type: string
required:
- name
type: object
service:
description: Service holds the name of a service which
selects pods with stable version and don't select
any pods with canary version.
type: string
type:
description: nginx, alb, istio etc.
type: string
required:
- service
- type
type: object
type: array
type: object
paused:
description: Paused indicates that the Rollout is paused. Default
value is false
type: boolean
type: object
required:
- objectRef
- strategy
type: object
status:
description: RolloutStatus defines the observed state of Rollout
properties:
canaryStatus:
description: Canary describes the state of the canary rollout
properties:
canaryReadyReplicas:
description: CanaryReadyReplicas the numbers of ready canary revision
pods
format: int32
type: integer
canaryReplicas:
description: CanaryReplicas the numbers of canary revision pods
format: int32
type: integer
canaryRevision:
description: CanaryRevision is calculated by rollout based on
podTemplateHash, and the internal logic flow uses It may be
different from rs podTemplateHash in different k8s versions,
so it cannot be used as service selector label
type: string
canaryService:
description: CanaryService holds the name of a service which selects
pods with canary version and don't select any pods with stable
version.
type: string
currentStepIndex:
description: CurrentStepIndex defines the current step of the
rollout is on. If the current step index is null, the controller
will execute the rollout.
format: int32
type: integer
currentStepState:
type: string
lastReadyTime:
description: The last time this step pods is ready.
format: date-time
type: string
message:
type: string
observedWorkloadGeneration:
description: observedWorkloadGeneration is the most recent generation
observed for this Rollout ref workload generation.
format: int64
type: integer
podTemplateHash:
description: pod template hash is used as service selector label
type: string
rolloutHash:
description: RolloutHash from rollout.spec object
type: string
required:
- canaryReadyReplicas
- canaryReplicas
- canaryService
- currentStepState
- podTemplateHash
type: object
conditions:
description: Conditions a list of conditions a rollout can have.
items:
description: RolloutCondition describes the state of a rollout at
a certain point.
properties:
lastTransitionTime:
description: Last time the condition transitioned from one status
to another.
format: date-time
type: string
lastUpdateTime:
description: The last time this condition was updated.
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: Phase of the condition, one of True, False, Unknown.
type: string
type:
description: Type of rollout condition.
type: string
required:
- message
- reason
- status
- type
type: object
type: array
message:
description: Message provides details on why the rollout is in its
current phase
type: string
observedGeneration:
description: observedGeneration is the most recent generation observed
for this Rollout.
format: int64
type: integer
phase:
description: BlueGreenStatus *BlueGreenStatus `json:"blueGreenStatus,omitempty"`
Phase is the rollout phase.
type: string
stableRevision:
description: CanaryRevision the hash of the canary pod template CanaryRevision
string `json:"canaryRevision,omitempty"` StableRevision indicates
the revision pods that has successfully rolled out
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
+2
View File
@@ -37,6 +37,7 @@ import (
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/oam-dev/terraform-config-inspect/tfconfig"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
errors2 "github.com/pkg/errors"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
istioclientv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
@@ -102,6 +103,7 @@ func init() {
_ = ocmworkv1.Install(Scheme)
_ = clustergatewayapi.AddToScheme(Scheme)
_ = metricsV1beta1api.AddToScheme(Scheme)
_ = kruisev1alpha1.AddToScheme(Scheme)
_ = prismclusterv1alpha1.AddToScheme(Scheme)
// +kubebuilder:scaffold:scheme
}
+20 -3
View File
@@ -89,7 +89,8 @@ func init() {
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}: nil,
{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}: nil,
},
DefaultGenListOptionFunc: helmRelease2AnyListOption,
DefaultGenListOptionFunc: helmRelease2AnyListOption,
DisableFilterByOwnerReference: true,
}
}
@@ -117,6 +118,8 @@ type ChildrenResourcesRule struct {
CareResource map[ResourceType]genListOptionFunc
// if specified genListOptionFunc is nil will use use default genListOptionFunc to generate listOption.
DefaultGenListOptionFunc genListOptionFunc
// DisableFilterByOwnerReference means don't use parent resource's UID filter the result.
DisableFilterByOwnerReference bool
}
type genListOptionFunc func(unstructured.Unstructured) (client.ListOptions, error)
@@ -618,7 +621,7 @@ func fetchObjectWithResourceTreeNode(ctx context.Context, cluster string, k8sCli
}
func listItemByRule(clusterCTX context.Context, k8sClient client.Client, resource ResourceType,
parentObject unstructured.Unstructured, specifiedFunc genListOptionFunc, defaultFunc genListOptionFunc) ([]unstructured.Unstructured, error) {
parentObject unstructured.Unstructured, specifiedFunc genListOptionFunc, defaultFunc genListOptionFunc, disableFilterByOwner bool) ([]unstructured.Unstructured, error) {
itemList := unstructured.UnstructuredList{}
itemList.SetAPIVersion(resource.APIVersion)
@@ -657,6 +660,20 @@ func listItemByRule(clusterCTX context.Context, k8sClient client.Client, resourc
if err != nil {
return nil, err
}
if !disableFilterByOwner {
var res []unstructured.Unstructured
for _, item := range itemList.Items {
if len(item.GetOwnerReferences()) == 0 {
res = append(res, item)
}
for _, reference := range item.GetOwnerReferences() {
if reference.UID == parentObject.GetUID() {
res = append(res, item)
}
}
}
return res, nil
}
return itemList.Items, nil
}
@@ -676,7 +693,7 @@ func iteratorChildResources(ctx context.Context, cluster string, k8sClient clien
var resList []*types.ResourceTreeNode
for resource, specifiedFunc := range rules.CareResource {
clusterCTX := multicluster.ContextWithClusterName(ctx, cluster)
items, err := listItemByRule(clusterCTX, k8sClient, resource, *parentObject, specifiedFunc, rules.DefaultGenListOptionFunc)
items, err := listItemByRule(clusterCTX, k8sClient, resource, *parentObject, specifiedFunc, rules.DefaultGenListOptionFunc, rules.DisableFilterByOwnerReference)
if err != nil {
if meta.IsNoMatchError(err) || runtime.IsNotRegisteredError(err) {
log.Logger.Errorf("error to list subresources: %s err: %v", resource.Kind, err)
+3 -3
View File
@@ -1266,14 +1266,14 @@ var _ = Describe("unit-test to e2e test", func() {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(deploy1.DeepCopy())
Expect(err).Should(BeNil())
items, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "apps/v1", Kind: "ReplicaSet"}, unstructured.Unstructured{Object: u},
deploy2RsLabelListOption, nil)
deploy2RsLabelListOption, nil, true)
Expect(err).Should(BeNil())
Expect(len(items)).Should(BeEquivalentTo(2))
u2, err := runtime.DefaultUnstructuredConverter.ToUnstructured(deploy2.DeepCopy())
Expect(err).Should(BeNil())
items2, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "apps/v1", Kind: "ReplicaSet"}, unstructured.Unstructured{Object: u2},
nil, deploy2RsLabelListOption)
nil, deploy2RsLabelListOption, true)
Expect(len(items2)).Should(BeEquivalentTo(1))
// test use ownerReference UId to filter
@@ -1285,7 +1285,7 @@ var _ = Describe("unit-test to e2e test", func() {
Expect(k8sClient.Get(ctx, types2.NamespacedName{Namespace: u3.GetNamespace(), Name: u3.GetName()}, &u3))
Expect(err).Should(BeNil())
items3, err := listItemByRule(ctx, k8sClient, ResourceType{APIVersion: "v1", Kind: "Pod"}, u3,
nil, nil)
nil, nil, true)
Expect(err).Should(BeNil())
Expect(len(items3)).Should(BeEquivalentTo(1))
})
+289
View File
@@ -0,0 +1,289 @@
/*
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 operation
import (
"context"
"fmt"
"io"
"k8s.io/client-go/util/retry"
"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/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/service"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
"github.com/oam-dev/kubevela/pkg/rollout"
errors3 "github.com/oam-dev/kubevela/pkg/utils/errors"
"github.com/pkg/errors"
)
// WorkflowOperator is opratior handler for workflow's resume/rollback/restart
type WorkflowOperator interface {
Suspend(ctx context.Context, app *v1beta1.Application) error
Resume(ctx context.Context, app *v1beta1.Application) error
Rollback(ctx context.Context, app *v1beta1.Application) error
Restart(ctx context.Context, app *v1beta1.Application) error
Terminate(ctx context.Context, app *v1beta1.Application) error
}
// NewWorkflowOperator get an workflow operator with k8sClient and ioWriter(optional, useful for cli)
func NewWorkflowOperator(cli client.Client, w io.Writer) WorkflowOperator {
return wfOperator{cli: cli, outputWriter: w}
}
type wfOperator struct {
cli client.Client
outputWriter io.Writer
}
// Suspend a running workflow
func (wo wfOperator) Suspend(ctx context.Context, app *v1beta1.Application) error {
if app.Status.Workflow == nil {
return fmt.Errorf("the workflow in application is not running")
}
var err error
if err = rollout.SuspendRollout(context.Background(), wo.cli, app, wo.outputWriter); err != nil {
return err
}
appKey := client.ObjectKeyFromObject(app)
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := wo.cli.Get(ctx, appKey, app); err != nil {
return err
}
// set the workflow suspend to true
app.Status.Workflow.Suspend = true
return wo.cli.Status().Patch(ctx, app, client.Merge)
}); err != nil {
return err
}
return wo.writeOutputF("Successfully suspend workflow: %s\n", app.Name)
}
// Resume a suspending workflow
func (wo wfOperator) Resume(ctx context.Context, app *v1beta1.Application) error {
if app.Status.Workflow == nil {
return fmt.Errorf("the workflow in application is not running")
}
if app.Status.Workflow.Terminated {
return fmt.Errorf("can not resume a terminated workflow")
}
var rolloutResumed bool
var err error
if rolloutResumed, err = rollout.ResumeRollout(context.Background(), wo.cli, app, wo.outputWriter); err != nil {
return err
}
if !rolloutResumed && !app.Status.Workflow.Suspend {
return wo.writeOutput("the workflow is not suspending")
}
if app.Status.Workflow.Suspend {
if err = service.ResumeWorkflow(ctx, wo.cli, app); err != nil {
return err
}
}
return nil
}
// Rollback a running in middle state workflow.
//nolint
func (wo wfOperator) Rollback(ctx context.Context, app *v1beta1.Application) error {
if oam.GetPublishVersion(app) == "" {
return fmt.Errorf("app without public version cannot rollback")
}
appRevs, err := application.GetSortedAppRevisions(ctx, wo.cli, app.Name, app.Namespace)
if err != nil {
return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name)
}
// find succeeded revision to rollback
var rev *v1beta1.ApplicationRevision
var outdatedRev []*v1beta1.ApplicationRevision
for i := range appRevs {
candidate := appRevs[len(appRevs)-i-1]
_rev := candidate.DeepCopy()
if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" {
outdatedRev = append(outdatedRev, _rev)
continue
}
rev = _rev
break
}
if rev == nil {
return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name)
}
publishVersion := oam.GetPublishVersion(rev)
revisionNumber, err := utils.ExtractRevision(rev.Name)
if err != nil {
return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name)
}
_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, wo.cli, app)
if err != nil {
return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name)
}
var matchRT *v1beta1.ResourceTracker
for _, rt := range append(historyRTs, currentRT) {
if rt == nil {
continue
}
labels := rt.GetLabels()
if labels != nil && labels[oam.LabelAppRevision] == rev.Name {
matchRT = rt.DeepCopy()
}
}
if matchRT == nil {
return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name)
}
if matchRT.DeletionTimestamp != nil {
return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name)
}
err = wo.writeOutput("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n")
if err != nil {
return err
}
appKey := client.ObjectKeyFromObject(app)
// rollback application spec and freeze
controllerRequirement, err := utils.FreezeApplication(ctx, wo.cli, app, func() {
app.Spec = rev.Spec.Application.Spec
oam.SetPublishVersion(app, publishVersion)
})
if err != nil {
return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
}
err = wo.writeOutput("Application spec rollback successfully.\n")
if err != nil {
return err
}
// rollback application status
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = wo.cli.Get(ctx, appKey, app); err != nil {
return err
}
app.Status.Workflow = rev.Status.Workflow
app.Status.Services = []common.ApplicationComponentStatus{}
app.Status.AppliedResources = []common.ClusterObjectReference{}
for _, rsc := range matchRT.Spec.ManagedResources {
app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference)
}
app.Status.LatestRevision = &common.Revision{
Name: rev.Name,
Revision: int64(revisionNumber),
RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash],
}
return wo.cli.Status().Update(ctx, app)
}); err != nil {
return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
}
err = wo.writeOutput("Application status rollback successfully.\n")
if err != nil {
return err
}
// update resource tracker generation
matchRTKey := client.ObjectKeyFromObject(matchRT)
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = wo.cli.Get(ctx, matchRTKey, matchRT); err != nil {
return err
}
matchRT.Spec.ApplicationGeneration = app.Generation
return wo.cli.Update(ctx, matchRT)
}); err != nil {
return errors.Wrapf(err, "failed to update application generation in resource tracker")
}
// unfreeze application
if err = utils.UnfreezeApplication(ctx, wo.cli, app, nil, controllerRequirement); err != nil {
return errors.Wrapf(err, "failed to resume application to restart")
}
rollback, err := rollout.RollbackRollout(ctx, wo.cli, app, wo.outputWriter)
if err != nil {
return err
}
if rollback {
err = wo.writeOutput("Successfully rollback rollout")
if err != nil {
return err
}
}
// clean up outdated revisions
var errs errors3.ErrorList
for _, _rev := range outdatedRev {
if err = wo.cli.Delete(ctx, _rev); err != nil {
errs = append(errs, err)
}
}
if errs.HasError() {
return errors.Wrapf(errs, "failed to clean up outdated revisions")
}
err = wo.writeOutput("Application outdated revision cleaned up.\n")
if err != nil {
return err
}
return nil
}
// Restart a terminated or finished workflow.
func (wo wfOperator) Restart(ctx context.Context, app *v1beta1.Application) error {
if app.Status.Workflow == nil {
return fmt.Errorf("the workflow in application is not running")
}
// reset the workflow status to restart the workflow
app.Status.Workflow = nil
if err := wo.cli.Status().Update(context.TODO(), app); err != nil {
return err
}
return wo.writeOutputF("Successfully restart workflow: %s\n", app.Name)
}
func (wo wfOperator) Terminate(ctx context.Context, app *v1beta1.Application) error {
if err := service.TerminateWorkflow(context.TODO(), wo.cli, app); err != nil {
return err
}
return nil
}
func (wo wfOperator) writeOutput(str string) error {
if wo.outputWriter == nil {
return nil
}
_, err := wo.outputWriter.Write([]byte(str))
return err
}
func (wo wfOperator) writeOutputF(format string, a ...interface{}) error {
if wo.outputWriter == nil {
return nil
}
_, err := wo.outputWriter.Write([]byte(fmt.Sprintf(format, a...)))
return err
}
+215
View File
@@ -0,0 +1,215 @@
/*
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 operation
import (
"context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"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("Kruise rollout test", func() {
ctx := context.Background()
BeforeEach(func() {
Expect(k8sClient.Create(ctx, myRollout.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, rt.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, app.DeepCopy())).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
})
It("Suspend workflow", func() {
checkApp := v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
checkApp.Status.Workflow = &common.WorkflowStatus{Suspend: false, StartTime: metav1.Now()}
Expect(k8sClient.Status().Update(ctx, &checkApp)).Should(BeNil())
operator := NewWorkflowOperator(k8sClient, nil)
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
Expect(operator.Suspend(ctx, checkApp.DeepCopy())).Should(BeNil())
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
Expect(checkApp.Status.Workflow.Suspend).Should(BeEquivalentTo(true))
})
It("Resume workflow", func() {
checkApp := v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
operator := NewWorkflowOperator(k8sClient, nil)
Expect(operator.Resume(ctx, &checkApp)).Should(BeNil())
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
Expect(checkApp.Status.Workflow.Suspend).Should(BeEquivalentTo(false))
})
It("Terminate workflow", func() {
checkApp := v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
operator := NewWorkflowOperator(k8sClient, nil)
Expect(operator.Terminate(ctx, &checkApp)).Should(BeNil())
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
Expect(checkApp.Status.Workflow.Terminated).Should(BeEquivalentTo(true))
})
It("Restart workflow", func() {
checkApp := v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
operator := NewWorkflowOperator(k8sClient, nil)
Expect(operator.Restart(ctx, &checkApp)).Should(BeNil())
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
Expect(checkApp.Status.Workflow).Should(BeNil())
})
It("Rollback workflow", func() {
Expect(k8sClient.Create(ctx, &appRev)).Should(BeNil())
checkAppRev := v1beta1.ApplicationRevision{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app-v1"}, &checkAppRev)).Should(BeNil())
checkAppRev.Status.Succeeded = true
Expect(k8sClient.Status().Update(ctx, checkAppRev.DeepCopy())).Should(BeNil())
checkApp := v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
checkApp.Annotations = map[string]string{
oam.AnnotationPublishVersion: "v2",
}
operator := NewWorkflowOperator(k8sClient, nil)
Expect(operator.Rollback(ctx, checkApp.DeepCopy())).Should(BeNil())
checkApp = v1beta1.Application{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "opt-app"}, &checkApp)).Should(BeNil())
// must rollback to v1
Expect(oam.GetPublishVersion(&checkApp)).Should(BeEquivalentTo("v1"))
Expect(checkApp.Status.LatestRevision.Name).Should(BeEquivalentTo("opt-app-v1"))
Expect(checkApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
})
})
var app = v1beta1.Application{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "Application",
},
ObjectMeta: metav1.ObjectMeta{
Name: "opt-app",
Namespace: "default",
Generation: 1,
Labels: map[string]string{
oam.AnnotationPublishVersion: "v2",
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{},
},
}
var rt = v1beta1.ResourceTracker{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "ResourceTracker",
},
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-app",
Labels: map[string]string{
"app.oam.dev/appRevision": "opt-app-v1",
"app.oam.dev/name": "opt-app",
"app.oam.dev/namespace": "default",
},
},
Spec: v1beta1.ResourceTrackerSpec{
ApplicationGeneration: 1,
Type: v1beta1.ResourceTrackerTypeVersioned,
ManagedResources: []v1beta1.ManagedResource{
{
ClusterObjectReference: common.ClusterObjectReference{
ObjectReference: v1.ObjectReference{
APIVersion: "rollouts.kruise.io/v1alpha1",
Kind: "Rollout",
Name: "my-rollout",
Namespace: "default",
},
},
OAMObjectReference: common.OAMObjectReference{
Component: "my-rollout",
},
},
},
},
}
var appRev = v1beta1.ApplicationRevision{
TypeMeta: metav1.TypeMeta{
APIVersion: "core.oam.dev/v1beta1",
Kind: "ApplicationRevision",
},
ObjectMeta: metav1.ObjectMeta{
Name: "opt-app-v1",
Namespace: "default",
Labels: map[string]string{
"app.oam.dev/name": "opt-app",
},
Annotations: map[string]string{
oam.AnnotationPublishVersion: "v1",
},
},
Spec: v1beta1.ApplicationRevisionSpec{
Application: v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{},
},
},
},
}
var myRollout = kruisev1alpha1.Rollout{
TypeMeta: metav1.TypeMeta{
APIVersion: "rollouts.kruise.io/v1alpha1",
Kind: "Rollout",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-rollout",
Namespace: "default",
},
Spec: kruisev1alpha1.RolloutSpec{
ObjectRef: kruisev1alpha1.ObjectRef{
WorkloadRef: &kruisev1alpha1.WorkloadRef{
APIVersion: "appsv1",
Kind: "Deployment",
Name: "canary-demo",
},
},
Strategy: kruisev1alpha1.RolloutStrategy{
Canary: &kruisev1alpha1.CanaryStrategy{
Steps: []kruisev1alpha1.CanaryStep{
{
Weight: 30,
},
},
},
Paused: false,
},
},
}
+119
View File
@@ -0,0 +1,119 @@
/*
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 operation
import (
"context"
"path/filepath"
"testing"
"time"
"k8s.io/client-go/discovery"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
ocmworkv1 "open-cluster-management.io/api/work/v1"
v12 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/cue/packages"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
// +kubebuilder:scaffold:imports
)
var cfg *rest.Config
var scheme *runtime.Scheme
var k8sClient client.Client
var testEnv *envtest.Environment
var dm discoverymapper.DiscoveryMapper
var pd *packages.PackageDiscover
var testns string
var dc *discovery.DiscoveryClient
func TestAddon(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Kruise rollout Suite test",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "charts", "vela-core", "crds"), filepath.Join("", "testdata")},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
scheme = runtime.NewScheme()
Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(crdv1.AddToScheme(scheme)).NotTo(HaveOccurred())
_ = ocmclusterv1alpha1.Install(scheme)
_ = ocmclusterv1.Install(scheme)
_ = ocmworkv1.Install(scheme)
_ = kruisev1alpha1.AddToScheme(scheme)
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
dc, err = discovery.NewDiscoveryClientForConfig(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(dc).ShouldNot(BeNil())
dm, err = discoverymapper.New(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(dm).ToNot(BeNil())
pd, err = packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(pd).ToNot(BeNil())
testns = "vela-system"
Expect(k8sClient.Create(context.Background(),
&v12.Namespace{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, ObjectMeta: metav1.ObjectMeta{
Name: testns,
}}))
close(done)
}, 120)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
+290
View File
@@ -0,0 +1,290 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
creationTimestamp: null
name: rollouts.rollouts.kruise.io
spec:
group: rollouts.kruise.io
names:
kind: Rollout
listKind: RolloutList
plural: rollouts
singular: rollout
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: The rollout status phase
jsonPath: .status.phase
name: STATUS
type: string
- description: The rollout canary status step
jsonPath: .status.canaryStatus.currentStepIndex
name: CANARY_STEP
type: integer
- description: The rollout canary status step state
jsonPath: .status.canaryStatus.currentStepState
name: CANARY_STATE
type: string
- description: The rollout canary status message
jsonPath: .status.message
name: MESSAGE
type: string
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: Rollout is the Schema for the rollouts 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: RolloutSpec defines the desired state of Rollout
properties:
objectRef:
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file
ObjectRef indicates workload'
properties:
workloadRef:
description: WorkloadRef contains enough information to let you
identify a workload for Rollout Batch release of the bypass
properties:
apiVersion:
description: API Version of the referent
type: string
kind:
description: Kind of the referent
type: string
name:
description: Name of the referent
type: string
required:
- apiVersion
- kind
- name
type: object
type: object
strategy:
description: rollout strategy
properties:
canary:
description: CanaryStrategy defines parameters for a Replica Based
Canary
properties:
steps:
description: Steps define the order of phases to execute release
in batches(20%, 40%, 60%, 80%, 100%)
items:
description: CanaryStep defines a step of a canary workload.
properties:
pause:
description: Pause defines a pause stage for a rollout,
manual or auto
properties:
duration:
description: Duration the amount of time to wait
before moving to the next step.
format: int32
type: integer
type: object
replicas:
anyOf:
- type: integer
- type: string
description: 'Replicas is the number of expected canary
pods in this batch it can be an absolute number (ex:
5) or a percentage of total pods.'
x-kubernetes-int-or-string: true
weight:
description: SetWeight sets what percentage of the canary
pods should receive
format: int32
type: integer
type: object
type: array
trafficRoutings:
description: TrafficRoutings hosts all the supported service
meshes supported to enable more fine-grained traffic routing
todo current only support one
items:
description: TrafficRouting hosts all the different configuration
for supported service meshes to enable more fine-grained
traffic routing
properties:
gracePeriodSeconds:
description: Optional duration in seconds the traffic
provider(e.g. nginx ingress controller) consumes the
service, ingress configuration changes gracefully.
format: int32
type: integer
ingress:
description: Ingress holds Ingress specific configuration
to route traffic, e.g. Nginx, Alb.
properties:
name:
description: Name refers to the name of an `Ingress`
resource in the same namespace as the `Rollout`
type: string
required:
- name
type: object
service:
description: Service holds the name of a service which
selects pods with stable version and don't select
any pods with canary version.
type: string
type:
description: nginx, alb, istio etc.
type: string
required:
- service
- type
type: object
type: array
type: object
paused:
description: Paused indicates that the Rollout is paused. Default
value is false
type: boolean
type: object
required:
- objectRef
- strategy
type: object
status:
description: RolloutStatus defines the observed state of Rollout
properties:
canaryStatus:
description: Canary describes the state of the canary rollout
properties:
canaryReadyReplicas:
description: CanaryReadyReplicas the numbers of ready canary revision
pods
format: int32
type: integer
canaryReplicas:
description: CanaryReplicas the numbers of canary revision pods
format: int32
type: integer
canaryRevision:
description: CanaryRevision is calculated by rollout based on
podTemplateHash, and the internal logic flow uses It may be
different from rs podTemplateHash in different k8s versions,
so it cannot be used as service selector label
type: string
canaryService:
description: CanaryService holds the name of a service which selects
pods with canary version and don't select any pods with stable
version.
type: string
currentStepIndex:
description: CurrentStepIndex defines the current step of the
rollout is on. If the current step index is null, the controller
will execute the rollout.
format: int32
type: integer
currentStepState:
type: string
lastReadyTime:
description: The last time this step pods is ready.
format: date-time
type: string
message:
type: string
observedWorkloadGeneration:
description: observedWorkloadGeneration is the most recent generation
observed for this Rollout ref workload generation.
format: int64
type: integer
podTemplateHash:
description: pod template hash is used as service selector label
type: string
rolloutHash:
description: RolloutHash from rollout.spec object
type: string
required:
- canaryReadyReplicas
- canaryReplicas
- canaryService
- currentStepState
- podTemplateHash
type: object
conditions:
description: Conditions a list of conditions a rollout can have.
items:
description: RolloutCondition describes the state of a rollout at
a certain point.
properties:
lastTransitionTime:
description: Last time the condition transitioned from one status
to another.
format: date-time
type: string
lastUpdateTime:
description: The last time this condition was updated.
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: Phase of the condition, one of True, False, Unknown.
type: string
type:
description: Type of rollout condition.
type: string
required:
- message
- reason
- status
- type
type: object
type: array
message:
description: Message provides details on why the rollout is in its
current phase
type: string
observedGeneration:
description: observedGeneration is the most recent generation observed
for this Rollout.
format: int64
type: integer
phase:
description: BlueGreenStatus *BlueGreenStatus `json:"blueGreenStatus,omitempty"`
Phase is the rollout phase.
type: string
stableRevision:
description: CanaryRevision the hash of the canary pod template CanaryRevision
string `json:"canaryRevision,omitempty"` StableRevision indicates
the revision pods that has successfully rolled out
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []