From 5e064219e73ecac8688de1ab0303a3068bd74573 Mon Sep 17 00:00:00 2001 From: yangsoon Date: Tue, 20 Jul 2021 20:40:01 +0800 Subject: [PATCH] Enhance Initializer: export the phase of Initializer and help install build-in Initializer (#1932) * add phase for initilaizer * help install build-in initializer * add test --- apis/core.oam.dev/v1beta1/initializer_type.go | 16 + .../crds/core.oam.dev_initializers.yaml | 12 +- .../validatingWebhookConfiguration.yaml | 26 +- .../crds/core.oam.dev_initializers.yaml | 10 + .../initializer/initializer_controller.go | 72 +++- pkg/controller/utils/utils.go | 43 +++ pkg/oam/labels.go | 3 + pkg/oam/util/helper.go | 2 +- pkg/webhook/core.oam.dev/register.go | 2 + .../componentdefinition/validating_handler.go | 2 +- .../initializer/validating_handler.go | 104 ++++++ test/e2e-test/initializer_test.go | 350 +++++++++++++----- 12 files changed, 522 insertions(+), 120 deletions(-) create mode 100644 pkg/webhook/core.oam.dev/v1alpha2/initializer/validating_handler.go diff --git a/apis/core.oam.dev/v1beta1/initializer_type.go b/apis/core.oam.dev/v1beta1/initializer_type.go index 883414a05..a6af53b2b 100644 --- a/apis/core.oam.dev/v1beta1/initializer_type.go +++ b/apis/core.oam.dev/v1beta1/initializer_type.go @@ -22,6 +22,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// InitializerPhase is a label for the condition of a initializer at the current time +type InitializerPhase string + +const ( + // InitializerCheckingDependsOn means the initializer is checking the status of dependent Initializer + InitializerCheckingDependsOn InitializerPhase = "checkingDependsOn" + // InitializerInitializing means the initializer is initializing + InitializerInitializing InitializerPhase = "initializing" + // InitializerSuccess means the initializer successfully initialized the environment + InitializerSuccess InitializerPhase = "success" +) + // DependsOn refer to an object which Initializer depends on type DependsOn struct { Ref corev1.ObjectReference `json:"ref"` @@ -42,6 +54,8 @@ type InitializerStatus struct { // ConditionedStatus reflects the observed status of a resource runtimev1alpha1.ConditionedStatus `json:",inline"` + Phase InitializerPhase `json:"status,omitempty"` + // The generation observed by the Initializer controller. // +optional ObservedGeneration int64 `json:"observedGeneration"` @@ -52,6 +66,8 @@ type InitializerStatus struct { // Initializer is the Schema for the Initializer API // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced,categories={oam},shortName=init +// +kubebuilder:printcolumn:name="PHASE",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="AGE",type=date,JSONPath=".metadata.creationTimestamp" type Initializer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/charts/vela-core/crds/core.oam.dev_initializers.yaml b/charts/vela-core/crds/core.oam.dev_initializers.yaml index 014684362..7de96275b 100644 --- a/charts/vela-core/crds/core.oam.dev_initializers.yaml +++ b/charts/vela-core/crds/core.oam.dev_initializers.yaml @@ -19,7 +19,14 @@ spec: singular: initializer scope: Namespaced versions: - - name: v1beta1 + - additionalPrinterColumns: + - jsonPath: .status.status + name: PHASE + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1beta1 schema: openAPIV3Schema: description: Initializer is the Schema for the Initializer API @@ -692,6 +699,9 @@ spec: description: The generation observed by the Initializer controller. format: int64 type: integer + status: + description: InitializerPhase is a label for the condition of a initializer at the current time + type: string type: object type: object served: true diff --git a/charts/vela-core/templates/admission-webhooks/validatingWebhookConfiguration.yaml b/charts/vela-core/templates/admission-webhooks/validatingWebhookConfiguration.yaml index f15329c83..701f42316 100644 --- a/charts/vela-core/templates/admission-webhooks/validatingWebhookConfiguration.yaml +++ b/charts/vela-core/templates/admission-webhooks/validatingWebhookConfiguration.yaml @@ -190,5 +190,29 @@ webhooks: - UPDATE resources: - componentdefinitions - + - clientConfig: + caBundle: Cg== + service: + name: {{ template "kubevela.name" . }}-webhook + namespace: {{ .Release.Namespace }} + path: /validating-core-oam-dev-v1beta1-initializers + {{- if .Values.admissionWebhooks.patch.enabled }} + failurePolicy: Ignore + {{- else }} + failurePolicy: Fail + {{- end }} + name: validating.core.oam-dev.v1beta1.initializers + sideEffects: None + admissionReviewVersions: + - v1beta1 + rules: + - apiGroups: + - core.oam.dev + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - initializers {{- end -}} diff --git a/legacy/charts/vela-core-legacy/crds/core.oam.dev_initializers.yaml b/legacy/charts/vela-core-legacy/crds/core.oam.dev_initializers.yaml index f92268ac0..d58934aa9 100644 --- a/legacy/charts/vela-core-legacy/crds/core.oam.dev_initializers.yaml +++ b/legacy/charts/vela-core-legacy/crds/core.oam.dev_initializers.yaml @@ -7,6 +7,13 @@ metadata: controller-gen.kubebuilder.io/version: v0.2.4 name: initializers.core.oam.dev spec: + additionalPrinterColumns: + - JSONPath: .status.status + name: PHASE + type: string + - JSONPath: .metadata.creationTimestamp + name: AGE + type: date group: core.oam.dev names: categories: @@ -692,6 +699,9 @@ spec: description: The generation observed by the Initializer controller. format: int64 type: integer + status: + description: InitializerPhase is a label for the condition of a initializer at the current time + type: string type: object type: object version: v1beta1 diff --git a/pkg/controller/core.oam.dev/v1alpha2/initializer/initializer_controller.go b/pkg/controller/core.oam.dev/v1alpha2/initializer/initializer_controller.go index b8256e066..f1f40986e 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/initializer/initializer_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/initializer/initializer_controller.go @@ -20,7 +20,9 @@ import ( "context" "time" + cpv1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -34,8 +36,11 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + velatypes "github.com/oam-dev/kubevela/apis/types" oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" + "github.com/oam-dev/kubevela/pkg/controller/utils" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + oamutil "github.com/oam-dev/kubevela/pkg/oam/util" ) // InitializerReconcileWaitTime is the time to wait before reconcile again @@ -66,52 +71,84 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } klog.Info("Check the status of the Initializers which you depend on") - unsatisfied, err := r.checkDependsOn(ctx, init.Spec.DependsOn) + init.Status.Phase = v1beta1.InitializerCheckingDependsOn + dependsOnInitReady, err := r.checkOrInstallDependsOn(ctx, init.Spec.DependsOn) if err != nil { klog.ErrorS(err, "Initializers which you depend on are not ready") r.record.Event(init, event.Warning("Initializers which you depend on are not ready", err)) - return ctrl.Result{}, err + return r.endWithNegativeCondition(ctx, init, cpv1alpha1.ReconcileError(err)) } - if !unsatisfied { + if !dependsOnInitReady { + klog.Info("Wait for dependent Initializer to be ready") return reconcile.Result{RequeueAfter: InitializerReconcileWaitTime}, nil } - ready, err := r.applyResources(ctx, init) + init.Status.Phase = v1beta1.InitializerInitializing + appReady, err := r.applyResources(ctx, init) if err != nil { klog.ErrorS(err, "Could not create resources via application to initialize the env") r.record.Event(init, event.Warning("Could not create resources via application", err)) - return ctrl.Result{}, err + return r.endWithNegativeCondition(ctx, init, cpv1alpha1.ReconcileError(err)) } - if !ready { + if !appReady { + klog.Info("Wait for the Application created by Initializer to be ready") return reconcile.Result{RequeueAfter: InitializerReconcileWaitTime}, nil } - if err = r.updateObservedGeneration(ctx, init); err != nil { - klog.ErrorS(err, "Could not update ObservedGeneration") - r.record.Event(init, event.Warning("Could not update ObservedGeneration", err)) - return ctrl.Result{}, err + init.Status.Phase = v1beta1.InitializerSuccess + if err = r.patchStatus(ctx, init); err != nil { + klog.ErrorS(err, "Could not update status") + r.record.Event(init, event.Warning("Could not update status", err)) + return ctrl.Result{}, oamutil.EndReconcileWithNegativeCondition(ctx, r, init, cpv1alpha1.ReconcileError(err)) } return ctrl.Result{}, nil } -func (r *Reconciler) checkDependsOn(ctx context.Context, depends []v1beta1.DependsOn) (bool, error) { +// checkOrInstallDependsOn check the status of dependOn Initializer or help install the build-in Initializer. +// If the dependOn Initializer is not found and the namespace is default or vela-system, we will try to find +// and install the build-in Initializer from ConfigMap. +func (r *Reconciler) checkOrInstallDependsOn(ctx context.Context, depends []v1beta1.DependsOn) (bool, error) { for _, depend := range depends { - dependInit := new(v1beta1.Initializer) - if err := r.Client.Get(ctx, client.ObjectKey{Namespace: depend.Ref.Namespace, Name: depend.Ref.Name}, dependInit); err != nil { + dependInit, err := utils.GetInitializer(ctx, r.Client, depend.Ref.Namespace, depend.Ref.Name) + if err != nil { + // if Initializer is not found and the namespace is default or vela-system, + // try to install build-in initializer from ConfigMap + if apierrors.IsNotFound(err) && (depend.Ref.Namespace == "" || depend.Ref.Namespace == velatypes.DefaultKubeVelaNS) { + init, err := utils.GetBuildInInitializer(ctx, r.Client, depend.Ref.Name) + if err != nil { + return false, err + } + return false, r.Client.Create(ctx, init) + } return false, err } - if dependInit.Status.ObservedGeneration < dependInit.Generation { + + if dependInit.Status.Phase != v1beta1.InitializerSuccess { + klog.InfoS("Initializer you depend on is not ready", + "initializer", klog.KObj(dependInit), "phase", dependInit.Status.Phase) return false, nil } } return true, nil } -func (r *Reconciler) updateObservedGeneration(ctx context.Context, init *v1beta1.Initializer) error { +func (r *Reconciler) endWithNegativeCondition(ctx context.Context, init *v1beta1.Initializer, condition cpv1alpha1.Condition) (ctrl.Result, error) { + init.SetConditions(condition) + if err := r.patchStatus(ctx, init); err != nil { + return ctrl.Result{}, errors.WithMessage(err, "cannot update initializer status") + } + return ctrl.Result{}, errors.Errorf("object level reconcile error, type: %q, msg: %q", string(condition.Type), condition.Message) +} + +func (r *Reconciler) patchStatus(ctx context.Context, init *v1beta1.Initializer) error { + updateObservedGeneration(init) + return r.Client.Status().Patch(ctx, init, client.Merge) +} + +func updateObservedGeneration(init *v1beta1.Initializer) { if init.Status.ObservedGeneration != init.Generation { init.Status.ObservedGeneration = init.Generation } - return r.UpdateStatus(ctx, init) } func (r *Reconciler) applyResources(ctx context.Context, init *v1beta1.Initializer) (bool, error) { @@ -137,11 +174,12 @@ func (r *Reconciler) applyResources(ctx context.Context, init *v1beta1.Initializ return false, err } - klog.InfoS("Check the status of Application", "app", klog.KObj(app)) err := r.Client.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app) if err != nil { return false, err } + + klog.InfoS("Check the status of Application", "app", klog.KObj(app), "phase", app.Status.Phase) if app.Status.Phase != common.ApplicationRunning { return false, nil } diff --git a/pkg/controller/utils/utils.go b/pkg/controller/utils/utils.go index 769959804..c373d6fc7 100644 --- a/pkg/controller/utils/utils.go +++ b/pkg/controller/utils/utils.go @@ -28,7 +28,9 @@ import ( runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" mapset "github.com/deckarep/golang-set" + "github.com/ghodss/yaml" "github.com/mitchellh/hashstructure/v2" + "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -45,6 +47,7 @@ import ( commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/controller/common" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/oam" @@ -385,6 +388,46 @@ func GetUnstructuredObjectStatusCondition(obj *unstructured.Unstructured, condTy return nil, false, nil } +// GetInitializer get initializer from two level namespace +func GetInitializer(ctx context.Context, cli client.Client, namespace, name string) (*v1beta1.Initializer, error) { + init := new(v1beta1.Initializer) + req := client.ObjectKey{Namespace: namespace, Name: name} + err := cli.Get(ctx, req, init) + if kerrors.IsNotFound(err) && req.Namespace == "" { + req.Namespace = velatypes.DefaultKubeVelaNS + err = cli.Get(ctx, req, init) + return init, err + } + return init, err +} + +// GetBuildInInitializer get build-in initializer from configMap in vela-system namespace +func GetBuildInInitializer(ctx context.Context, cli client.Client, name string) (*v1beta1.Initializer, error) { + listOpts := []client.ListOption{ + client.InNamespace(velatypes.DefaultKubeVelaNS), + client.MatchingLabels{ + oam.LabelAddonsName: name, + }, + } + configMapList := new(corev1.ConfigMapList) + err := cli.List(ctx, configMapList, listOpts...) + if err != nil { + return nil, err + } + if len(configMapList.Items) != 1 { + return nil, errors.Errorf("fail to get build-in initializer %s, there are %d matched initializers", name, len(configMapList.Items)) + } + + init := new(v1beta1.Initializer) + initYaml := configMapList.Items[0].Data["initializer"] + err = yaml.Unmarshal([]byte(initYaml), init) + if err != nil { + return nil, errors.WithMessagef(err, "fail to unmarshal build-in initializer %s from configmap", name) + } + + return init, nil +} + // ReadyCondition generate ready condition for conditionType func ReadyCondition(tpy string) runtimev1alpha1.Condition { return runtimev1alpha1.Condition{ diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index 59697d593..ee62ff25d 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -56,6 +56,9 @@ const ( LabelControllerRevisionComponent = "controller.oam.dev/component" // LabelComponentRevisionHash records the hash value of a component LabelComponentRevisionHash = "app.oam.dev/component-revision-hash" + + // LabelAddonsName records the name of initializer stored in configMap + LabelAddonsName = "addons.oam.dev/type" ) const ( diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go index 900f1974e..8599a548b 100644 --- a/pkg/oam/util/helper.go +++ b/pkg/oam/util/helper.go @@ -479,7 +479,7 @@ func EndReconcileWithNegativeCondition(ctx context.Context, r client.StatusClien } // if no condition is changed, patching status can not trigger requeue, so we must return an error to // requeue the resource - return fmt.Errorf(ErrReconcileErrInCondition, condition[0].Type, condition[0].Message) + return errors.Errorf(ErrReconcileErrInCondition, condition[0].Type, condition[0].Message) } // EndReconcileWithPositiveCondition is used to handle reconcile success for a conditioned resource. diff --git a/pkg/webhook/core.oam.dev/register.go b/pkg/webhook/core.oam.dev/register.go index 3d16c89a8..44765effe 100644 --- a/pkg/webhook/core.oam.dev/register.go +++ b/pkg/webhook/core.oam.dev/register.go @@ -26,6 +26,7 @@ import ( "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationrollout" "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/component" "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/componentdefinition" + "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/initializer" "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/traitdefinition" ) @@ -37,6 +38,7 @@ func Register(mgr manager.Manager, args controller.Args) { componentdefinition.RegisterMutatingHandler(mgr, args) componentdefinition.RegisterValidatingHandler(mgr, args) traitdefinition.RegisterValidatingHandler(mgr, args) + initializer.RegisterValidatingHandler(mgr, args) applicationrollout.RegisterMutatingHandler(mgr) applicationrollout.RegisterValidatingHandler(mgr) } diff --git a/pkg/webhook/core.oam.dev/v1alpha2/componentdefinition/validating_handler.go b/pkg/webhook/core.oam.dev/v1alpha2/componentdefinition/validating_handler.go index 57945dad1..b11d59b31 100644 --- a/pkg/webhook/core.oam.dev/v1alpha2/componentdefinition/validating_handler.go +++ b/pkg/webhook/core.oam.dev/v1alpha2/componentdefinition/validating_handler.go @@ -73,7 +73,7 @@ func (h *ValidatingHandler) InjectDecoder(d *admission.Decoder) error { return nil } -// RegisterValidatingHandler will register TraitDefinition validation to webhook +// RegisterValidatingHandler will register ComponentDefinition validation to webhook func RegisterValidatingHandler(mgr manager.Manager, args controller.Args) { server := mgr.GetWebhookServer() server.Register("/validating-core-oam-dev-v1beta1-componentdefinitions", &webhook.Admission{Handler: &ValidatingHandler{ diff --git a/pkg/webhook/core.oam.dev/v1alpha2/initializer/validating_handler.go b/pkg/webhook/core.oam.dev/v1alpha2/initializer/validating_handler.go new file mode 100644 index 000000000..1b16aa8f4 --- /dev/null +++ b/pkg/webhook/core.oam.dev/v1alpha2/initializer/validating_handler.go @@ -0,0 +1,104 @@ +/* + Copyright 2021. The KubeVela Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package initializer + +import ( + "context" + "fmt" + "net/http" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/runtime/inject" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + velatypes "github.com/oam-dev/kubevela/apis/types" + controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" + "github.com/oam-dev/kubevela/pkg/controller/utils" +) + +var initializerGVR = v1beta1.SchemeGroupVersion.WithResource("initializers") + +// ValidatingHandler handles validation of initializer +type ValidatingHandler struct { + Client client.Client + + // Decoder decodes object + Decoder *admission.Decoder +} + +var _ inject.Client = &ValidatingHandler{} + +// InjectClient injects the client into the InitializerValidateHandler +func (h *ValidatingHandler) InjectClient(c client.Client) error { + if h.Client != nil { + return nil + } + h.Client = c + return nil +} + +var _ admission.DecoderInjector = &ValidatingHandler{} + +// InjectDecoder injects the decoder into the ValidatingHandler +func (h *ValidatingHandler) InjectDecoder(d *admission.Decoder) error { + h.Decoder = d + return nil +} + +var _ admission.Handler = &ValidatingHandler{} + +// Handle validate initializer +func (h *ValidatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + obj := &v1beta1.Initializer{} + if req.Resource.String() != initializerGVR.String() { + return admission.Errored(http.StatusBadRequest, fmt.Errorf("expect resource to be %s", initializerGVR)) + } + + if req.Operation == admissionv1beta1.Create || req.Operation == admissionv1beta1.Update { + err := h.Decoder.Decode(req, obj) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + for _, depend := range obj.Spec.DependsOn { + _, err = utils.GetInitializer(ctx, h.Client, depend.Ref.Namespace, depend.Ref.Name) + if err != nil { + if apierrors.IsNotFound(err) && (depend.Ref.Namespace == "" || depend.Ref.Namespace == velatypes.DefaultKubeVelaNS) { + _, err = utils.GetBuildInInitializer(ctx, h.Client, depend.Ref.Name) + if err != nil { + return admission.Denied(fmt.Sprintf("fail to get dependOn Initializer %s from err: %s", depend.Ref.Name, err.Error())) + } + continue + } + return admission.Denied(fmt.Sprintf("fail to get dependOn Initializer %s err: %s", depend.Ref.Name, err.Error())) + } + } + + } + return admission.ValidationResponse(true, "") +} + +// RegisterValidatingHandler will register initializer validation to webhook +func RegisterValidatingHandler(mgr manager.Manager, args controller.Args) { + server := mgr.GetWebhookServer() + server.Register("/validating-core-oam-dev-v1beta1-initializers", &webhook.Admission{Handler: &ValidatingHandler{}}) +} diff --git a/test/e2e-test/initializer_test.go b/test/e2e-test/initializer_test.go index fde12b40a..8d22d8bdb 100644 --- a/test/e2e-test/initializer_test.go +++ b/test/e2e-test/initializer_test.go @@ -23,12 +23,14 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -116,104 +118,7 @@ var _ = Describe("Initializer Normal tests", func() { if err != nil { return err } - if init.Status.ObservedGeneration < init.Generation { - return fmt.Errorf("environment was not successfully initialized") - } - return nil - }, 30*time.Second, 5*time.Second).Should(Succeed()) - }) - - It("Test apply initializer depends on other initializer", func() { - compName := "env2-comp" - - init := &v1beta1.Initializer{ - TypeMeta: metav1.TypeMeta{ - Kind: "Initializer", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "env2", - Namespace: namespace, - }, - Spec: v1beta1.InitializerSpec{ - AppTemplate: v1beta1.Application{ - Spec: v1beta1.ApplicationSpec{ - Components: []v1beta1.ApplicationComponent{ - { - Name: compName, - Type: "worker", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "busybox", - "cmd": []string{"sleep", "10000"}, - }), - }, - }, - }, - }, - }, - } - - By("Create Initializer env2") - Eventually(func() error { - return k8sClient.Create(ctx, init) - }, 10*time.Second, 500*time.Millisecond).Should(Succeed()) - - compName2 := "env3-comp" - init2 := &v1beta1.Initializer{ - TypeMeta: metav1.TypeMeta{ - Kind: "Initializer", - APIVersion: "core.oam.dev/v1beta1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "env3", - Namespace: namespace, - }, - Spec: v1beta1.InitializerSpec{ - AppTemplate: v1beta1.Application{ - Spec: v1beta1.ApplicationSpec{ - Components: []v1beta1.ApplicationComponent{ - { - Name: compName2, - Type: "worker", - Properties: util.Object2RawExtension(map[string]interface{}{ - "image": "busybox", - "cmd": []string{"sleep", "10000"}, - }), - }, - }, - }, - }, - DependsOn: []v1beta1.DependsOn{ - { - Ref: corev1.ObjectReference{ - APIVersion: "core.oam.dev/v1beta1", - Kind: "Initializer", - Name: "env2", - Namespace: namespace, - }, - }, - }, - }, - } - - By("Create Initializer env3 which depends on env2") - Eventually(func() error { - return k8sClient.Create(ctx, init2) - }, 10*time.Second, 500*time.Millisecond).Should(Succeed()) - - By("Verify the application is created successfully") - app2 := new(v1beta1.Application) - Eventually(func() error { - return k8sClient.Get(ctx, client.ObjectKey{Name: init2.Name, Namespace: namespace}, app2) - }, 60*time.Second, 2*time.Millisecond).Should(Succeed()) - - By("Verify the initializer env3 successfully initialized the environment") - Eventually(func() error { - err := k8sClient.Get(ctx, client.ObjectKey{Name: init2.Name, Namespace: namespace}, init2) - if err != nil { - return err - } - if init2.Status.ObservedGeneration < init2.Generation { + if init.Status.Phase != v1beta1.InitializerSuccess { return fmt.Errorf("environment was not successfully initialized") } return nil @@ -268,10 +173,257 @@ var _ = Describe("Initializer Normal tests", func() { if err != nil { return err } - if init.Status.ObservedGeneration < init.Generation { + if init.Status.Phase != v1beta1.InitializerSuccess { return fmt.Errorf("environment was not successfully initialized") } return nil }, 30*time.Second, 5*time.Second).Should(Succeed()) }) + + Context("Test apply initializer depends on other initializer", func() { + + It("initializer depends on existing initializer", func() { + compName := "env2-comp" + + init := &v1beta1.Initializer{ + TypeMeta: metav1.TypeMeta{ + Kind: "Initializer", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "env2", + Namespace: namespace, + }, + Spec: v1beta1.InitializerSpec{ + AppTemplate: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName, + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "10000"}, + }), + }, + }, + }, + }, + }, + } + + By("Create Initializer env2") + Eventually(func() error { + return k8sClient.Create(ctx, init) + }, 10*time.Second, 500*time.Millisecond).Should(Succeed()) + + compName2 := "env3-comp" + init2 := &v1beta1.Initializer{ + TypeMeta: metav1.TypeMeta{ + Kind: "Initializer", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "env3", + Namespace: namespace, + }, + Spec: v1beta1.InitializerSpec{ + AppTemplate: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: compName2, + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "10000"}, + }), + }, + }, + }, + }, + DependsOn: []v1beta1.DependsOn{ + { + Ref: corev1.ObjectReference{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "Initializer", + Name: "env2", + Namespace: namespace, + }, + }, + }, + }, + } + + By("Create Initializer env3 which depends on env2") + Eventually(func() error { + return k8sClient.Create(ctx, init2) + }, 10*time.Second, 500*time.Millisecond).Should(Succeed()) + + By("Verify the application is created successfully") + app2 := new(v1beta1.Application) + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: init2.Name, Namespace: namespace}, app2) + }, 60*time.Second, 2*time.Millisecond).Should(Succeed()) + + By("Verify the initializer env3 successfully initialized the environment") + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: init2.Name, Namespace: namespace}, init2) + if err != nil { + return err + } + if init2.Status.Phase != v1beta1.InitializerSuccess { + return fmt.Errorf("environment was not successfully initialized") + } + return nil + }, 30*time.Second, 5*time.Second).Should(Succeed()) + }) + + It("initializer depends on non build-in initializer, should be rejected by webhook", func() { + init := &v1beta1.Initializer{ + TypeMeta: metav1.TypeMeta{ + Kind: "Initializer", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "env-depends", + Namespace: namespace, + }, + Spec: v1beta1.InitializerSpec{ + AppTemplate: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: "", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "10000"}, + }), + }, + }, + }, + }, + DependsOn: []v1beta1.DependsOn{ + { + Ref: corev1.ObjectReference{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "Initializer", + Name: "non-build-in", + }, + }, + }, + }, + } + + By("Create Initializer env-depends") + Expect(k8sClient.Create(ctx, init)).Should(HaveOccurred()) + }) + + It("initializer depends on build-in initializer", func() { + initCm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "build-in", + Namespace: velatypes.DefaultKubeVelaNS, + Labels: map[string]string{ + "addons.oam.dev/type": "build-in", + }, + }, + Data: map[string]string{ + "initializer": initYaml, + }, + } + + By("create build-in addon") + Expect(k8sClient.Create(ctx, initCm)).Should(Succeed()) + + init := &v1beta1.Initializer{ + TypeMeta: metav1.TypeMeta{ + Kind: "Initializer", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "env-depends-buildin", + Namespace: namespace, + }, + Spec: v1beta1.InitializerSpec{ + AppTemplate: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []v1beta1.ApplicationComponent{ + { + Name: "", + Type: "worker", + Properties: util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "10000"}, + }), + }, + }, + }, + }, + DependsOn: []v1beta1.DependsOn{ + { + Ref: corev1.ObjectReference{ + APIVersion: "core.oam.dev/v1beta1", + Kind: "Initializer", + Name: "build-in", + Namespace: velatypes.DefaultKubeVelaNS, + }, + }, + }, + }, + } + + By("Create Initializer env-depends") + Expect(k8sClient.Create(ctx, init)).Should(Succeed()) + + By("Check build-in Initializer is ready") + buildInInit := new(v1beta1.Initializer) + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "build-in", Namespace: "vela-system"}, buildInInit) + if err != nil { + return err + } + if buildInInit.Status.Phase == v1beta1.InitializerSuccess { + return nil + } + return fmt.Errorf("initializer %s is not ready", buildInInit.Name) + }, 30*time.Second, 500*time.Millisecond).Should(Succeed()) + + By("Check Initializer env-depends-buildin is ready") + Eventually(func() error { + err := k8sClient.Get(ctx, client.ObjectKey{Name: "env-depends-buildin", Namespace: namespace}, buildInInit) + if err != nil { + return err + } + if buildInInit.Status.Phase == v1beta1.InitializerSuccess { + return nil + } + return errors.New("initializer env-depends-buildin is not ready") + }, 30*time.Second, 500*time.Millisecond).Should(Succeed()) + }) + }) }) + +var initYaml = ` +apiVersion: core.oam.dev/v1beta1 +kind: Initializer +metadata: + annotations: + addons.oam.dev/description: Kruise is a Kubernetes extended suite for application automations + name: build-in + namespace: vela-system +spec: + appTemplate: + spec: + components: + - name: kruise-repo + type: worker + properties: + image: busybox, + cmd: ["sleep", "10000"] +`