diff --git a/config/definition/default.yaml b/config/definition/default.yaml index d42ef6b..6e04b83 100644 --- a/config/definition/default.yaml +++ b/config/definition/default.yaml @@ -109,6 +109,31 @@ spec: } } + if parameter.createService == true { + service: { + apiVersion: "v1" + kind: "Service" + metadata: { + name: parameter.name + namespace: triggerService.namespace + } + spec:{ + selector:{ + "app.kubernetes.io/name": parameter.name + "trigger.oam.dev/name": triggerService.name + } + ports:[{ + for _,v in parameter.service.ports { + name: v.name + port: v.port + targetPort: v.targetPort + } + }] + type: parameter.service.type + } + } + } + triggerService: { name: string namespace: *"vela-system" | string @@ -147,4 +172,17 @@ spec: ip?: string hostNames?: [...string] }] + createService: *true | bool + service: { + ports: *[{ + name: "default" + port: 80 + targetPort: 80 + }] | [...{ + name: string + port: int + targetPort: int + }] + type: *"ClusterIP" | "NodePort" | "LoadBalancer" | "ExternalName" + } } diff --git a/config/manager/role.yaml b/config/manager/role.yaml index 675adf7..031d7d0 100644 --- a/config/manager/role.yaml +++ b/config/manager/role.yaml @@ -5,7 +5,9 @@ metadata: creationTimestamp: null name: kube-trigger-manager-role rules: -- resources: +- apiGroups: + - "" + resources: - configmaps verbs: - get diff --git a/controllers/triggerservice/suite_test.go b/controllers/triggerservice/suite_test.go new file mode 100644 index 0000000..7be1c77 --- /dev/null +++ b/controllers/triggerservice/suite_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 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 triggerservice + +import ( + "context" + "path/filepath" + "testing" + + standardv1alpha1 "github.com/kubevela/kube-trigger/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + //+kubebuilder:scaffold:imports +) + +// 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 k8sClient client.Client +var reconciler *Reconciler +var controllerDone context.CancelFunc +var testEnv *envtest.Environment + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + useExistingCluster := false + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd"), filepath.Join("..", "..", "config", "definition")}, + ErrorIfCRDPathMissing: true, + Config: ctrl.GetConfigOrDie(), + UseExistingCluster: &useExistingCluster, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = standardv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + reconciler = &Reconciler{ + Client: k8sClient, + Scheme: scheme.Scheme, + } + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + MetricsBindAddress: "0", + LeaderElection: false, + LeaderElectionNamespace: "default", + LeaderElectionID: "test", + }) + var ctx context.Context + ctx, controllerDone = context.WithCancel(context.Background()) + go func() { + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + if controllerDone != nil { + controllerDone() + } + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/controllers/triggerservice/triggerservice_controller.go b/controllers/triggerservice/triggerservice_controller.go index 8749eec..1a12d52 100644 --- a/controllers/triggerservice/triggerservice_controller.go +++ b/controllers/triggerservice/triggerservice_controller.go @@ -20,7 +20,6 @@ import ( "context" "fmt" - "cuelang.org/go/cue" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -35,6 +34,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "cuelang.org/go/cue" + "github.com/kubevela/pkg/cue/cuex" "github.com/kubevela/pkg/util/slices" "github.com/kubevela/pkg/util/template/definition" @@ -91,25 +92,36 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, err } - deploymentList := &appsv1.DeploymentList{} - if err := r.List(ctx, deploymentList, client.MatchingLabels(map[string]string{ - triggerNameLabel: ts.Name, - })); err != nil { + if err := r.handleWorker(ctx, ts); err != nil { + logger.Error(err, "failed to handle trigger worker") return ctrl.Result{}, err } - // if no worker deployment exists, create a new one to run trigger - if len(deploymentList.Items) == 0 { - if err := r.createWorker(ctx, ts); err != nil { - logger.Error(err, "failed to create worker deployment for trigger") - return ctrl.Result{}, err - } - logger.Info("successfully create worker deployment for trigger") + return ctrl.Result{}, nil +} + +func (r *Reconciler) handleWorker(ctx context.Context, ts *standardv1alpha1.TriggerService) error { + + v, err := r.loadTemplateCueValue(ctx, ts) + if err != nil { + return err } - return ctrl.Result{}, nil + if err := r.createRoles(ctx, ts, v); err != nil { + return err + } + + if err := r.createDeployment(ctx, ts, v); err != nil { + return err + } + + if err := r.createService(ctx, ts, v); err != nil { + return err + } + + return nil } -func (r *Reconciler) createWorker(ctx context.Context, ts *standardv1alpha1.TriggerService) error { +func (r *Reconciler) loadTemplateCueValue(ctx context.Context, ts *standardv1alpha1.TriggerService) (cue.Value, error) { templateName := "default" opts := make([]cuex.CompileOption, 0) opts = append(opts, cuex.WithExtraData("triggerService", map[string]string{ @@ -124,25 +136,38 @@ func (r *Reconciler) createWorker(ctx context.Context, ts *standardv1alpha1.Trig } template, err := definition.NewTemplateLoader(ctx, r.Client).LoadTemplate(ctx, templateName, definition.WithType(triggertypes.DefinitionTypeTriggerWorker)) if err != nil { - return err + return cue.Value{}, err } + v, err := cuex.CompileStringWithOptions(ctx, template.Compile(), opts...) if err != nil { - return err - } - if err := r.createRoles(ctx, ts, v); err != nil { - return err + return cue.Value{}, err } + + return v, nil +} + +func (r *Reconciler) createDeployment(ctx context.Context, ts *standardv1alpha1.TriggerService, v cue.Value) error { data, err := v.LookupPath(cue.ParsePath("deployment")).MarshalJSON() if err != nil { return err } - deploy := &appsv1.Deployment{} - if err := json.Unmarshal(data, deploy); err != nil { + + expectedDeployment := new(appsv1.Deployment) + if err := json.Unmarshal(data, expectedDeployment); err != nil { + return err + } + + existDeployment := new(appsv1.Deployment) + if err := r.Get(ctx, types.NamespacedName{Namespace: ts.Namespace, Name: ts.Name}, existDeployment); err != nil { + if apierrors.IsNotFound(err) { + utils.SetOwnerReference(expectedDeployment, ts) + return r.Create(ctx, expectedDeployment) + } return err } - utils.SetOwnerReference(deploy, ts) - return r.Create(ctx, deploy) + + return r.Patch(ctx, expectedDeployment, client.Merge) } func (r *Reconciler) createRoles(ctx context.Context, ts *standardv1alpha1.TriggerService, v cue.Value) error { @@ -183,6 +208,44 @@ func (r *Reconciler) createRoles(ctx context.Context, ts *standardv1alpha1.Trigg return nil } +func (r *Reconciler) createService(ctx context.Context, ts *standardv1alpha1.TriggerService, v cue.Value) error { + needCreateService, err := v.LookupPath(cue.ParsePath("parameter.createService")).Bool() + if err != nil { + return err + } + + if !needCreateService { + existSvc := new(corev1.Service) + if err := r.Get(ctx, types.NamespacedName{Namespace: ts.Namespace, Name: ts.Name}, existSvc); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + return r.Delete(ctx, existSvc) + } + + data, err := v.LookupPath(cue.ParsePath("service")).MarshalJSON() + if err != nil { + return err + } + + expectSvc := new(corev1.Service) + if err := json.Unmarshal(data, expectSvc); err != nil { + return err + } + + existSvc := new(corev1.Service) + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: ts.Namespace, Name: ts.Name}, existSvc); err != nil { + if apierrors.IsNotFound(err) { + utils.SetOwnerReference(expectSvc, ts) + return r.Client.Create(ctx, expectSvc) + } + return err + } + return r.Client.Patch(ctx, expectSvc, client.Merge) +} + func (r *Reconciler) handleTriggerConfig(ctx context.Context, ts *standardv1alpha1.TriggerService) error { // Add TriggerService into ConfigMap jsonByte, err := json.Marshal(ts.Spec) diff --git a/controllers/triggerservice/triggerservice_controller_test.go b/controllers/triggerservice/triggerservice_controller_test.go new file mode 100644 index 0000000..8e361f8 --- /dev/null +++ b/controllers/triggerservice/triggerservice_controller_test.go @@ -0,0 +1,364 @@ +/* +Copyright 2022 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 triggerservice + +import ( + "context" + "encoding/json" + "github.com/kubevela/kube-trigger/pkg/source/builtin/k8sresourcewatcher/types" + "github.com/kubevela/pkg/util/slices" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "path/filepath" + + "github.com/kubevela/kube-trigger/api/v1alpha1" + "github.com/kubevela/kube-trigger/controllers/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" +) + +var _ = Describe("TriggerinstanceController", Ordered, func() { + ctx := context.TODO() + + ts := v1alpha1.TriggerService{} + tsJSON, _ := yaml.YAMLToJSON([]byte(normalTriggerInstance)) + + tsNoService := v1alpha1.TriggerService{} + tsNoServiceJSON, _ := yaml.YAMLToJSON([]byte(normalTriggerServiceWithOutService)) + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-system", + }, + } + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-trigger", + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "cluster-admin", + APIGroup: "rbac.authorization.k8s.io", + }, + } + + BeforeEach(func() { + Expect(k8sClient.Create(ctx, ns.DeepCopy())).Should(SatisfyAny(Succeed(), &utils.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, clusterRoleBinding.DeepCopy())).Should(SatisfyAny(Succeed(), &utils.AlreadyExistMatcher{})) + for _, file := range []string{"bump-application-revision", "create-event-listener", "default", "patch-resource", "record-event"} { + Expect(utils.InstallDefinition(ctx, k8sClient, filepath.Join("../../config/definition", file+".yaml"))). + Should(SatisfyAny(Succeed(), &utils.AlreadyExistMatcher{})) + } + Expect(json.Unmarshal(tsJSON, &ts)).Should(BeNil()) + Expect(json.Unmarshal(tsNoServiceJSON, &tsNoService)).Should(BeNil()) + }) + + It("test normal triggerInstance create relevant resource", func() { + tsKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + Expect(k8sClient.Create(ctx, ts.DeepCopy())).Should(BeNil()) + Expect(utils.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: tsKey})).Should(BeNil()) + + newTs := v1alpha1.TriggerService{} + Expect(k8sClient.Get(ctx, tsKey, &newTs)).Should(BeNil()) + Expect(newTs.Name).Should(Equal("kubetrigger-sample-config-service")) + + cm := corev1.ConfigMap{} + cmKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + + Expect(k8sClient.Get(ctx, cmKey, &cm)).Should(BeNil()) + Expect(len(cm.OwnerReferences)).Should(Equal(1)) + Expect(cm.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(cm.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + clusterRoleBing := rbacv1.ClusterRoleBinding{} + crbKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: "kube-trigger", + } + Expect(k8sClient.Get(ctx, crbKey, &clusterRoleBing)).Should(BeNil()) + subject := rbacv1.Subject{ + Kind: "ServiceAccount", + Name: "kube-trigger", + Namespace: ts.Namespace, + } + Expect(slices.Contains(clusterRoleBing.Subjects, subject)).Should(BeTrue()) + + sa := corev1.ServiceAccount{} + saKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: "kube-trigger", + } + Expect(k8sClient.Get(ctx, saKey, &sa)).Should(BeNil()) + Expect(len(sa.OwnerReferences)).Should(Equal(1)) + Expect(sa.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(sa.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + deploy := appsv1.Deployment{} + deployKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + Expect(k8sClient.Get(ctx, deployKey, &deploy)).Should(BeNil()) + Expect(len(deploy.OwnerReferences)).Should(Equal(1)) + Expect(deploy.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(deploy.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + svc := corev1.Service{} + svcKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + Expect(k8sClient.Get(ctx, svcKey, &svc)).Should(BeNil()) + Expect(len(svc.OwnerReferences)).Should(Equal(1)) + Expect(svc.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(svc.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + }) + + It("test normal triggerInstance create relevant resource, update config and restart pod", func() { + tsKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + + newTs := v1alpha1.TriggerService{} + Expect(k8sClient.Get(ctx, tsKey, &newTs)).Should(BeNil()) + Expect(newTs.Name).Should(Equal("kubetrigger-sample-config-service")) + + triggers := newTs.Spec.Triggers + for i := range triggers { + bytes, err := triggers[i].Source.Properties.MarshalJSON() + if err != nil { + return + } + conf := new(types.Config) + err = json.Unmarshal(bytes, conf) + + conf.Events = append(conf.Events, types.EventTypeCreate) + marshal, err := json.Marshal(conf) + extension := &runtime.RawExtension{Raw: marshal} + triggers[i].Source.Properties = extension + } + + Expect(k8sClient.Update(ctx, &newTs)).Should(BeNil()) + Expect(utils.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: tsKey})).Should(BeNil()) + + cm := corev1.ConfigMap{} + cmKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + + Expect(k8sClient.Get(ctx, cmKey, &cm)).Should(BeNil()) + Expect(len(cm.OwnerReferences)).Should(Equal(1)) + Expect(cm.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(cm.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + clusterRoleBing := rbacv1.ClusterRoleBinding{} + crbKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: "kube-trigger", + } + Expect(k8sClient.Get(ctx, crbKey, &clusterRoleBing)).Should(BeNil()) + subject := rbacv1.Subject{ + Kind: "ServiceAccount", + Name: "kube-trigger", + Namespace: ts.Namespace, + } + Expect(slices.Contains(clusterRoleBing.Subjects, subject)).Should(BeTrue()) + + sa := corev1.ServiceAccount{} + saKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: "kube-trigger", + } + Expect(k8sClient.Get(ctx, saKey, &sa)).Should(BeNil()) + Expect(len(sa.OwnerReferences)).Should(Equal(1)) + Expect(sa.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(sa.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + deploy := appsv1.Deployment{} + deployKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + Expect(k8sClient.Get(ctx, deployKey, &deploy)).Should(BeNil()) + Expect(len(deploy.OwnerReferences)).Should(Equal(1)) + Expect(deploy.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(deploy.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + svc := corev1.Service{} + svcKey := client.ObjectKey{ + Namespace: ts.Namespace, + Name: ts.Name, + } + Expect(k8sClient.Get(ctx, svcKey, &svc)).Should(BeNil()) + Expect(len(svc.OwnerReferences)).Should(Equal(1)) + Expect(svc.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(svc.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + }) + + It("test normal triggerInstance create relevant resource without service", func() { + tsNoServiceKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: tsNoService.Name, + } + Expect(k8sClient.Create(ctx, tsNoService.DeepCopy())).Should(BeNil()) + Expect(utils.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: tsNoServiceKey})).Should(BeNil()) + + newTs := v1alpha1.TriggerService{} + Expect(k8sClient.Get(ctx, tsNoServiceKey, &newTs)).Should(BeNil()) + Expect(newTs.Name).Should(Equal("kubetrigger-sample-config-no-service")) + + cm := corev1.ConfigMap{} + cmKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: tsNoService.Name, + } + + Expect(k8sClient.Get(ctx, cmKey, &cm)).Should(BeNil()) + Expect(len(cm.OwnerReferences)).Should(Equal(1)) + Expect(cm.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(cm.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + clusterRoleBing := rbacv1.ClusterRoleBinding{} + crbKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: "kube-trigger", + } + Expect(k8sClient.Get(ctx, crbKey, &clusterRoleBing)).Should(BeNil()) + subject := rbacv1.Subject{ + Kind: "ServiceAccount", + Name: "kube-trigger", + Namespace: ts.Namespace, + } + Expect(slices.Contains(clusterRoleBing.Subjects, subject)).Should(BeTrue()) + + sa := corev1.ServiceAccount{} + saKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: "no-service", + } + Expect(k8sClient.Get(ctx, saKey, &sa)).Should(BeNil()) + Expect(len(sa.OwnerReferences)).Should(Equal(1)) + Expect(sa.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(sa.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + deploy := appsv1.Deployment{} + deployKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: tsNoService.Name, + } + Expect(k8sClient.Get(ctx, deployKey, &deploy)).Should(BeNil()) + Expect(len(deploy.OwnerReferences)).Should(Equal(1)) + Expect(deploy.OwnerReferences[0].Name).Should(Equal(newTs.Name)) + Expect(deploy.OwnerReferences[0].UID).Should(Equal(newTs.UID)) + + svc := corev1.Service{} + svcKey := client.ObjectKey{ + Namespace: tsNoService.Namespace, + Name: tsNoService.Name, + } + Expect(apierrors.IsNotFound(k8sClient.Get(ctx, svcKey, &svc))).Should(BeTrue()) + + }) + +}) + +const ( + normalTriggerInstance = ` +apiVersion: standard.oam.dev/v1alpha1 +kind: TriggerService +metadata: + name: kubetrigger-sample-config-service + namespace: default +spec: + # A trigger is a group of Source, Filters, and Actions. + # You can add multiple triggers. + worker: + properties: + createService: true + triggers: + - source: + type: resource-watcher + properties: + # We are interested in ConfigMap events. + apiVersion: "v1" + kind: ConfigMap + namespace: default + # Only watch update event. + events: + - update + filter: | + context: data: metadata: name: =~"this-will-trigger-update-.*" + action: + # Bump Application Revision to update Application. + type: bump-application-revision + properties: + namespace: default + # Select Applications to bump using labels. + nameSelector: + fromLabel: "watch-this" +` + normalTriggerServiceWithOutService = ` +apiVersion: standard.oam.dev/v1alpha1 +kind: TriggerService +metadata: + name: kubetrigger-sample-config-no-service + namespace: default +spec: + # A trigger is a group of Source, Filters, and Actions. + # You can add multiple triggers. + worker: + properties: + createService: false + serviceAccount: no-service + triggers: + - source: + type: resource-watcher + properties: + # We are interested in ConfigMap events. + apiVersion: "v1" + kind: ConfigMap + namespace: default + # Only watch update event. + events: + - update + filter: | + context: data: metadata: name: =~"this-will-trigger-update-.*" + action: + # Bump Application Revision to update Application. + type: bump-application-revision + properties: + namespace: default + # Select Applications to bump using labels. + namSelector: + fromLabel: "watch-this" +` +) diff --git a/controllers/utils/test_util.go b/controllers/utils/test_util.go new file mode 100644 index 0000000..5765842 --- /dev/null +++ b/controllers/utils/test_util.go @@ -0,0 +1,121 @@ +/* +Copyright 2022 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 utils + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" +) + +// ReconcileOnce will just reconcile once. +func ReconcileOnce(r reconcile.Reconciler, req reconcile.Request) error { + if _, err := r.Reconcile(context.TODO(), req); err != nil { + return err + } + return nil +} + +// InstallDefaultDefinition install the default template +func InstallDefaultDefinition() error { + defaultDefinitionPath := filepath.Join("..", "..", "config", "definition", "default.yaml") + cmd := exec.Command("kubectl", "apply", "-f", defaultDefinitionPath) + return cmd.Run() +} + +// UnInstallDefaultDefinition uninstall the default template +func UnInstallDefaultDefinition() error { + defaultDefinitionPath := filepath.Join("..", "..", "config", "definition", "default.yaml") + cmd := exec.Command("kubectl", "delete", "-f", defaultDefinitionPath) + return cmd.Run() +} + +// InstallDefinition install definition before test +func InstallDefinition(ctx context.Context, cli client.Client, defPath string) error { + b, err := os.ReadFile(defPath) + if err != nil { + return err + } + s := string(b) + defJSON, err := yaml.YAMLToJSON([]byte(s)) + if err != nil { + return err + } + u := &unstructured.Unstructured{} + if err := json.Unmarshal(defJSON, u); err != nil { + return err + } + return cli.Create(ctx, u.DeepCopy()) +} + +// AlreadyExistMatcher matches the error to be already exist +type AlreadyExistMatcher struct { +} + +// Match matches error. +func (matcher AlreadyExistMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, nil + } + actualError := actual.(error) + return apierrors.IsAlreadyExists(actualError), nil +} + +// FailureMessage builds an error message. +func (matcher AlreadyExistMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be already exist") +} + +// NegatedFailureMessage builds an error message. +func (matcher AlreadyExistMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be already exist") +} + +var _ types.GomegaMatcher = NotFoundMatcher{} + +// NotFoundMatcher matches the error to be not found. +type NotFoundMatcher struct { +} + +// Match matches the api error. +func (matcher NotFoundMatcher) Match(actual interface{}) (success bool, err error) { + if actual == nil { + return false, nil + } + actualError := actual.(error) + return apierrors.IsNotFound(actualError), nil +} + +// FailureMessage builds an error message. +func (matcher NotFoundMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to be not found") +} + +// NegatedFailureMessage builds an error message. +func (matcher NotFoundMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to be not found") +} diff --git a/examples/README.md b/examples/README.md index a6e9eb0..35be2d1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,10 @@ updated whenever the ConfigMaps that are referenced by `ref-objects` are updated ## Prerequisites - Install [KubeVela](https://kubevela.net/docs/install) in your cluster +- Enable the `kube-trigger` addon +```shell +vela addon enable kube-trigger +``` ## What we want to achieve? @@ -26,7 +30,7 @@ Apply `sample.yaml` to create 2 Applications and 2 ConfigMaps in the default nam trigger 2 Application updates. ```shell -kubectl apply sample.yaml +kubectl apply -f examples/sample.yaml ``` 2. **Run kube-trigger** @@ -43,7 +47,9 @@ Standalone: In-Cluster: ```shell -kubectl apply -f config/ +kubectl apply -f config/crd/ +kubectl apply -f config/definition/ +kubectl apply -f config/manager/ ``` 3. **Watch ApplicationRevision changes** so that you can see what it does. diff --git a/go.mod b/go.mod index 867e2e0..35a04f5 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/google/go-cmp v0.5.9 github.com/kubevela/pkg v1.8.1-0.20230411071527-ac5fa22727f7 github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/onsi/ginkgo/v2 v2.9.2 + github.com/onsi/gomega v1.27.5 github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/sirupsen/logrus v1.9.0 @@ -21,6 +23,7 @@ require ( k8s.io/klog/v2 v2.80.1 k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 sigs.k8s.io/controller-runtime v0.14.5 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -46,12 +49,14 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/cel-go v0.12.6 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect @@ -97,6 +102,7 @@ require ( golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect + golang.org/x/tools v0.7.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 // indirect @@ -118,7 +124,6 @@ require ( sigs.k8s.io/apiserver-runtime v1.1.2-0.20221118041430-0a6394f6dda3 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index f49d6b8..53993c7 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,9 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -109,6 +112,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -159,6 +163,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -173,6 +178,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= @@ -233,9 +239,10 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oam-dev/cluster-gateway v1.9.0-alpha.1 h1:1V9nm8XO4ZqJK6rkJ7c1kglQN8biQtrFiHaJnAtjjLE= github.com/oam-dev/cluster-gateway v1.9.0-alpha.1/go.mod h1:g8ivuGBu1MwtvM0AKGc81TvsLvEsosoUXENhbBFJgtc= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.5 h1:T/X6I0RNFw/kTqgfkZPcQ5KU6vCnWNBGdtrIx2dpGeQ= +github.com/onsi/gomega v1.27.5/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/openshift/library-go v0.0.0-20230327085348-8477ec72b725 h1:GC0oekPo2BDqK+2Mv6W/VuvkaUUMFcmqp0AZDN2vWrA= github.com/openshift/library-go v0.0.0-20230327085348-8477ec72b725/go.mod h1:OspkL5FZZapzNcka6UkNMFD7ifLT/dWUNvtwErpRK9k= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -417,6 +424,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -467,6 +475,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=