Merge pull request #963 from ryanzhang-oss/appDeployment-structure

further develop appDeployment controller
This commit is contained in:
Jianbo Sun
2021-01-29 11:19:59 +08:00
committed by GitHub
16 changed files with 148 additions and 54 deletions
@@ -29,8 +29,8 @@ type ApplicationDeploymentSpec struct {
TargetApplicationName string `json:"targetApplicationName"`
// SourceApplicationName contains the name of the application that we need to upgrade from.
// it can be omitted only when it's the first time to deploy the application
SourceApplicationName *string `json:"sourceApplicationName,omitempty"`
// it can be empty only when it's the first time to deploy the application
SourceApplicationName string `json:"sourceApplicationName"`
// The list of component to upgrade in the application.
// We only support single component application so far
@@ -321,11 +321,6 @@ func (in *ApplicationDeploymentList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDeploymentSpec) DeepCopyInto(out *ApplicationDeploymentSpec) {
*out = *in
if in.SourceApplicationName != nil {
in, out := &in.SourceApplicationName, &out.SourceApplicationName
*out = new(string)
**out = **in
}
if in.ComponentList != nil {
in, out := &in.ComponentList, &out.ComponentList
*out = make([]string, len(*in))
@@ -251,13 +251,14 @@ spec:
type: integer
type: object
sourceApplicationName:
description: SourceApplicationName contains the name of the application that we need to upgrade from. it can be omitted only when it's the first time to deploy the application
description: SourceApplicationName contains the name of the application that we need to upgrade from. it can be empty only when it's the first time to deploy the application
type: string
targetApplicationName:
description: TargetApplicationName contains the name of the application that we need to upgrade to. We assume that an application is immutable, thus the name alone is suffice
type: string
required:
- rolloutPlan
- sourceApplicationName
- targetApplicationName
type: object
status:
+1 -1
View File
@@ -174,7 +174,7 @@ We will use a pre-defined annotation "app.oam.dev/rollout" that equals to "true"
utilizes an appDeployment object to follow this rule.
- Upon creation, the appDeployment controller marks itself as the owner of the application. The
application controller will have built-in logic to ignore any applications that has the
"app.oam.dev/rollout" annotation set to true.
"app.oam.dev/rollout-template" annotation set to true.
- The appDeployment controller can change the target application fields. For example,
- It might remove all the conflict traits, such as HPA during upgrade.
- It might modify the label selectors fields in the services to make sure there are ways to
@@ -251,13 +251,14 @@ spec:
type: integer
type: object
sourceApplicationName:
description: SourceApplicationName contains the name of the application that we need to upgrade from. it can be omitted only when it's the first time to deploy the application
description: SourceApplicationName contains the name of the application that we need to upgrade from. it can be empty only when it's the first time to deploy the application
type: string
targetApplicationName:
description: TargetApplicationName contains the name of the application that we need to upgrade to. We assume that an application is immutable, thus the name alone is suffice
type: string
required:
- rolloutPlan
- sourceApplicationName
- targetApplicationName
type: object
status:
@@ -0,0 +1,19 @@
package rollout
import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
// ReconcileRolloutPlan generates the rollout plan and reconcile it
func ReconcileRolloutPlan(ctx context.Context, client client.Client, rolloutSpec *v1alpha1.RolloutPlan,
targetWorkload, sourceWorkload *unstructured.Unstructured) error {
klog.InfoS("generate the rollout plan", "rollout Spec", rolloutSpec,
"target workload", klog.KObj(targetWorkload))
return nil
}
@@ -36,7 +36,8 @@ import (
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
const rolloutReconcileWaitTime = time.Second * 30
// RolloutReconcileWaitTime is the time to wait before reconcile again an application still in rollout phase
const RolloutReconcileWaitTime = time.Second * 3
// Reconciler reconciles a Application object
type Reconciler struct {
@@ -75,7 +76,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
app.Status.Phase = v1alpha2.ApplicationRollingOut
app.Status.SetConditions(readyCondition("Rolling"))
// do not process apps still in rolling out
return ctrl.Result{RequeueAfter: rolloutReconcileWaitTime}, r.Status().Update(ctx, app)
return ctrl.Result{RequeueAfter: RolloutReconcileWaitTime}, r.Status().Update(ctx, app)
}
applog.Info("Start Rendering")
@@ -679,7 +679,7 @@ var _ = Describe("Test Application Controller", func() {
Namespace: app.Namespace,
}
result, err := reconciler.Reconcile(reconcile.Request{NamespacedName: appKey})
Expect(result).To(BeIdenticalTo(ctrl.Result{RequeueAfter: rolloutReconcileWaitTime}))
Expect(result).To(BeIdenticalTo(ctrl.Result{RequeueAfter: RolloutReconcileWaitTime}))
Expect(err).ToNot(HaveOccurred())
By("Check App status is rollingout")
checkApp := &v1alpha2.Application{}
@@ -0,0 +1,27 @@
package applicationdeployment
import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/oam"
)
// adjustTargetApplicationTemplate makes sure that the target template is in compliance before handing it over to
// the application controller
func (r *Reconciler) adjustTargetApplicationTemplate(ctx context.Context, targetWorkload *unstructured.Unstructured,
targetApp *corev1alpha2.Application) error {
klog.InfoS("Start to adjust the target application template",
"application", klog.KObj(targetApp), "target workload", targetWorkload)
// TODO: adjust the target workload if needed
// update the template without the rollout annotation so that the application controller can take over
anno := targetApp.GetAnnotations()
delete(anno, oam.AnnotationAppRollout)
targetApp.SetAnnotations(anno)
return r.Update(ctx, targetApp)
}
@@ -7,22 +7,23 @@ import (
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
appUtil "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationdeployment"
)
// extractWorkload extracts the workload
func (r *Reconciler) extractWorkload(componentList []string, targetApp,
sourceApp *corev1alpha2.Application) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
// extractWorkloadTypeAndGVK extracts the workload type and gvk
func (r *Reconciler) extractWorkloadTypeAndGVK(ctx context.Context, componentList []string, targetApp,
sourceApp *corev1alpha2.Application) (string, *schema.GroupVersionKind, error) {
var componentType string
// assume that the validator webhook has already guaranteed that there is no more than one component for now
if len(componentList) == 0 {
// we need to find a default component
commons := appUtil.FindCommonComponent(targetApp, sourceApp)
if len(commons) != 1 {
return nil, nil, fmt.Errorf("cannot find a default component, too many common components: %+v", commons)
return "", nil, fmt.Errorf("cannot find a default component, too many common components: %+v", commons)
}
componentType = commons[0]
} else {
@@ -30,33 +31,44 @@ func (r *Reconciler) extractWorkload(componentList []string, targetApp,
}
// get the workload definition
// the validator webhook has checked that source and the target are the same type
wd, err := oamutil.GetWorkloadDefinition(r, componentType)
wd, err := oamutil.GetWorkloadDefinition(ctx, r, componentType)
if err != nil {
return nil, nil, errors.Wrap(err, fmt.Sprintf("failed to get workload definition %s", componentType))
return "", nil, errors.Wrap(err, fmt.Sprintf("failed to get workload definition %s", componentType))
}
// get the CR kind from the definitionRef
gvk, err := oamutil.GetGVKFromDefinition(r.dm, wd.Spec.Reference)
if err != nil {
return nil, nil, errors.Wrap(err, fmt.Sprintf("failed to get workload GVK from definition ref %s",
return "", nil, errors.Wrap(err, fmt.Sprintf("failed to get workload GVK from definition ref %s",
wd.Spec.Reference))
}
targetWorkload, err := r.fetchWorkload(targetApp, componentType, gvk)
return componentType, &gvk, nil
}
// fetchWorkload based on the component type and the application and its gvk
func (r *Reconciler) fetchWorkloads(ctx context.Context, targetApp, sourceApp *corev1alpha2.Application, workloadType string,
workloadGVK *schema.GroupVersionKind) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
targetWorkload, err := r.fetchWorkload(ctx, targetApp, workloadType, *workloadGVK)
if err != nil {
return nil, nil, err
}
klog.InfoS("get the target workload we need to work on", "targetWorkload", klog.KObj(targetWorkload))
if sourceApp == nil {
return targetWorkload, nil, nil
}
// get the component definition in the app,
sourceWorkload, err := r.fetchWorkload(sourceApp, componentType, gvk)
sourceWorkload, err := r.fetchWorkload(ctx, sourceApp, workloadType, *workloadGVK)
if err != nil {
return nil, nil, err
}
if sourceWorkload != nil {
klog.InfoS("get the source workload we need to work on", "sourceWorkload", klog.KObj(sourceWorkload))
}
return targetWorkload, sourceWorkload, nil
}
// fetchWorkload based on the component type and the application and its gvk
func (r *Reconciler) fetchWorkload(app *corev1alpha2.Application, componentType string,
func (r *Reconciler) fetchWorkload(ctx context.Context, app *corev1alpha2.Application, componentType string,
gvk schema.GroupVersionKind) (*unstructured.Unstructured, error) {
// get the component definition in the app,
comp := app.GetComponent(componentType)
@@ -64,8 +76,7 @@ func (r *Reconciler) fetchWorkload(app *corev1alpha2.Application, componentType
return nil, fmt.Errorf("cannot find the component %s in the application", componentType)
}
// get the workload given GVK and name
workload, err := oamutil.GetObjectGivenGVKAndName(context.Background(), r, gvk, app.GetNamespace(),
comp.Name)
workload, err := oamutil.GetObjectGivenGVKAndName(ctx, r, gvk, app.GetNamespace(), comp.Name)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to get workload %s with gvk %+v ", componentType, gvk))
}
@@ -3,6 +3,7 @@ package applicationdeployment
import (
"context"
"fmt"
"time"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/logging"
@@ -15,11 +16,15 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"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/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
const appDeployfinalizer = "finalizers.applicationdeployment.oam.dev"
const appDeployFinalizer = "finalizers.applicationdeployment.oam.dev"
const reconcileTimeOut = 10 * time.Second
// Reconciler reconciles an ApplicationDeployment object
type Reconciler struct {
@@ -29,13 +34,16 @@ type Reconciler struct {
Scheme *runtime.Scheme
}
// Reconcile is the main logic of applicationdeployment controller
// +kubebuilder:rbac:groups=core.oam.dev,resources=applicationdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.oam.dev,resources=applicationdeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core.oam.dev,resources=applicationconfigurations,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.oam.dev,resources=applications,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.oam.dev,resources=applications/status,verbs=get;update;patch
// Reconcile is the main logic of applicationdeployment controller
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
var appDeploy corev1alpha2.ApplicationDeployment
ctx, cancel := context.WithTimeout(context.TODO(), reconcileTimeOut)
defer cancel()
if err := r.Get(ctx, req.NamespacedName, &appDeploy); err != nil {
if apierrors.IsNotFound(err) {
klog.InfoS("application deployment does not exist", "appDeploy", klog.KRef(req.Namespace, req.Name))
@@ -47,7 +55,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
r.handleFinalizer(&appDeploy)
// Get the target application
var targetApp, sourceApp corev1alpha2.Application
var targetApp corev1alpha2.Application
var sourceApp *corev1alpha2.Application
targetAppName := appDeploy.Spec.TargetApplicationName
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: req.Namespace, Name: targetAppName},
&targetApp); err != nil {
@@ -55,37 +64,66 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
klog.KRef(req.Namespace, targetAppName))
return ctrl.Result{}, err
}
// Get the source application
sourceAppName := appDeploy.Spec.SourceApplicationName
if sourceAppName == nil {
if sourceAppName == "" {
klog.Info("source app fields not filled, we assume it is deployed for the first time")
} else if err := r.Get(ctx, ktypes.NamespacedName{Namespace: req.Namespace, Name: *sourceAppName},
&sourceApp); err != 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))
sourceAppName))
return ctrl.Result{}, err
}
// Get the kubernetes workloads to upgrade from the application
targetWorkload, sourceWorkload, err := r.extractWorkload(appDeploy.Spec.ComponentList, &targetApp, &sourceApp)
workloadType, workloadGVK, err := r.extractWorkloadTypeAndGVK(ctx,
appDeploy.Spec.ComponentList, &targetApp, sourceApp)
if err != nil {
klog.Error(err, "cannot locate the workloads object")
klog.ErrorS(err, "cannot extract the workloadType and GVK",
"component list", appDeploy.Spec.ComponentList, "target app", klog.KObj(&targetApp))
return ctrl.Result{}, err
}
targetWorkload, sourceWorkload, err := r.fetchWorkloads(ctx, &targetApp, sourceApp, workloadType, workloadGVK)
if err != nil {
klog.ErrorS(err, "cannot fetch the workloads to upgrade", "workload Type", workloadType,
"workload GVK", *workloadGVK, "target application", klog.KRef(req.Namespace, targetAppName),
"source application", klog.KRef(req.Namespace, sourceAppName))
return ctrl.Result{}, client.IgnoreNotFound(err)
}
klog.InfoS("get the target workload we need to work on", "targetWorkload", klog.KObj(targetWorkload))
// check if the target application is still in rolling
if _, exist := targetApp.GetAnnotations()[oam.AnnotationAppRollout]; exist {
// adjust the target workload if it's still a template
if err := r.adjustTargetApplicationTemplate(ctx, targetWorkload, &targetApp); err != nil {
klog.ErrorS(err, "cannot adjust the target workload", "target workload", klog.KObj(targetWorkload))
return ctrl.Result{}, err
}
// requeue it to process hopefully after the application controller takes over
return ctrl.Result{RequeueAfter: 2 * application.RolloutReconcileWaitTime}, nil
}
if sourceWorkload != nil {
klog.InfoS("get the source workload we need to work on", "sourceWorkload", klog.KObj(sourceWorkload))
}
// TODO: pass these two object to the rollout plan
// reconcile the rollout part of the spec given the target and source workload
err = rollout.ReconcileRolloutPlan(ctx, r, &appDeploy.Spec.RolloutPlan, targetWorkload, sourceWorkload)
if err != nil {
klog.ErrorS(err, "cannot reconcile the rollout plan", "rollout spec", appDeploy.Spec.RolloutPlan)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *Reconciler) handleFinalizer(appDeploy *corev1alpha2.ApplicationDeployment) {
if appDeploy.DeletionTimestamp.IsZero() {
if !slice.ContainsString(appDeploy.Finalizers, appDeployfinalizer, nil) {
if !slice.ContainsString(appDeploy.Finalizers, appDeployFinalizer, nil) {
// TODO: add finalizer
klog.Info("add finalizer")
}
} else if slice.ContainsString(appDeploy.Finalizers, appDeployfinalizer, nil) {
} else if slice.ContainsString(appDeploy.Finalizers, appDeployFinalizer, nil) {
// TODO: perform finalize
klog.Info("perform clean up")
}
@@ -0,0 +1 @@
package workloads
+7 -6
View File
@@ -182,7 +182,7 @@ func FetchTraitDefinition(ctx context.Context, r client.Reader, dm discoverymapp
return nil, err
}
// Fetch the corresponding traitDefinition CR
traitDefinition, err := GetTraitDefinition(r, trName)
traitDefinition, err := GetTraitDefinition(ctx, r, trName)
if err != nil {
return nil, err
}
@@ -198,7 +198,7 @@ func FetchWorkloadDefinition(ctx context.Context, r client.Reader, dm discoverym
return nil, err
}
// Fetch the corresponding workloadDefinition CR
workloadDefinition, err := GetWorkloadDefinition(r, wldName)
workloadDefinition, err := GetWorkloadDefinition(ctx, r, wldName)
if err != nil {
return nil, err
}
@@ -214,18 +214,19 @@ func GenNamespacedDefinitionName(dn string) types.NamespacedName {
}
// GetWorkloadDefinition Get WorkloadDefinition
func GetWorkloadDefinition(cli client.Reader, workitemName string) (*v1alpha2.WorkloadDefinition, error) {
func GetWorkloadDefinition(ctx context.Context, cli client.Reader,
workitemName string) (*v1alpha2.WorkloadDefinition, error) {
wd := new(v1alpha2.WorkloadDefinition)
if err := cli.Get(context.Background(), GenNamespacedDefinitionName(workitemName), wd); err != nil {
if err := cli.Get(ctx, GenNamespacedDefinitionName(workitemName), wd); err != nil {
return nil, err
}
return wd, nil
}
// GetTraitDefinition Get TraitDefinition
func GetTraitDefinition(cli client.Reader, traitName string) (*v1alpha2.TraitDefinition, error) {
func GetTraitDefinition(ctx context.Context, cli client.Reader, traitName string) (*v1alpha2.TraitDefinition, error) {
td := new(v1alpha2.TraitDefinition)
if err := cli.Get(context.Background(), GenNamespacedDefinitionName(traitName), td); err != nil {
if err := cli.Get(ctx, GenNamespacedDefinitionName(traitName), td); err != nil {
return nil, err
}
return td, nil
+2 -2
View File
@@ -38,7 +38,7 @@ func GetScopeGVK(cli client.Client, dm discoverymapper.DiscoveryMapper,
func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, error) {
switch kd {
case types.TypeWorkload:
wd, err := GetWorkloadDefinition(cli, key)
wd, err := GetWorkloadDefinition(context.TODO(), cli, key)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -57,7 +57,7 @@ func LoadTemplate(cli client.Reader, key string, kd types.CapType) (*Template, e
return tmpl, nil
case types.TypeTrait:
td, err := GetTraitDefinition(cli, key)
td, err := GetTraitDefinition(context.TODO(), cli, key)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -15,7 +15,7 @@ func FindCommonComponent(targetApp, sourceApp *v1alpha2.Application) []string {
return commonComponents
}
// find the common components in both the source and target application
// write an O(N) algorithm just for fun, totally doesn't worth the extra effort
// write an O(N) algorithm just for fun, totally doesn't worth the extra space
targetComponents := make(map[string]bool)
for _, comp := range targetApp.Spec.Components {
targetComponents[comp.WorkloadType] = true
@@ -26,5 +26,4 @@ func FindCommonComponent(targetApp, sourceApp *v1alpha2.Application) []string {
}
}
return commonComponents
}
@@ -39,12 +39,12 @@ func (h *ValidatingHandler) ValidateCreate(appDeploy *v1alpha2.ApplicationDeploy
return allErrs
}
sourceAppName := appDeploy.Spec.SourceApplicationName
if sourceAppName != nil {
if err := h.Get(context.Background(), ktypes.NamespacedName{Namespace: appDeploy.Namespace, Name: *sourceAppName},
if sourceAppName != "" {
if err := h.Get(context.Background(), ktypes.NamespacedName{Namespace: appDeploy.Namespace, Name: sourceAppName},
&sourceApp); err != nil {
klog.ErrorS(err, "cannot locate source application", "source application",
klog.KRef(appDeploy.Namespace, *sourceAppName))
allErrs = append(allErrs, field.NotFound(fldPath.Child("sourceApplicationName"), *sourceAppName))
klog.KRef(appDeploy.Namespace, sourceAppName))
allErrs = append(allErrs, field.NotFound(fldPath.Child("sourceApplicationName"), sourceAppName))
}
}