diff --git a/apis/types/event.go b/apis/types/event.go new file mode 100644 index 000000000..f69dde95c --- /dev/null +++ b/apis/types/event.go @@ -0,0 +1,47 @@ +/* +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 types + +// reason for Application +const ( + ReasonParsed = "Parsed" + ReasonRendered = "Rendered" + ReasonApplied = "Applied" + ReasonHealthCheck = "HealthChecked" + ReasonDeployed = "Deployed" + + ReasonFailedParse = "FailedParse" + ReasonFailedRender = "FailedRender" + ReasonFailedApply = "FailedApply" + ReasonFailedHealthCheck = "FailedHealthCheck" + ReasonFailedGC = "FailedGC" +) + +// event message for Application +const ( + MessageParsed = "Parsed successfully" + MessageRendered = "Rendered successfully" + MessageApplied = "Applied successfully" + MessageHealthCheck = "Health checked healthy" + MessageDeployed = "Deployed successfully" + + MessageFailedParse = "fail to parse application, err: %v" + MessageFailedRender = "fail to render application, err: %v" + MessageFailedApply = "fail to apply component, err: %v" + MessageFailedHealthCheck = "fail to health check, err: %v" + MessageFailedGC = "fail to garbage collection, err: %v" +) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 6a9fce3b9..29772bce5 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -18,6 +18,7 @@ package application import ( "context" + "fmt" "time" "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" @@ -25,9 +26,11 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/go-logr/logr" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,6 +38,7 @@ 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" "github.com/oam-dev/kubevela/pkg/appfile" core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" "github.com/oam-dev/kubevela/pkg/dsl/definition" @@ -58,6 +62,7 @@ type Reconciler struct { pd *definition.PackageDiscover Log logr.Logger Scheme *runtime.Scheme + Recorder record.EventRecorder applicator apply.Applicator appRevisionLimit int } @@ -119,6 +124,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { if err != nil { applog.Error(err, "[Handle Parse]") app.Status.SetConditions(errorCondition("Parsed", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedParse, fmt.Sprintf(velatypes.MessageFailedParse, err)) return handler.handleErr(err) } @@ -129,8 +135,11 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { if err != nil { applog.Error(err, "[Handle Calculate Revision]") app.Status.SetConditions(errorCondition("Parsed", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedParse, fmt.Sprintf(velatypes.MessageFailedParse, err)) return handler.handleErr(err) } + + r.Recorder.Event(app, corev1.EventTypeNormal, velatypes.ReasonParsed, velatypes.MessageParsed) // Record the revision so it can be used to render data in context.appRevision generatedAppfile.RevisionName = appRev.Name @@ -140,6 +149,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { if err != nil { applog.Error(err, "[Handle GenerateApplicationConfiguration]") app.Status.SetConditions(errorCondition("Built", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedRender, fmt.Sprintf(velatypes.MessageFailedRender, err)) return handler.handleErr(err) } @@ -147,6 +157,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { if err != nil { applog.Error(err, "[Handle resourceTracker]") app.Status.SetConditions(errorCondition("Handle resourceTracker", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedRender, fmt.Sprintf(velatypes.MessageFailedRender, err)) return handler.handleErr(err) } @@ -154,15 +165,18 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { oamutil.PassLabelAndAnnotation(app, ac) app.Status.SetConditions(readyCondition("Built")) + r.Recorder.Event(app, corev1.EventTypeNormal, velatypes.ReasonRendered, velatypes.MessageRendered) applog.Info("apply application revision & component to the cluster") // apply application revision & component to the cluster if err := handler.apply(ctx, appRev, ac, comps); err != nil { applog.Error(err, "[Handle apply]") app.Status.SetConditions(errorCondition("Applied", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedApply, fmt.Sprintf(velatypes.MessageFailedApply, err)) return handler.handleErr(err) } app.Status.SetConditions(readyCondition("Applied")) + r.Recorder.Event(app, corev1.EventTypeNormal, velatypes.ReasonApplied, velatypes.MessageApplied) app.Status.Phase = common.ApplicationHealthChecking applog.Info("check application health status") // check application health status @@ -170,6 +184,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { if err != nil { applog.Error(err, "[status aggregate]") app.Status.SetConditions(errorCondition("HealthCheck", err)) + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedHealthCheck, fmt.Sprintf(velatypes.MessageFailedHealthCheck, err)) return handler.handleErr(err) } if !healthy { @@ -181,11 +196,15 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } app.Status.Services = appCompStatus app.Status.SetConditions(readyCondition("HealthCheck")) + r.Recorder.Event(app, corev1.EventTypeNormal, velatypes.ReasonHealthCheck, velatypes.MessageHealthCheck) app.Status.Phase = common.ApplicationRunning + err = garbageCollection(ctx, handler) if err != nil { applog.Error(err, "[Garbage collection]") + r.Recorder.Event(app, corev1.EventTypeWarning, velatypes.ReasonFailedGC, fmt.Sprintf(velatypes.MessageFailedGC, err)) } + // Gather status of components var refComps []v1alpha1.TypedReference for _, comp := range comps { @@ -197,6 +216,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { }) } app.Status.Components = refComps + r.Recorder.Event(app, corev1.EventTypeNormal, velatypes.ReasonDeployed, velatypes.MessageDeployed) return ctrl.Result{}, r.UpdateStatus(ctx, app) } @@ -235,6 +255,7 @@ func Setup(mgr ctrl.Manager, args core.Args, _ logging.Logger) error { Client: mgr.GetClient(), Log: ctrl.Log.WithName("Application"), Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("Application"), dm: args.DiscoveryMapper, pd: args.PackageDiscover, applicator: apply.NewAPIApplicator(mgr.GetClient()), diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index 4d2535c98..cea0d081a 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go @@ -26,6 +26,8 @@ import ( "strconv" "time" + velatypes "github.com/oam-dev/kubevela/apis/types" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -94,6 +96,16 @@ var _ = Describe("Test Application Controller", func() { }, } + appFailParse := appwithNoTrait.DeepCopy() + appFailParse.SetName("app-fail-to-parsed") + appFailParse.Spec.Components[0].Type = "fakeWorker" + + appFailRender := appwithNoTrait.DeepCopy() + appFailRender.SetName("app-fail-to-render") + appFailRender.Spec.Components[0].Properties = runtime.RawExtension{ + Raw: []byte(`{"cmd1":["sleep","1000"],"image1":"busybox"}`), + } + appImportPkg := &v1beta1.Application{ TypeMeta: metav1.TypeMeta{ Kind: "Application", @@ -249,6 +261,73 @@ var _ = Describe("Test Application Controller", func() { By("[TEST] Clean up resources after an integration test") }) + It("app step will set event", func() { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-test-app-without-trait-event", + }, + } + appwithNoTrait.SetNamespace(ns.Name) + Expect(k8sClient.Create(ctx, ns)).Should(BeNil()) + Expect(k8sClient.Create(ctx, appwithNoTrait.DeepCopyObject())).Should(BeNil()) + + appKey := client.ObjectKey{ + Name: appwithNoTrait.Name, + Namespace: appwithNoTrait.Namespace, + } + + reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey}) + + events, err := recorder.GetEventsWithName(appwithNoTrait.Name) + Expect(err).Should(BeNil()) + Expect(len(events)).ShouldNot(Equal(0)) + for _, event := range events { + Expect(event.EventType).ShouldNot(Equal(corev1.EventTypeWarning)) + Expect(event.EventType).Should(Equal(corev1.EventTypeNormal)) + } + + // fail to parse application + appFailParse.SetNamespace(ns.Name) + appFailParseKey := client.ObjectKey{ + Name: appFailParse.Name, + Namespace: appFailParse.Namespace, + } + + Expect(k8sClient.Create(ctx, appFailParse.DeepCopyObject())).Should(BeNil()) + reconcileOnce(reconciler, reconcile.Request{NamespacedName: appFailParseKey}) + + parseEvents, err := recorder.GetEventsWithName(appFailParse.Name) + Expect(err).Should(BeNil()) + Expect(len(parseEvents)).Should(Equal(1)) + for _, event := range parseEvents { + Expect(event.EventType).Should(Equal(corev1.EventTypeWarning)) + Expect(event.Reason).Should(Equal(velatypes.ReasonFailedParse)) + } + + // fail to render application + appFailRender.SetNamespace(ns.Name) + appFailRenderKey := client.ObjectKey{ + Name: appFailRender.Name, + Namespace: appFailRender.Namespace, + } + Expect(k8sClient.Create(ctx, appFailRender.DeepCopyObject())).Should(BeNil()) + reconcileOnce(reconciler, reconcile.Request{NamespacedName: appFailRenderKey}) + + renderEvents, err := recorder.GetEventsWithName(appFailRender.Name) + Expect(err).Should(BeNil()) + Expect(len(renderEvents)).Should(Equal(2)) + + var count int + for _, event := range renderEvents { + if event.EventType == corev1.EventTypeWarning { + Expect(event.Reason).Should(Equal(velatypes.ReasonFailedRender)) + count++ + } + } + Expect(count).Should(Equal(1)) + + }) + It("app can consume db secret generated by other application", func() { var ( appName = "webapp" @@ -1524,6 +1603,10 @@ func reconcileRetry(r reconcile.Reconciler, req reconcile.Request) { }, 30*time.Second, time.Second).Should(BeNil()) } +func reconcileOnce(r reconcile.Reconciler, req reconcile.Request) { + r.Reconcile(req) +} + const ( scopeDefYaml = `apiVersion: core.oam.dev/v1beta1 kind: ScopeDefinition diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go index 85bac7eaa..9bcba8a43 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go @@ -18,12 +18,17 @@ package application import ( "context" + "fmt" "math/rand" "os" "path/filepath" "testing" "time" + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/api/meta" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -55,6 +60,7 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var cfg *rest.Config +var recorder = NewFakeRecorder(10000) var k8sClient client.Client var testEnv *envtest.Environment var testScheme = runtime.NewScheme() @@ -128,6 +134,7 @@ var _ = BeforeSuite(func(done Done) { Scheme: testScheme, dm: dm, pd: pd, + Recorder: recorder, appRevisionLimit: appRevisionLimit, } // setup the controller manager since we need the component handler to run in the background @@ -168,3 +175,71 @@ var _ = AfterSuite(func() { Expect(err).ToNot(HaveOccurred()) close(stop) }) + +type FakeRecorder struct { + Events chan string + Message map[string][]*Events +} + +type Events struct { + Name string + Namespace string + EventType string + Reason string + Message string +} + +func (f *FakeRecorder) Event(object runtime.Object, eventtype, reason, message string) { + if f.Events != nil { + objectMeta, err := meta.Accessor(object) + if err != nil { + return + } + + event := &Events{ + Name: objectMeta.GetName(), + Namespace: objectMeta.GetNamespace(), + EventType: eventtype, + Reason: reason, + Message: message, + } + + records, ok := f.Message[objectMeta.GetName()] + if !ok { + f.Message[objectMeta.GetName()] = []*Events{event} + return + } + + records = append(records, event) + f.Message[objectMeta.GetName()] = records + + } +} + +func (f *FakeRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { + if f.Events != nil { + f.Events <- fmt.Sprintf(eventtype+" "+reason+" "+messageFmt, args...) + } +} + +func (f *FakeRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { + f.Eventf(object, eventtype, reason, messageFmt, args...) +} + +func (f *FakeRecorder) GetEventsWithName(name string) ([]*Events, error) { + records, ok := f.Message[name] + if !ok { + return nil, errors.New("not found events") + } + + return records, nil +} + +// NewFakeRecorder creates new fake event recorder with event channel with +// buffer of given size. +func NewFakeRecorder(bufferSize int) *FakeRecorder { + return &FakeRecorder{ + Events: make(chan string, bufferSize), + Message: make(map[string][]*Events), + } +}