Merge pull request #1142 from ryanzhang-oss/e2e-test

add rollout cloneset based rudimentory e2e test
This commit is contained in:
Jianbo Sun
2021-03-05 12:15:49 +08:00
committed by GitHub
10 changed files with 460 additions and 31 deletions
@@ -64,7 +64,10 @@ func makeHTTPRequest(ctx context.Context, webhookEndPoint, method string, payloa
// failed even with retry
if err != nil {
return nil, r.StatusCode, err
if r != nil {
return nil, r.StatusCode, err
}
return nil, -1, err
}
return body, r.StatusCode, nil
}
@@ -18,7 +18,7 @@ const mockUrl = "127.0.0.1:4848"
func Test_MakeHTTPRequest(t *testing.T) {
ctx := context.TODO()
type parameter struct {
type mockHTTPParameter struct {
method string
statusCode int
body string
@@ -29,15 +29,16 @@ func Test_MakeHTTPRequest(t *testing.T) {
body string
}
tests := map[string]struct {
method string
payload interface{}
parameter parameter
want want
url string
method string
payload interface{}
httpParameter mockHTTPParameter
want want
}{
"Test normal case": {
method: http.MethodPost,
payload: "doesn't matter",
parameter: parameter{
httpParameter: mockHTTPParameter{
method: http.MethodPost,
statusCode: http.StatusAccepted,
body: "all good",
@@ -48,10 +49,25 @@ func Test_MakeHTTPRequest(t *testing.T) {
body: "all good",
},
},
"Test http failed case with retry": {
url: "127.0.0.1:13622",
method: http.MethodPost,
payload: "doesn't matter",
httpParameter: mockHTTPParameter{
method: http.MethodGet,
statusCode: http.StatusAccepted,
body: "doesn't matter",
},
want: want{
err: fmt.Errorf("internal server error, status code = %d", http.StatusNotImplemented),
statusCode: -1,
body: "",
},
},
"Test failed case with retry": {
method: http.MethodPost,
payload: "doesn't matter",
parameter: parameter{
httpParameter: mockHTTPParameter{
method: http.MethodPost,
statusCode: http.StatusNotImplemented,
body: "please retry",
@@ -65,7 +81,7 @@ func Test_MakeHTTPRequest(t *testing.T) {
"Test client error failed case": {
method: http.MethodPost,
payload: "doesn't matter",
parameter: parameter{
httpParameter: mockHTTPParameter{
method: http.MethodPost,
statusCode: http.StatusBadRequest,
body: "bad request",
@@ -80,22 +96,33 @@ func Test_MakeHTTPRequest(t *testing.T) {
for testName, tt := range tests {
t.Run(testName, func(t *testing.T) {
// generate a test server so we can capture and inspect the request
testServer := NewMock(tt.parameter.method, tt.parameter.statusCode, tt.parameter.body)
testServer := NewMock(tt.httpParameter.method, tt.httpParameter.statusCode, tt.httpParameter.body)
defer testServer.Close()
gotReply, gotCode, gotErr := makeHTTPRequest(ctx, "http://"+mockUrl, tt.method, tt.payload)
if (tt.want.err == nil && gotErr != nil) || (tt.want.err != nil && gotErr == nil) {
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
}
if tt.want.err != nil && gotErr != nil && gotErr.Error() != tt.want.err.Error() {
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
}
if string(gotReply) != tt.want.body {
t.Errorf("\n%s\nr.Reconcile(...): want reply `%s`, got reply:`%s`\n", testName, tt.want.body, string(gotReply))
if len(tt.url) == 0 {
tt.url = mockUrl
}
gotReply, gotCode, gotErr := makeHTTPRequest(ctx, "http://"+tt.url, tt.method, tt.payload)
if gotCode != tt.want.statusCode {
t.Errorf("\n%s\nr.Reconcile(...): want code `%d`, got code:`%d`\n", testName, tt.want.statusCode,
gotCode)
}
if gotCode == -1 {
// we don't know exactly what error we should get when the network call failed
if gotErr == nil {
t.Errorf("\n%s\nr.Reconcile(...): want some error, got error:`%s`\n", testName, gotErr)
}
} else {
if (tt.want.err == nil && gotErr != nil) || (tt.want.err != nil && gotErr == nil) {
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
}
if tt.want.err != nil && gotErr != nil && gotErr.Error() != tt.want.err.Error() {
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
}
}
if string(gotReply) != tt.want.body {
t.Errorf("\n%s\nr.Reconcile(...): want reply `%s`, got reply:`%s`\n", testName, tt.want.body, string(gotReply))
}
})
}
}
+207
View File
@@ -0,0 +1,207 @@
package controllers_test
import (
"context"
"math/rand"
"strconv"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
oamstd "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
var _ = Describe("Test Rolling out Application", func() {
ctx := context.Background()
namespace := "rolling"
var ns corev1.Namespace
BeforeEach(func() {
logf.Log.Info("Start to run a test, clean up previous resources")
namespace = string(strconv.AppendInt([]byte(namespace), rand.Int63(), 16))
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).
Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
logf.Log.Info("make sure all the resources are removed")
objectKey := client.ObjectKey{
Name: namespace,
}
res := &corev1.Namespace{}
Eventually(
func() error {
return k8sClient.Get(ctx, objectKey, res)
},
time.Second*120, time.Millisecond*500).Should(&util.NotFoundMatcher{})
Eventually(
func() error {
return k8sClient.Create(ctx, &ns)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
By("Install CloneSet based workloadDefinition")
var cd v1alpha2.WorkloadDefinition
Expect(readYaml("testdata/rollout/clonesetDefinition.yaml", &cd)).Should(BeNil())
// create the workloadDefinition if not exist
Eventually(
func() error {
return k8sClient.Create(ctx, &cd)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
})
AfterEach(func() {
logf.Log.Info("Clean up resources")
// delete the namespace with all its resources
Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(BeNil())
})
It("Basic cloneset rollout", func() {
By("Apply an application")
var app v1alpha2.Application
Expect(readYaml("testdata/rollout/app-source.yaml", &app)).Should(BeNil())
app.Namespace = namespace
Expect(k8sClient.Create(ctx, &app)).Should(Succeed())
By("Get Application latest status after AppConfig created")
Eventually(
func() *v1alpha2.Revision {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Name}, &app)
return app.Status.LatestRevision
},
time.Second*30, time.Millisecond*500).ShouldNot(BeNil())
By("Wait for AppConfig1 synced")
var appConfig1 v1alpha2.ApplicationConfiguration
Eventually(
func() corev1.ConditionStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Status.LatestRevision.Name}, &appConfig1)
return appConfig1.Status.GetCondition(v1alpha1.TypeSynced).Status
},
time.Second*30, time.Millisecond*500).Should(BeEquivalentTo(corev1.ConditionTrue))
By("Mark the application as rolling")
Expect(readYaml("testdata/rollout/app-source-prep.yaml", &app)).Should(BeNil())
app.Namespace = namespace
Expect(k8sClient.Update(ctx, &app)).Should(Succeed())
By("Wait for AppConfig1 to be templated")
Eventually(
func() v1alpha2.RollingStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Status.LatestRevision.Name}, &appConfig1)
return appConfig1.Status.RollingStatus
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(v1alpha2.RollingTemplated))
By("Update the application during rolling")
Expect(readYaml("testdata/rollout/app-target.yaml", &app)).Should(BeNil())
app.Namespace = namespace
Expect(k8sClient.Update(ctx, &app)).Should(Succeed())
By("Get Application latest status after AppConfig created")
Eventually(
func() int64 {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Name}, &app)
return app.Status.LatestRevision.Revision
},
time.Second*10, time.Millisecond*500).ShouldNot(BeEquivalentTo(1))
By("Wait for AppConfig2 synced")
var appConfig2 v1alpha2.ApplicationConfiguration
Eventually(
func() corev1.ConditionStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Status.LatestRevision.Name}, &appConfig2)
return appConfig2.Status.GetCondition(v1alpha1.TypeSynced).Status
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(corev1.ConditionTrue))
By("Wait for AppConfig2 to be templated")
Eventually(
func() v1alpha2.RollingStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Status.LatestRevision.Name}, &appConfig2)
return appConfig2.Status.RollingStatus
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(v1alpha2.RollingTemplated))
By("Get the cloneset workload")
var kc kruise.CloneSet
workloadName := utils.ExtractComponentName(appConfig2.Spec.Components[0].RevisionName)
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workloadName},
&kc)).ShouldNot(HaveOccurred())
Expect(kc.Spec.UpdateStrategy.Paused).Should(BeTrue())
By("Apply the application rollout that stops after two batches")
var appDeploy v1alpha2.ApplicationDeployment
Expect(readYaml("testdata/rollout/app-deploy-pause.yaml", &appDeploy)).Should(BeNil())
appDeploy.Namespace = namespace
Expect(k8sClient.Create(ctx, &appDeploy)).Should(Succeed())
By("Wait for the rollout phase change to rolling in batches")
Eventually(
func() oamstd.RollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appDeploy.Name}, &appDeploy)
return appDeploy.Status.RollingState
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
By("Wait for rollout to finish two batches")
Eventually(
func() int32 {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appDeploy.Name}, &appDeploy)
return appDeploy.Status.CurrentBatch
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(1))
By("Verify that the rollout stops at two batches")
// wait for the batch to be ready
Eventually(
func() oamstd.BatchRollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appDeploy.Name}, &appDeploy)
return appDeploy.Status.BatchRollingState
},
time.Second*60, time.Millisecond*500).Should(Equal(oamstd.BatchReadyState))
// wait for 30 seconds, it should still be at 1
time.Sleep(30 * time.Second)
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appDeploy.Name}, &appDeploy)
Expect(appDeploy.Status.CurrentBatch).Should(BeEquivalentTo(1))
Expect(appDeploy.Status.BatchRollingState).Should(BeEquivalentTo(oamstd.BatchReadyState))
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workloadName},
&kc)).ShouldNot(HaveOccurred())
Expect(kc.Status.UpdatedReplicas).Should(BeEquivalentTo(3))
Expect(kc.Status.UpdatedReadyReplicas).Should(BeEquivalentTo(3))
By("Finish the application rollout")
Expect(readYaml("testdata/rollout/app-deploy-finish.yaml", &appDeploy)).Should(BeNil())
appDeploy.Namespace = namespace
Expect(k8sClient.Update(ctx, &appDeploy)).Should(Succeed())
By("Wait for the rollout phase change to succeeded")
Eventually(
func() oamstd.RollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appDeploy.Name}, &appDeploy)
return appDeploy.Status.RollingState
},
time.Second*60, time.Millisecond*500).Should(Equal(oamstd.RolloutSucceedState))
By("Wait for rollout to finish two batches")
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: workloadName},
&kc)).ShouldNot(HaveOccurred())
Expect(kc.Status.UpdatedReplicas).Should(BeEquivalentTo(5))
Expect(kc.Status.UpdatedReadyReplicas).Should(BeEquivalentTo(5))
// Clean up
k8sClient.Delete(ctx, &appDeploy)
k8sClient.Delete(ctx, &appConfig2)
k8sClient.Delete(ctx, &appConfig1)
k8sClient.Delete(ctx, &app)
})
})
+12 -12
View File
@@ -19,10 +19,12 @@ import (
"context"
"encoding/json"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
@@ -75,6 +77,8 @@ var _ = BeforeSuite(func(done Done) {
Expect(err).Should(BeNil())
err = crdv1.AddToScheme(scheme)
Expect(err).Should(BeNil())
err = kruise.AddToScheme(scheme)
Expect(err).Should(BeNil())
depExample := &unstructured.Unstructured{}
depExample.SetGroupVersionKind(schema.GroupVersionKind{
Group: "example.com",
@@ -94,15 +98,6 @@ var _ = BeforeSuite(func(done Done) {
}
By("Finished setting up test environment")
By("Applying CRD of WorkloadDefinition and TraitDefinition")
var workloadDefinitionCRD crdv1.CustomResourceDefinition
Expect(readYaml("../../charts/vela-core/crds/core.oam.dev_workloaddefinitions.yaml", &workloadDefinitionCRD)).Should(BeNil())
Expect(k8sClient.Create(context.Background(), &workloadDefinitionCRD)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
var traitDefinitionCRD crdv1.CustomResourceDefinition
Expect(readYaml("../../charts/vela-core/crds/core.oam.dev_traitdefinitions.yaml", &traitDefinitionCRD)).Should(BeNil())
Expect(k8sClient.Create(context.Background(), &traitDefinitionCRD)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
// Create manual scaler trait definition
manualscalertrait = v1alpha2.TraitDefinition{
ObjectMeta: metav1.ObjectMeta{
@@ -263,7 +258,11 @@ var _ = BeforeSuite(func(done Done) {
By("Create workload definition for revision mechanism test")
var nwd v1alpha2.WorkloadDefinition
Expect(readYaml("testdata/revision/workload-def.yaml", &nwd)).Should(BeNil())
Expect(k8sClient.Create(context.Background(), &nwd)).Should(Succeed())
Eventually(
func() error {
return k8sClient.Create(context.Background(), &nwd)
},
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
close(done)
}, 300)
@@ -288,12 +287,13 @@ var _ = AfterSuite(func() {
Expect(k8sClient.Delete(context.Background(), &crd)).Should(BeNil())
By("Deleted the custom resource definition")
By("Deleting all the definitions by deleting the definition CRDs")
crd = crdv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "workloaddefinitions.core.oam.dev",
},
}
Expect(k8sClient.Delete(context.Background(), &crd)).Should(BeNil())
Expect(k8sClient.Delete(context.Background(), &crd)).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
By("Deleted the workloaddefinitions CRD")
crd = crdv1.CustomResourceDefinition{
@@ -301,6 +301,6 @@ var _ = AfterSuite(func() {
Name: "traitdefinitions.core.oam.dev",
},
}
Expect(k8sClient.Delete(context.Background(), &crd)).Should(BeNil())
Expect(k8sClient.Delete(context.Background(), &crd)).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{}))
By("Deleted the workloaddefinitions CRD")
})
+18
View File
@@ -0,0 +1,18 @@
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationDeployment
metadata:
name: rolling-e2e-test
spec:
# application (revision) reference
targetApplicationName: test-e2e-rolling-v2
sourceApplicationName: test-e2e-rolling-v1
# HPA reference (optional)
componentList:
- metrics-provider
rolloutPlan:
rolloutStrategy: "IncreaseFirst"
rolloutBatches:
- replicas: 10%
- replicas: 2
- replicas: 2
batchPartition: 2
+18
View File
@@ -0,0 +1,18 @@
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationDeployment
metadata:
name: rolling-e2e-test
spec:
# application (revision) reference
targetApplicationName: test-e2e-rolling-v2
sourceApplicationName: test-e2e-rolling-v1
# HPA reference (optional)
componentList:
- metrics-provider
rolloutPlan:
rolloutStrategy: "IncreaseFirst"
rolloutBatches:
- replicas: 10%
- replicas: 2
- replicas: 2
batchPartition: 1
+18
View File
@@ -0,0 +1,18 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: test-e2e-rolling
annotations:
"app.oam.dev/rolling-components": "metrics-provider"
"app.oam.dev/rollout-template": "true"
spec:
components:
- name: metrics-provider
type: clonesetservice
settings:
cmd:
- ./podinfo
- stress-cpu=1
image: stefanprodan/podinfo:4.0.6
port: 8080
updateStrategyType: InPlaceIfPossible
+15
View File
@@ -0,0 +1,15 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: test-e2e-rolling
spec:
components:
- name: metrics-provider
type: clonesetservice
settings:
cmd:
- ./podinfo
- stress-cpu=1
image: stefanprodan/podinfo:4.0.6
port: 8080
updateStrategyType: InPlaceIfPossible
+18
View File
@@ -0,0 +1,18 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: test-e2e-rolling
annotations:
"app.oam.dev/rolling-components": "metrics-provider"
"app.oam.dev/rollout-template": "true"
spec:
components:
- name: metrics-provider
type: clonesetservice
settings:
cmd:
- ./podinfo
- stress-cpu=1
image: stefanprodan/podinfo:5.0.2
port: 8080
updateStrategyType: InPlaceIfPossible
+105
View File
@@ -0,0 +1,105 @@
# Code generated by KubeVela templates. DO NOT EDIT.
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: clonesetservice
annotations:
definition.oam.dev/description: "Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers.
If workload type is skipped for any service defined in Appfile, it will be defaulted to `webservice` type."
spec:
definitionRef:
name: clonesets.apps.kruise.io
schematic:
cue:
template: |
output: {
apiVersion: "apps.kruise.io/v1alpha1"
kind: "CloneSet"
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
replicas: parameter.replicas
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
if parameter["updateStrategyType"] != _|_ {
updateStrategy: {
type: parameter.updateStrategyType
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
// +usage=Which port do you want customer traffic sent to
// +short=p
port: *80 | int
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
// +usage=Cloneset updateStrategy, candidates are `ReCreate`/`InPlaceIfPossible`/`InPlaceOnly`
updateStrategyType?: string
// +usage=Number of pods in the cloneset
replicas: *5 | int
}