Merge pull request #1112 from ryanzhang-oss/rollout-e2e

Rollout e2e demo
This commit is contained in:
Jianbo Sun
2021-03-01 19:25:40 +08:00
committed by GitHub
37 changed files with 569 additions and 224 deletions
+1 -1
View File
@@ -296,7 +296,7 @@ type ComponentStatus struct {
type Revision struct {
Name string `json:"name"`
Revision int64 `json:"revision"`
RevisionHash string `json:"revisionHash"`
RevisionHash string `json:"revisionHash,omitempty"`
}
// +kubebuilder:object:root=true
@@ -98,7 +98,7 @@ type RolloutPlan struct {
// This is designed for the operators to manually rollout
// Default is the the number of batches which will rollout all the batches
// +optional
BatchPartition *int32 `json:"lastBatchToRollout,omitempty"`
BatchPartition *int32 `json:"batchPartition,omitempty"`
// Paused the rollout, default is false
// +optional
@@ -157,11 +157,13 @@ func (r *RolloutStatus) RolloutFailed(reason string) {
func (r *RolloutStatus) StateTransition(event RolloutEvent) {
rollingState := r.RollingState
batchRollingState := r.BatchRollingState
defer klog.InfoS("try to execute a rollout state transition",
"pre rolling state", rollingState,
"pre batch rolling state", batchRollingState,
"post rolling state", r.RollingState,
"post batch rolling state", r.BatchRollingState)
defer func() {
klog.InfoS("try to execute a rollout state transition",
"pre rolling state", rollingState,
"pre batch rolling state", batchRollingState,
"post rolling state", r.RollingState,
"post batch rolling state", r.BatchRollingState)
}()
// we have special transition for these two types of event
if event == RollingFailedEvent || event == RollingRetriableFailureEvent {
@@ -45,6 +45,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -96,10 +100,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
@@ -73,6 +73,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources The controller simply replace the old resources with the new one if there is no rollout plan involved
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -124,10 +128,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
@@ -362,7 +362,6 @@ spec:
required:
- name
- revision
- revisionHash
type: object
rollingState:
description: RollingState is the Rollout State
@@ -119,7 +119,6 @@ spec:
required:
- name
- revision
- revisionHash
type: object
observedGeneration:
description: The generation observed by the component controller.
@@ -35,6 +35,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -86,10 +90,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
+2 -1
View File
@@ -27,6 +27,7 @@ import (
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -117,7 +118,7 @@ func main() {
} else {
w = os.Stdout
}
klog.InitFlags(nil)
ctrl.SetLogger(zap.New(func(o *zap.Options) {
o.Development = true
o.DestWritter = w
+1 -1
View File
@@ -5,7 +5,7 @@ metadata:
spec:
components:
- name: express-server4
type: webserver
type: webservice
settings:
cmd:
- node
+14
View File
@@ -0,0 +1,14 @@
# Rollout Example
[kubevela.io](https://kubevela.io)
## Install kruise
```shell
helm install kruise https://github.com/openkruise/kruise/releases/download/v0.7.0/kruise-chart.tgz
kubectl apply -f charts/vela-core/crds
kubectl apply -f charts/vela-core/templates/defwithtemplate
kubectl apply -f docs/examples/rollout/clonesetDefinition.yaml
kubectl apply -f docs/examples/rollout/app-source.yaml
kubectl apply -f docs/examples/rollout/app-source-prep.yaml
kubectl apply -f docs/examples/rollout/app-target.yaml
kubectl apply -f docs/examples/rollout/app-deploy.yaml
```
+18
View File
@@ -0,0 +1,18 @@
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationDeployment
metadata:
name: rolling-test
spec:
# application (revision) reference
targetApplicationName: test-rolling-v2
sourceApplicationName: test-rolling-v1
# HPA reference (optional)
componentList:
- metrics-provider
rolloutPlan:
rolloutStrategy: "IncreaseFirst"
rolloutBatches:
- replicas: 10%
- replicas: 2
- replicas: 2
batchPartition: 2
@@ -0,0 +1,17 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: test-rolling
annotations:
"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-rolling
spec:
components:
- name: metrics-provider
type: clonesetservice
settings:
cmd:
- ./podinfo
- stress-cpu=1
image: stefanprodan/podinfo:4.0.6
port: 8080
updateStrategyType: InPlaceIfPossible
+17
View File
@@ -0,0 +1,17 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: test-rolling
annotations:
"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
@@ -0,0 +1,103 @@
# 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
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
}
@@ -45,6 +45,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -96,10 +100,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
@@ -73,6 +73,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources The controller simply replace the old resources with the new one if there is no rollout plan involved
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -124,10 +128,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
@@ -362,7 +362,6 @@ spec:
required:
- name
- revision
- revisionHash
type: object
rollingState:
description: RollingState is the Rollout State
@@ -119,7 +119,6 @@ spec:
required:
- name
- revision
- revisionHash
type: object
observedGeneration:
description: The generation observed by the component controller.
@@ -35,6 +35,10 @@ spec:
rolloutPlan:
description: RolloutPlan is the details on how to rollout the resources
properties:
batchPartition:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
canaryMetric:
description: CanaryMetric provides a way for the rollout process to automatically check certain metrics before complete the process
items:
@@ -86,10 +90,6 @@ spec:
- name
type: object
type: array
lastBatchToRollout:
description: All pods in the batches up to the batchPartition (included) will have the target resource specification while the rest still have the source resource This is designed for the operators to manually rollout Default is the the number of batches which will rollout all the batches
format: int32
type: integer
numBatches:
description: The number of batches, default = 1
format: int32
@@ -3,9 +3,11 @@ package rollout
import (
"context"
"fmt"
"reflect"
"time"
"github.com/crossplane/crossplane-runtime/pkg/event"
kruisev1 "github.com/openkruise/kruise-api/apps/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
@@ -37,15 +39,22 @@ type Controller struct {
// NewRolloutPlanController creates a RolloutPlanController
func NewRolloutPlanController(client client.Client, parentController oam.Object, recorder event.Recorder,
rolloutSpec *v1alpha1.RolloutPlan,
rolloutStatus *v1alpha1.RolloutStatus, targetWorkload,
sourceWorkload *unstructured.Unstructured) *Controller {
rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus,
targetWorkload, sourceWorkload *unstructured.Unstructured) *Controller {
initializedRolloutStatus := rolloutStatus.DeepCopy()
// use Mutation webhook?
if len(initializedRolloutStatus.RollingState) == 0 {
initializedRolloutStatus.RollingState = v1alpha1.VerifyingState
}
if len(initializedRolloutStatus.BatchRollingState) == 0 {
initializedRolloutStatus.BatchRollingState = v1alpha1.BatchInitializingState
}
return &Controller{
client: client,
parentController: parentController,
recorder: recorder,
rolloutSpec: rolloutSpec.DeepCopy(),
rolloutStatus: rolloutStatus.DeepCopy(),
rolloutStatus: initializedRolloutStatus,
targetWorkload: targetWorkload,
sourceWorkload: sourceWorkload,
}
@@ -56,16 +65,17 @@ func (r *Controller) Reconcile(ctx context.Context) (res reconcile.Result, statu
klog.InfoS("Reconcile the rollout plan", "rollout Spec", r.rolloutSpec,
"target workload", klog.KObj(r.targetWorkload))
if r.sourceWorkload != nil {
klog.InfoS("we will do rolling upgrades", "source workload", klog.KObj(r.sourceWorkload))
klog.InfoS("We will do rolling upgrades", "source workload", klog.KObj(r.sourceWorkload))
}
klog.InfoS("rollout spec ", "rollout state", r.rolloutStatus.RollingState, "batch rolling state",
klog.InfoS("rollout status", "rollout state", r.rolloutStatus.RollingState, "batch rolling state",
r.rolloutStatus.BatchRollingState, "current batch", r.rolloutStatus.CurrentBatch, "upgraded Replicas",
r.rolloutStatus.UpgradedReplicas)
defer klog.InfoS("Finished reconciling rollout plan", "rollout state", status.RollingState,
"batch rolling state", status.BatchRollingState, "current batch", status.CurrentBatch,
"upgraded Replicas", status.UpgradedReplicas, "reconcile result ", res)
defer func() {
klog.InfoS("Finished reconciling rollout plan", "rollout state", status.RollingState,
"batch rolling state", status.BatchRollingState, "current batch", status.CurrentBatch,
"upgraded Replicas", status.UpgradedReplicas, "reconcile result ", res)
}()
status = r.rolloutStatus
defer func() {
@@ -293,6 +303,8 @@ func (r *Controller) validateRollingBatchStatus(totalSize int) bool {
if i < currentBatch {
batchSize, _ := intstr.GetValueFromIntOrPercent(&r.Replicas, totalSize, true)
podCount += batchSize
} else {
break
}
}
// the recorded number should be at least as much as the all the pods before the current batch
@@ -327,12 +339,11 @@ func (r *Controller) GetWorkloadController() (workloads.WorkloadController, erro
Name: r.targetWorkload.GetName(),
}
switch kind {
case "CloneSet":
return workloads.NewCloneSetController(r.client, r.recorder, r.parentController,
r.rolloutSpec, r.rolloutStatus, target), nil
default:
return nil, fmt.Errorf("the workload kind `%s` is not supported", kind)
if r.targetWorkload.GroupVersionKind().Group == kruisev1.GroupVersion.Group {
if r.targetWorkload.GetKind() == reflect.TypeOf(kruisev1.CloneSet{}).Name() {
return workloads.NewCloneSetController(r.client, r.recorder, r.parentController,
r.rolloutSpec, r.rolloutStatus, target), nil
}
}
return nil, fmt.Errorf("the workload kind `%s` is not supported", kind)
}
@@ -92,19 +92,10 @@ func (c *CloneSetController) Verify(ctx context.Context) (status *v1alpha1.Rollo
return
}
// the rollout batch partition is either automatic or zero
if c.rolloutSpec.BatchPartition != nil && *c.rolloutSpec.BatchPartition != 0 {
verifyErr = fmt.Errorf("the rollout plan has to start from zero, partition= %d", *c.rolloutSpec.BatchPartition)
c.rolloutStatus.RolloutFailed(verifyErr.Error())
return
}
// Cloneset should not be in the middle of an upgrade (the number of new version pod should be 0)
oldVersionPod, _ := intstr.GetValueFromIntOrPercent(c.cloneSet.Spec.UpdateStrategy.Partition, int(totalReplicas),
true)
if oldVersionPod != int(totalReplicas) {
verifyErr = fmt.Errorf("the cloneset was still in the middle of updating, number of old pods= %d", oldVersionPod)
c.rolloutStatus.RolloutFailed(verifyErr.Error())
if !c.cloneSet.Spec.UpdateStrategy.Paused {
verifyErr = fmt.Errorf("the cloneset %s is in the middle of updating, need to be paused first",
c.cloneSet.GetName())
c.rolloutStatus.RolloutRetry(verifyErr.Error())
return
}
@@ -117,11 +108,22 @@ func (c *CloneSetController) Verify(ctx context.Context) (status *v1alpha1.Rollo
// Initialize makes sure that
func (c *CloneSetController) Initialize(ctx context.Context) *v1alpha1.RolloutStatus {
if c.fetchCloneSet(ctx) != nil {
totalReplicas, err := c.Size(ctx)
if err != nil {
return c.rolloutStatus
}
// kick start the update and start from every pod in the old version
clonePatch := client.MergeFrom(c.cloneSet.DeepCopyObject())
c.cloneSet.Spec.UpdateStrategy.Paused = false
c.cloneSet.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.Int, IntVal: totalReplicas}
// mark the rollout initialized, there is nothing we need to do for Cloneset for now
// patch the CloneSet
if err := c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
c.recorder.Event(c.parentController, event.Warning("Failed to the start the cloneset update", err))
c.rolloutStatus.RolloutRetry(err.Error())
return c.rolloutStatus
}
// mark the rollout initialized
c.recorder.Event(c.parentController, event.Normal("Initialized", "Rollout resource are initialized"))
c.rolloutStatus.StateTransition(v1alpha1.RollingInitializedEvent)
return c.rolloutStatus
@@ -139,7 +141,7 @@ func (c *CloneSetController) RolloutOneBatchPods(ctx context.Context) *v1alpha1.
IntVal: cloneSetSize - int32(newPodTarget)}
// patch the Cloneset
if err := c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
c.recorder.Event(c.parentController, event.Warning("Failed to patch update the Cloneset", err))
c.recorder.Event(c.parentController, event.Warning("Failed to update the cloneset to upgrade", err))
c.rolloutStatus.RolloutRetry(err.Error())
return c.rolloutStatus
}
@@ -35,7 +35,6 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/appfile"
core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -102,7 +101,6 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
// pass the App label and annotation to ac except some app specific ones
oamutil.PassLabelAndAnnotation(app, ac)
oamutil.RemoveAnnotations(ac, []string{oam.AnnotationAppRollout})
app.Status.SetConditions(readyCondition("Built"))
applog.Info("apply appConfig & component to the cluster")
// apply appConfig & component to the cluster
@@ -113,7 +111,6 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
}
app.Status.SetConditions(readyCondition("Applied"))
app.Status.Phase = v1alpha2.ApplicationHealthChecking
applog.Info("check application health status")
// check application health status
@@ -239,7 +239,7 @@ var _ = Describe("Test Application Controller", func() {
Expect(component.Status.LatestRevision).ShouldNot(BeNil())
// check that the new appconfig has the correct annotation and labels
Expect(appConfig.GetAnnotations()[oam.AnnotationNewAppConfig]).Should(Equal(strconv.FormatBool(true)))
Expect(appConfig.GetAnnotations()[oam.AnnotationAppRollout]).Should(BeEmpty())
Expect(appConfig.GetLabels()[oam.LabelAppConfigHash]).ShouldNot(BeEmpty())
// check the workload created should be the same as the raw data in the component
@@ -647,12 +647,6 @@ var _ = Describe("Test Application Controller", func() {
Expect(json.Unmarshal(component7.Spec.Workload.Raw, gotD3)).Should(BeNil())
fmt.Println(cmp.Diff(gotD3, expDeployment7))
Expect(gotD3).Should(BeEquivalentTo(expDeployment7))
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: app.Namespace,
Name: "myweb6",
}, component6)).Should(&util.NotFoundMatcher{})
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
@@ -821,8 +815,8 @@ var _ = Describe("Test Application Controller", func() {
compName := rolloutApp.Spec.Components[0].Name
// set the annotation
rolloutApp.SetAnnotations(map[string]string{
oam.AnnotationAppRollout: "true",
"keep": "true",
oam.AnnotationAppRollout: strconv.FormatBool(true),
"keep": strconv.FormatBool(true),
})
Expect(k8sClient.Create(ctx, rolloutApp)).Should(BeNil())
@@ -860,10 +854,9 @@ var _ = Describe("Test Application Controller", func() {
Expect(component.Status.LatestRevision.Revision).Should(
SatisfyAny(BeEquivalentTo(1), BeEquivalentTo(2)))
// check that the new appconfig has the correct annotation and labels
Expect(appConfig.GetAnnotations()[oam.AnnotationNewAppConfig]).Should(Equal(strconv.FormatBool(true)))
Expect(appConfig.GetAnnotations()[oam.AnnotationAppRollout]).Should(Equal(strconv.FormatBool(true)))
Expect(appConfig.GetAnnotations()[oam.AnnotationRollingComponent]).Should(Equal(component.Status.LatestRevision.Name))
Expect(appConfig.GetAnnotations()["keep"]).Should(Equal("true"))
Expect(appConfig.GetAnnotations()[oam.AnnotationAppRollout]).Should(BeEmpty())
Expect(appConfig.GetLabels()[oam.LabelAppConfigHash]).ShouldNot(BeEmpty())
Expect(appConfig.Spec.Components[0].ComponentName).Should(BeEmpty())
Expect(appConfig.Spec.Components[0].RevisionName).Should(Equal(component.Status.LatestRevision.Name))
@@ -82,14 +82,7 @@ func (h *appHandler) apply(ctx context.Context, ac *v1alpha2.ApplicationConfigur
Controller: pointer.BoolPtr(true),
}}
ac.SetOwnerReferences(owners)
hasRolloutLogic := false
var newComponents []string
// Check if we are doing rolling out
if _, exist := h.app.GetAnnotations()[oam.AnnotationAppRollout]; exist || h.app.Spec.RolloutPlan != nil {
h.logger.Info("The application rolling out is controlled by a rollout plan")
hasRolloutLogic = true
}
for _, comp := range comps {
comp.SetOwnerReferences(owners)
newComp := comp.DeepCopy()
@@ -98,7 +91,7 @@ func (h *appHandler) apply(ctx context.Context, ac *v1alpha2.ApplicationConfigur
if err != nil {
return err
}
if newRevision && hasRolloutLogic {
if newRevision {
newComponents = append(newComponents, revisionName)
}
// find the ACC that contains this component
@@ -112,32 +105,14 @@ func (h *appHandler) apply(ctx context.Context, ac *v1alpha2.ApplicationConfigur
}
}
// set the annotation on ac to point out which component are newly changed
ac.SetAnnotations(oamutil.MergeMapOverrideWithDst(ac.GetAnnotations(), map[string]string{
// make sure that it won't remove the value on the subsequent reconcile loops
ac.SetAnnotations(oamutil.MergeMapOverrideWithDst(map[string]string{
oam.AnnotationRollingComponent: strings.Join(newComponents, common.RollingComponentsSep),
}))
}, ac.GetAnnotations()))
if err := h.createOrUpdateAppConfig(ctx, ac); err != nil {
return err
}
// Garbage Collection for no used Components.
// There's no need to ApplicationConfiguration Garbage Collection, it has the same name with Application.
for _, comp := range h.app.Status.Components {
var exist = false
for _, cc := range comps {
if comp.Name == cc.Name {
exist = true
break
}
}
if exist {
continue
}
// Component not exits in current Application, should be deleted
var oldC = &v1alpha2.Component{ObjectMeta: metav1.ObjectMeta{Name: comp.Name, Namespace: ac.Namespace}}
if err := h.r.Delete(ctx, oldC); err != nil {
return err
}
}
return nil
}
@@ -313,8 +288,9 @@ func (h *appHandler) createOrUpdateAppConfig(ctx context.Context, appConfig *v1a
if curAppConfig.GetLabels()[oam.LabelAppConfigHash] == appConfig.GetLabels()[oam.LabelAppConfigHash] {
// Just to be safe that it's not because of a random Hash collision
if apiequality.Semantic.DeepEqual(&curAppConfig.Spec, &appConfig.Spec) {
// same spec, no need to create another AC
return nil
// same spec, no need to create another AC, still need to update the AC to apply label/annotation
oamutil.PassLabelAndAnnotation(appConfig, &curAppConfig)
return h.r.Update(ctx, &curAppConfig)
}
h.logger.Info("encountered a different app spec with same hash", "current spec",
curAppConfig.Spec, "new appConfig spec", appConfig.Spec)
@@ -336,11 +312,14 @@ func (h *appHandler) createNewAppConfig(ctx context.Context, appConfig *v1alpha2
RevisionHash: appConfig.GetLabels()[oam.LabelAppConfigHash],
}
appConfig.Name = revisionName
// indicate that the application is created by the applicationController
// appConfig controller should set this to false after the first successful reconcile
appConfig.SetAnnotations(oamutil.MergeMapOverrideWithDst(appConfig.GetAnnotations(), map[string]string{
oam.AnnotationNewAppConfig: strconv.FormatBool(true),
}))
// indicate that the applicationConfig is created if we are doing rolling out
if _, exist := h.app.GetAnnotations()[oam.AnnotationAppRollout]; exist || h.app.Spec.RolloutPlan != nil {
h.logger.Info(fmt.Sprintf("The application %s rolling out is controlled by a rollout plan", h.app.Name))
appConfig.SetAnnotations(oamutil.MergeMapOverrideWithDst(appConfig.GetAnnotations(), map[string]string{
oam.AnnotationAppRollout: strconv.FormatBool(true),
}))
}
// record that last appConfig we created first in the app's status
// make sure that we persist the latest revision first
if err := h.r.UpdateStatus(ctx, h.app); err != nil {
@@ -112,17 +112,19 @@ var _ = Describe("Test Application apply", func() {
By("Verify that the application status has the lastRevision name ")
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(app.Name + "-v1"))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(utils.ConstructRevisionName(app.Name, 1)))
curAC := &v1alpha2.ApplicationConfiguration{}
Expect(handler.r.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: app.Name + "-v1"},
types.NamespacedName{Namespace: ns.Name, Name: utils.ConstructRevisionName(app.Name, 1)},
curAC)).NotTo(HaveOccurred())
// check that the annotation/labels are correctly applied
Expect(curAC.GetAnnotations()[oam.AnnotationNewAppConfig]).Should(BeIdenticalTo("true"))
Expect(curAC.GetLabels()[oam.LabelAppConfigHash]).ShouldNot(BeEmpty())
hashValue := curAC.GetLabels()[oam.LabelAppConfigHash]
Expect(hashValue).ShouldNot(BeEmpty())
Expect(curApp.Status.LatestRevision.RevisionHash).Should(Equal(hashValue))
// TODO: verify that label and annotation change will be passed down
By("[TEST] apply the same appConfig mimic application controller, should do nothing")
// this should not lead to a new AC
err = handler.createOrUpdateAppConfig(ctx, appConfig.DeepCopy())
@@ -138,9 +140,10 @@ var _ = Describe("Test Application apply", func() {
By("Verify that the lastest revision does not change")
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(app.Name + "-v1"))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(utils.ConstructRevisionName(app.Name, 1)))
Expect(curApp.Status.LatestRevision.RevisionHash).Should(Equal(hashValue))
Expect(handler.r.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: app.Name + "-v1"},
types.NamespacedName{Namespace: ns.Name, Name: curApp.Status.LatestRevision.Name},
curAC)).NotTo(HaveOccurred())
By("[TEST] Modify the applicationConfiguration mimic AC controller, should only update")
@@ -148,8 +151,8 @@ var _ = Describe("Test Application apply", func() {
curAC.Status.SetConditions(readyCondition("newType"))
Expect(handler.r.Status().Update(ctx, curAC)).NotTo(HaveOccurred())
// set the new AppConfig annotation as false AC controller would do
cl := curAC.GetAnnotations()
cl[oam.AnnotationNewAppConfig] = strconv.FormatBool(false)
cl := make(map[string]string)
cl[oam.AnnotationAppRollout] = strconv.FormatBool(false)
curAC.SetAnnotations(cl)
Expect(handler.r.Update(ctx, curAC)).NotTo(HaveOccurred())
// this should not lead to a new AC
@@ -166,17 +169,18 @@ var _ = Describe("Test Application apply", func() {
By("Verify that the lastest revision does not change")
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(app.Name + "-v1"))
Expect(curApp.Status.LatestRevision.Name).Should(Equal(utils.ConstructRevisionName(app.Name, 1)))
Expect(curApp.Status.LatestRevision.RevisionHash).Should(Equal(hashValue))
Expect(handler.r.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: app.Name + "-v1"},
types.NamespacedName{Namespace: ns.Name, Name: curApp.Status.LatestRevision.Name},
curAC)).NotTo(HaveOccurred())
// check that the new app annotation is false
Expect(curAC.GetAnnotations()[oam.AnnotationNewAppConfig]).Should(Equal(strconv.FormatBool(false)))
Expect(curAC.GetAnnotations()[oam.AnnotationAppRollout]).Should(Equal(strconv.FormatBool(false)))
Expect(curAC.GetLabels()[oam.LabelAppConfigHash]).Should(Equal(hashValue))
Expect(curAC.GetCondition("newType").Status).Should(BeEquivalentTo(corev1.ConditionTrue))
// check that no new appConfig created
Expect(handler.r.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: app.Name + "-v2"},
curAC)).Should(&oamutil.NotFoundMatcher{})
Expect(handler.r.Get(ctx, types.NamespacedName{Namespace: ns.Name,
Name: utils.ConstructRevisionName(app.Name, 2)}, curAC)).Should(&oamutil.NotFoundMatcher{})
By("[TEST] Modify the applicationConfiguration spec, should lead to a new AC")
// update the spec of the AC which should lead to a new AC being created
@@ -215,9 +219,8 @@ var _ = Describe("Test Application apply", func() {
// check that the new app annotation exist and the hash value has changed
Expect(handler.r.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: app.Name + "-v2"},
types.NamespacedName{Namespace: ns.Name, Name: curApp.Status.LatestRevision.Name},
curAC)).NotTo(HaveOccurred())
Expect(curAC.GetAnnotations()[oam.AnnotationNewAppConfig]).Should(BeIdenticalTo(strconv.FormatBool(true)))
Expect(curAC.GetLabels()[oam.LabelAppConfigHash]).ShouldNot(BeEmpty())
Expect(curAC.GetLabels()[oam.LabelAppConfigHash]).ShouldNot(Equal(hashValue))
// check that no more new appConfig created
@@ -22,12 +22,11 @@ import (
"testing"
"time"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -38,6 +37,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
@@ -52,12 +52,8 @@ import (
const (
reconcileTimeout = 1 * time.Minute
dependCheckWait = 10 * time.Second
shortWait = 30 * time.Second
longWait = 1 * time.Minute
)
var errResult = reconcile.Result{RequeueAfter: shortWait}
// Reconcile error strings.
const (
errGetAppConfig = "cannot get application configuration"
@@ -230,7 +226,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
// stop processing this resource
return ctrl.Result{}, nil
}
return errResult, errors.Wrap(err, errGetAppConfig)
return reconcile.Result{}, errors.Wrap(err, errGetAppConfig)
}
acPatch := ac.DeepCopy()
if ac.ObjectMeta.DeletionTimestamp.IsZero() {
@@ -252,8 +248,8 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
// make sure this is the last functional defer function to be called
defer func() {
// Make sure if error occurs, reconcile will not happen too frequency
if returnErr != nil && result.RequeueAfter < shortWait {
result.RequeueAfter = shortWait
if returnErr != nil {
result.RequeueAfter = 0
}
}()
@@ -291,11 +287,11 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
// we have special logics for application generated applicationConfiguration
if isControlledByApp(ac) {
if ac.GetAnnotations()[oam.AnnotationNewAppConfig] == strconv.FormatBool(true) {
if ac.GetAnnotations()[oam.AnnotationAppRollout] == strconv.FormatBool(true) {
defer func() {
// we only flip the annotation after a successful reconcile
if returnErr == nil {
ac.GetAnnotations()[oam.AnnotationNewAppConfig] = strconv.FormatBool(false)
ac.GetAnnotations()[oam.AnnotationAppRollout] = strconv.FormatBool(false)
returnErr = r.client.Update(ctx, ac)
}
}()
@@ -304,20 +300,20 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
workloads, depStatus, err := r.components.Render(ctx, ac)
if err != nil {
log.Info("Cannot render components", "error", err, "requeue-after", time.Now().Add(shortWait))
log.Info("Cannot render components", "error", err)
r.record.Event(ac, event.Warning(reasonCannotRenderComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errRenderComponents)))
return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
return reconcile.Result{}, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
log.Debug("Successfully rendered components", "workloads", len(workloads))
r.record.Event(ac, event.Normal(reasonRenderComponents, "Successfully rendered components", "workloads", strconv.Itoa(len(workloads))))
applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode, log)}
if err := r.workloads.Apply(ctx, ac.Status.Workloads, workloads, applyOpts...); err != nil {
log.Debug("Cannot apply workload", "error", err, "requeue-after", time.Now().Add(shortWait))
log.Debug("Cannot apply workload", "error", err)
r.record.Event(ac, event.Warning(reasonCannotApplyComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errApplyComponents)))
return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
return reconcile.Result{}, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
log.Debug("Successfully applied components", "workloads", len(workloads))
r.record.Event(ac, event.Normal(reasonApplyComponents, "Successfully applied components", "workloads", strconv.Itoa(len(workloads))))
@@ -334,10 +330,10 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
record := r.record.WithAnnotations("kind", e.GetKind(), "name", e.GetName())
if err := r.client.Delete(ctx, &e); resource.IgnoreNotFound(err) != nil {
log.Debug("Cannot garbage collect component", "error", err, "requeue-after", time.Now().Add(shortWait))
log.Debug("Cannot garbage collect component", "error", err)
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
ac.SetConditions(v1alpha1.ReconcileError(errors.Wrap(err, errGCComponent)))
return errResult, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
return reconcile.Result{}, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
}
log.Debug("Garbage collected resource")
record.Event(ac, event.Normal(reasonGGComponent, "Successfully garbage collected component"))
@@ -347,7 +343,7 @@ func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (result reco
r.updateStatus(ctx, ac, acPatch, workloads)
ac.Status.Dependency = v1alpha2.DependencyStatus{}
waitTime := longWait
var waitTime time.Duration
if len(depStatus.Unsatisfied) != 0 {
waitTime = dependCheckWait
ac.Status.Dependency = *depStatus
@@ -464,6 +460,9 @@ func hasScope(ac *v1alpha2.ApplicationConfiguration) bool {
// A Workload produced by an OAM ApplicationConfiguration.
type Workload struct {
// SkipApply indicates that the workload should not be applied
SkipApply bool
// ComponentName that produced this workload.
ComponentName string
@@ -41,7 +41,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/oam/mock"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
@@ -158,7 +157,7 @@ func TestReconciler(t *testing.T) {
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
result: reconcile.Result{},
err: errors.Wrap(errBoom, errGetAppConfig),
},
},
@@ -187,7 +186,7 @@ func TestReconciler(t *testing.T) {
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
result: reconcile.Result{},
},
},
"ApplyComponentsError": {
@@ -217,7 +216,7 @@ func TestReconciler(t *testing.T) {
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
result: reconcile.Result{},
},
},
"GCDeleteError": {
@@ -251,7 +250,7 @@ func TestReconciler(t *testing.T) {
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
result: reconcile.Result{},
},
},
"Has dependency": {
@@ -355,7 +354,7 @@ func TestReconciler(t *testing.T) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
})),
WithPosthook("postHook", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
return reconcile.Result{}, nil
})),
},
},
@@ -423,7 +422,7 @@ func TestReconciler(t *testing.T) {
return []unstructured.Unstructured{*trait}
})),
WithPosthook("preHookSuccess", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
return reconcile.Result{}, nil
})),
WithPosthook("preHookFailed", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
@@ -474,7 +473,7 @@ func TestReconciler(t *testing.T) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
})),
WithPosthook("preHookSuccess", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
return reconcile.Result{}, nil
})),
WithPosthook("preHookFailed", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: 15 * time.Second}, errBoom
@@ -541,15 +540,15 @@ func TestReconciler(t *testing.T) {
return []unstructured.Unstructured{*trait}
})),
WithPrehook("preHook", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
return reconcile.Result{}, nil
})),
WithPosthook("postHook", ControllerHooksFn(func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, logger logging.Logger) (reconcile.Result, error) {
return reconcile.Result{RequeueAfter: shortWait}, nil
return reconcile.Result{}, nil
})),
},
},
want: want{
result: reconcile.Result{RequeueAfter: longWait},
result: reconcile.Result{RequeueAfter: 0},
},
},
"RegisterFinalizer": {
@@ -33,6 +33,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
@@ -104,7 +105,11 @@ func (a *workloads) Apply(ctx context.Context, status []v1alpha2.WorkloadStatus,
// they are all in the same namespace
var namespace = w[0].Workload.GetNamespace()
for _, wl := range w {
if !wl.HasDep {
if wl.SkipApply {
klog.InfoS("skip apply a workload due to rollout", "component name", wl.ComponentName, "component revision",
wl.ComponentRevisionName)
}
if !wl.HasDep && !wl.SkipApply {
// Apply the DataInputs to this workload
if err := a.ApplyInputRef(ctx, wl.Workload, wl.DataInputs, namespace, ao...); err != nil {
return err
@@ -99,22 +99,23 @@ func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfigu
workloads := make([]*Workload, 0, len(ac.Spec.Components))
dag := newDAG()
// we have special logics for application generated applicationConfiguration
isControlledByApp := isControlledByApp(ac)
// we have special logics for application generated applicationConfiguration during rolling out phase
rollingComponents := make(map[string]bool)
if isControlledByApp {
if ac.GetAnnotations()[oam.AnnotationNewAppConfig] == strconv.FormatBool(true) {
if anc, exist := ac.GetAnnotations()[oam.AnnotationRollingComponent]; exist {
// the new components annotation contains all the changed components in the application
for _, revisionName := range strings.Split(anc, common.RollingComponentsSep) {
rollingComponents[revisionName] = true
}
var rolloutTemplate string
var exist bool
if rolloutTemplate, exist = ac.GetAnnotations()[oam.AnnotationAppRollout]; exist {
// we only care about the new components when there is rolling out, no matter if it's true or not
if anc, exist := ac.GetAnnotations()[oam.AnnotationRollingComponent]; exist {
// the new components annotation contains all the changed components in the application
for _, revisionName := range strings.Split(anc, common.RollingComponentsSep) {
rollingComponents[revisionName] = true
}
}
}
for _, acc := range ac.Spec.Components {
w, err := r.renderComponent(ctx, acc, ac, isControlledByApp, rollingComponents[acc.RevisionName], dag)
w, err := r.renderComponent(ctx, acc, ac, isControlledByApp(ac), rollingComponents[acc.RevisionName],
rolloutTemplate == strconv.FormatBool(true), dag)
if err != nil {
return nil, nil, err
}
@@ -136,12 +137,14 @@ func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfigu
}
func (r *components) renderComponent(ctx context.Context, acc v1alpha2.ApplicationConfigurationComponent,
ac *v1alpha2.ApplicationConfiguration, isControlledByApp, isCompRolling bool, dag *dag) (*Workload, error) {
ac *v1alpha2.ApplicationConfiguration, isControlledByApp, isChanged, isRolloutTemplate bool, dag *dag) (*Workload, error) {
if acc.RevisionName != "" {
acc.ComponentName = utils.ExtractComponentName(acc.RevisionName)
}
klog.InfoS("render a component", "component name", acc.ComponentName, "component revision", acc.RevisionName,
"is generated by application", isControlledByApp, "is a newly changed component", isCompRolling)
"is generated by application", isControlledByApp, "is a changed component", isChanged,
"is the appConfig a rollout template", isRolloutTemplate)
c, componentRevisionName, err := util.GetComponent(ctx, r.client, acc, ac.GetNamespace())
if err != nil {
return nil, err
@@ -171,7 +174,7 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
// pass through labels and annotation from app-config to workload
util.PassLabelAndAnnotation(ac, w)
// don't pass the following annotation as those are for appConfig only
util.RemoveAnnotations(w, []string{oam.AnnotationNewAppConfig, oam.AnnotationRollingComponent})
util.RemoveAnnotations(w, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent})
ref := metav1.NewControllerRef(ac, v1alpha2.ApplicationConfigurationGroupVersionKind)
w.SetOwnerReferences([]metav1.OwnerReference{*ref})
w.SetNamespace(ac.GetNamespace())
@@ -190,7 +193,7 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
// pass through labels and annotation from app-config to trait
util.PassLabelAndAnnotation(ac, t)
util.RemoveAnnotations(t, []string{oam.AnnotationNewAppConfig, oam.AnnotationRollingComponent})
util.RemoveAnnotations(t, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent})
traits = append(traits, &Trait{Object: *t, Definition: *traitDef})
traitDefs = append(traitDefs, *traitDef)
}
@@ -210,9 +213,9 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
return nil, err
}
SetAppWorkloadInstanceName(acc.ComponentName, w, revision)
if isCompRolling {
// we have a special logic to emit the workload disabled so that the rollout process can take over
// this is the only place the appConfig controller is involved in the rollout process
if isChanged && isRolloutTemplate {
// we have a special logic to emit the workload disabled for the first time so that the rollout
// controller can take over.
if err := prepWorkloadInstanceForRollout(w); err != nil {
return nil, err
}
@@ -247,9 +250,11 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
}
addDataOutputsToDAG(dag, acc.DataOutputs, w)
return &Workload{ComponentName: acc.ComponentName, ComponentRevisionName: componentRevisionName,
Workload: w, Traits: traits, RevisionEnabled: isRevisionEnabled(traitDefs), Scopes: scopes}, nil
// To avoid conflict with rollout controller, we will not render the workload until the rollout phase is over
// indicated by the AnnotationAppRollout annotation disappear
return &Workload{SkipApply: isChanged && !isRolloutTemplate, ComponentName: acc.ComponentName,
ComponentRevisionName: componentRevisionName,
Workload: w, Traits: traits, RevisionEnabled: isRevisionEnabled(traitDefs), Scopes: scopes}, nil
}
func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait, ac *v1alpha2.ApplicationConfiguration,
@@ -92,20 +92,27 @@ func TestRender(t *testing.T) {
},
}
controlledAC := revAC.DeepCopy()
controlledAC.OwnerReferences = []metav1.OwnerReference{
controlledTemplateAC := revAC.DeepCopy()
controlledTemplateAC.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: v1alpha2.SchemeGroupVersion.String(),
Kind: v1alpha2.ApplicationKind,
Controller: pointer.BoolPtr(true),
},
}
controlledAC.Spec.Components[0].RevisionName = revisionName2
controlledAC.SetAnnotations(map[string]string{
oam.AnnotationNewAppConfig: strconv.FormatBool(true),
controlledTemplateAC.Spec.Components[0].RevisionName = revisionName2
controlledTemplateAC.SetAnnotations(map[string]string{
oam.AnnotationAppRollout: strconv.FormatBool(true),
oam.AnnotationRollingComponent: revisionName + "," + revisionName2,
"keep": strconv.FormatBool(true),
})
controlledNoneTemplateAC := controlledTemplateAC.DeepCopy()
controlledNoneTemplateAC.SetAnnotations(map[string]string{
oam.AnnotationAppRollout: strconv.FormatBool(false),
oam.AnnotationRollingComponent: revisionName + "," + revisionName2,
"keep": strconv.FormatBool(true),
})
ref := metav1.NewControllerRef(ac, v1alpha2.ApplicationConfigurationGroupVersionKind)
errTrait := errors.New("errTrait")
@@ -599,7 +606,7 @@ func TestRender(t *testing.T) {
return t, nil
}),
},
args: args{ac: controlledAC},
args: args{ac: controlledTemplateAC},
want: want{
w: []Workload{
{
@@ -677,7 +684,7 @@ func TestRender(t *testing.T) {
return t, nil
}),
},
args: args{ac: controlledAC},
args: args{ac: controlledTemplateAC},
want: want{
w: []Workload{
{
@@ -698,6 +705,85 @@ func TestRender(t *testing.T) {
},
},
},
"Success-With-NoneTemplate-Deployment": {
reason: "We do not render the workload after the template is out",
fields: fields{
client: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
switch defObj := obj.(type) {
case *v1alpha2.Component:
ccomp := v1alpha2.Component{
Status: v1alpha2.ComponentStatus{
LatestRevision: &v1alpha2.Revision{Name: revisionName2},
},
}
ccomp.DeepCopyInto(defObj)
case *v1alpha2.TraitDefinition:
ttrait := v1alpha2.TraitDefinition{ObjectMeta: metav1.ObjectMeta{Name: traitName},
Spec: v1alpha2.TraitDefinitionSpec{RevisionEnabled: true}}
ttrait.DeepCopyInto(defObj)
case *v1.ControllerRevision:
rev := &v1.ControllerRevision{
ObjectMeta: metav1.ObjectMeta{Name: revisionName, Namespace: namespace},
Data: runtime.RawExtension{Object: &v1alpha2.Component{
ObjectMeta: metav1.ObjectMeta{
Name: componentName,
Namespace: namespace,
},
Spec: v1alpha2.ComponentSpec{
Workload: runtime.RawExtension{
Object: &unstructured.Unstructured{},
},
},
Status: v1alpha2.ComponentStatus{
LatestRevision: &v1alpha2.Revision{Name: revisionName2},
},
}},
Revision: 2,
}
rev.DeepCopyInto(defObj)
}
return nil
})},
params: ParameterResolveFn(func(_ []v1alpha2.ComponentParameter, _ []v1alpha2.ComponentParameterValue) ([]Parameter, error) {
return nil, nil
}),
workload: ResourceRenderFn(func(_ []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
w := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
},
}
return w, nil
}),
trait: ResourceRenderFn(func(_ []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
t := &unstructured.Unstructured{}
t.SetName(traitName)
return t, nil
}),
},
args: args{ac: controlledNoneTemplateAC},
want: want{
w: []Workload{
{
SkipApply: true,
ComponentName: componentName,
ComponentRevisionName: revisionName2,
Workload: func() *unstructured.Unstructured {
w := &unstructured.Unstructured{}
w.SetNamespace(namespace)
w.SetName(revisionName2)
w.SetOwnerReferences([]metav1.OwnerReference{*ref})
w.SetAnnotations(map[string]string{
oam.AnnotationAppGeneration: "0",
})
return w
}(),
RevisionEnabled: true,
},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
@@ -718,9 +804,9 @@ func TestRender(t *testing.T) {
if diff := cmp.Diff(tc.want.w[0].Workload.GetName(), got[0].Workload.GetName()); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want, +got:\n%s\n", tc.reason, diff)
}
if _, exit := got[0].Workload.GetAnnotations()[oam.AnnotationNewAppConfig]; exit {
if _, exit := got[0].Workload.GetAnnotations()[oam.AnnotationAppRollout]; exit {
t.Errorf("\n%s\nr.Render(...) workload should not get annotation:%s\n", tc.reason,
oam.AnnotationNewAppConfig)
oam.AnnotationAppRollout)
}
if _, exit := got[0].Workload.GetAnnotations()[oam.AnnotationRollingComponent]; exit {
t.Errorf("\n%s\nr.Render(...) workload should not get annotation:%s\n", tc.reason,
@@ -738,6 +824,10 @@ func TestRender(t *testing.T) {
t.Errorf("\n%s\nr.Render(...): trait should get annotation:%s\n", tc.reason,
"keep")
}
isTempalte := tc.args.ac.GetAnnotations()[oam.AnnotationAppRollout] == strconv.FormatBool(true)
if !isTempalte && !got[0].SkipApply {
t.Errorf("\n%s\nr.Render(...): none template workload should be skip apply\n", tc.reason)
}
} else {
if diff := cmp.Diff(tc.want.w, got); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want, +got:\n%s\n", tc.reason, diff)
@@ -756,7 +846,8 @@ func TestRenderComponent(t *testing.T) {
type arg struct {
ac *v1alpha2.ApplicationConfiguration
isControlledByApp bool
isCompRolling bool
isCompChanged bool
isRollingTemplate bool
dag *dag
}
type want struct {
@@ -837,6 +928,46 @@ func TestRenderComponent(t *testing.T) {
//TODO: Add more failure cases
// add more dependency related tests for any future changes
// add more trait related tests
"Newly-Changed-Component-NotRolling": {
reason: "newly changed workload should not be rendered if it's not rolling anymore (not the first time)",
fields: field{
client: &test.MockClient{MockGet: mockGet},
workload: ResourceRenderFn(func(_ []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
w := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
},
}
return w, nil
}),
trait: ResourceRenderFn(func(_ []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
t := &unstructured.Unstructured{}
t.SetName(traitName)
return t, nil
}),
},
args: arg{
ac: revAC,
isControlledByApp: true,
isCompChanged: true,
isRollingTemplate: false,
},
want: want{
w: Workload{
ComponentName: componentName,
ComponentRevisionName: revisionName,
Workload: func() *unstructured.Unstructured {
w := &unstructured.Unstructured{}
w.SetName(revisionName)
w.SetOwnerReferences([]metav1.OwnerReference{*ref})
return w
}(),
RevisionEnabled: true,
},
},
},
"Success-With-Newly-Changed-Component-Deployment": {
reason: "Workload name should be revision name for deployment and it should be disabled",
fields: field{
@@ -860,7 +991,8 @@ func TestRenderComponent(t *testing.T) {
args: arg{
ac: revAC,
isControlledByApp: true,
isCompRolling: true,
isCompChanged: true,
isRollingTemplate: true,
},
want: want{
w: Workload{
@@ -899,7 +1031,8 @@ func TestRenderComponent(t *testing.T) {
args: arg{
ac: revAC2,
isControlledByApp: true,
isCompRolling: true,
isCompChanged: true,
isRollingTemplate: true,
},
want: want{
w: Workload{
@@ -938,10 +1071,12 @@ func TestRenderComponent(t *testing.T) {
args: arg{
ac: revAC,
isControlledByApp: true,
isCompRolling: false,
isCompChanged: false,
isRollingTemplate: false,
},
want: want{
w: Workload{
SkipApply: true,
ComponentName: componentName,
ComponentRevisionName: revisionName,
Workload: func() *unstructured.Unstructured {
@@ -960,7 +1095,7 @@ func TestRenderComponent(t *testing.T) {
r := &components{tc.fields.client, mock.NewMockDiscoveryMapper(), mockParams,
tc.fields.workload, tc.fields.trait}
got, err := r.renderComponent(ctx, tc.args.ac.Spec.Components[0], tc.args.ac, tc.args.isControlledByApp,
tc.args.isCompRolling, tc.args.dag)
tc.args.isCompChanged, tc.args.isRollingTemplate, tc.args.dag)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
@@ -975,15 +1110,20 @@ func TestRenderComponent(t *testing.T) {
t.Errorf("\n%s\nr.Render(...): -want, +got:\n%s\n", tc.reason, diff)
}
}
if tc.args.isCompRolling {
wantedSpec, exist, err := unstructured.NestedFieldCopy(tc.want.w.Workload.Object, "spec")
assert.True(t, exist)
assert.True(t, err == nil)
gotSpec, exist, err := unstructured.NestedFieldCopy(got.Workload.Object, "spec")
assert.True(t, exist)
assert.True(t, err == nil)
if diff := cmp.Diff(wantedSpec, gotSpec); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want, +got:\n%s\n", tc.reason, diff)
if tc.args.isCompChanged {
if tc.args.isRollingTemplate {
wantedSpec, exist, err := unstructured.NestedFieldCopy(tc.want.w.Workload.Object, "spec")
assert.True(t, exist)
assert.True(t, err == nil)
gotSpec, exist, err := unstructured.NestedFieldCopy(got.Workload.Object, "spec")
assert.True(t, exist)
assert.True(t, err == nil)
if diff := cmp.Diff(wantedSpec, gotSpec); diff != "" {
t.Errorf("\n%s\nr.Render(...): -want, +got:\n%s\n", tc.reason, diff)
}
} else {
// newly changed workloads are not rendered after the first time
assert.True(t, got.SkipApply)
}
} else {
// we won't touch the spec
@@ -54,7 +54,7 @@ func prepWorkloadInstanceForRollout(workload *unstructured.Unstructured) error {
// TODO: we can get the workloadDefinition name from workload.GetLabels()["oam.WorkloadTypeLabel"]
// and use a special field like "disablePath" in the definition to allow configurable behavior
// we hard code the behavior depends on the known workload group/kind for now. We need to
// we hard code the behavior depends on the known workload group/kind for now.
if workload.GroupVersionKind().Group == v1alpha1.GroupVersion.Group {
switch workload.GetKind() {
case reflect.TypeOf(v1alpha1.CloneSet{}).Name():
@@ -10,6 +10,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/util/slice"
ctrl "sigs.k8s.io/controller-runtime"
@@ -17,13 +18,16 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
oamv1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/common/rollout"
controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
const appDeployFinalizer = "finalizers.applicationdeployment.oam.dev"
const reconcileTimeOut = 30 * time.Second
const reconcileTimeOut = 60 * time.Second
// Reconciler reconciles an ApplicationDeployment object
type Reconciler struct {
@@ -71,7 +75,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
// Get the target application
var targetApp oamv1alpha2.ApplicationConfiguration
var sourceApp *oamv1alpha2.ApplicationConfiguration
sourceApp := &oamv1alpha2.ApplicationConfiguration{}
targetAppName := appDeploy.Spec.TargetApplicationName
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: req.Namespace, Name: targetAppName},
&targetApp); err != nil {
@@ -84,6 +88,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
sourceAppName := appDeploy.Spec.SourceApplicationName
if sourceAppName == "" {
klog.Info("source app fields not filled, we assume it is deployed for the first time")
sourceApp = nil
} else if err := r.Get(ctx, ktypes.NamespacedName{Namespace: req.Namespace, Name: sourceAppName}, sourceApp); err != nil {
klog.ErrorS(err, "cannot locate source application", "source application", klog.KRef(req.Namespace,
sourceAppName))
@@ -109,8 +114,29 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
result, rolloutStatus := rolloutPlanController.Reconcile(ctx)
// make sure that the new status is copied back
appDeploy.Status.RolloutStatus = *rolloutStatus
if rolloutStatus.RollingState == v1alpha1.RolloutSucceedState {
// remove the rollout annotation so that the target appConfig controller can take over the rest of the work
oamutil.RemoveAnnotations(&targetApp, []string{oam.AnnotationAppRollout})
if err := r.Update(ctx, &targetApp); err != nil {
klog.ErrorS(err, "cannot remove the rollout annotation", "target application",
klog.KRef(req.Namespace, targetAppName))
return ctrl.Result{}, err
}
}
// update the appDeploy status
return result, r.Update(ctx, &appDeploy)
return result, r.updateStatus(ctx, &appDeploy)
}
// UpdateStatus updates v1alpha2.ApplicationDeployment's Status with retry.RetryOnConflict
func (r *Reconciler) updateStatus(ctx context.Context, app *oamv1alpha2.ApplicationDeployment, opts ...client.UpdateOption) error {
status := app.DeepCopy().Status
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if err = r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil {
return
}
app.Status = status
return r.Status().Update(ctx, app, opts...)
})
}
func (r *Reconciler) handleFinalizer(appDeploy *oamv1alpha2.ApplicationDeployment) {
-5
View File
@@ -57,11 +57,6 @@ const (
// the application controller should not reconcile it yet
AnnotationAppRollout = "app.oam.dev/rollout-template"
// AnnotationNewAppConfig indicates that the application configuration is new
// this is to enable the applicationConfiguration controller to handle the
// first reconcile logic differently similar to what "finalize" field
AnnotationNewAppConfig = "app.oam.dev/new-appConfig"
// AnnotationRollingComponent indicates that the component is new
// this is to enable any concerned controllers to handle the first component apply logic differently
// the value of the annotation is a list of revision name of all the new component
+5 -6
View File
@@ -553,19 +553,18 @@ func AddAnnotations(o labelAnnotationObject, annos map[string]string) {
o.SetAnnotations(MergeMapOverrideWithDst(o.GetAnnotations(), annos))
}
// MergeMapOverrideWithDst merges two could be nil maps. If any conflicts, override src with dst.
// MergeMapOverrideWithDst merges two could be nil maps. Keep the dst for any conflicts,
func MergeMapOverrideWithDst(src, dst map[string]string) map[string]string {
if src == nil && dst == nil {
return nil
}
r := make(map[string]string)
for k, v := range src {
r[k] = v
}
// override the src for the same key
for k, v := range dst {
r[k] = v
}
for k, v := range src {
if _, exist := r[k]; !exist {
r[k] = v
}
}
return r
}
+4 -1
View File
@@ -120,7 +120,10 @@ func getModifiedConfiguration(obj runtime.Object, updateAnnotation bool) ([]byte
if updateAnnotation {
annots[oam.AnnotationLastAppliedConfig] = string(modified)
_ = metadataAccessor.SetAnnotations(obj, annots)
err = metadataAccessor.SetAnnotations(obj, annots)
if err != nil {
return nil, err
}
modified, err = json.Marshal(obj)
if err != nil {
return nil, err
@@ -33,6 +33,12 @@ func ValidateCreate(rollout *v1alpha1.RolloutPlan, rootPath *field.Path) field.E
var allErrs field.ErrorList
// TODO: The total number of num in the batches match the current target resource pod size
// the rollout batch partition is either automatic or positive
if rollout.BatchPartition != nil && *rollout.BatchPartition < 0 {
allErrs = append(allErrs, field.Invalid(rootPath.Child("batchPartition"), rollout.BatchPartition,
"the rollout plan has to be positive"))
}
// NumBatches has to be the size of RolloutBatches
if rollout.NumBatches != nil && len(rollout.RolloutBatches) != int(*rollout.NumBatches) {
allErrs = append(allErrs, field.Invalid(rootPath.Child("numBatches"), rollout.NumBatches,