diff --git a/Gopkg.lock b/Gopkg.lock index 26cea18a5a8..12231f8c69e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1270,6 +1270,7 @@ "golang.org/x/oauth2/google", "google.golang.org/api/option", "gopkg.in/yaml.v2", + "k8s.io/api/apps/v1", "k8s.io/api/core/v1", "k8s.io/api/rbac/v1", "k8s.io/apimachinery/pkg/api/equality", diff --git a/cmd/broker/filter/kodata/HEAD b/cmd/broker/filter/kodata/HEAD new file mode 120000 index 00000000000..481bd4eff49 --- /dev/null +++ b/cmd/broker/filter/kodata/HEAD @@ -0,0 +1 @@ +../../../../.git/HEAD \ No newline at end of file diff --git a/cmd/broker/filter/kodata/LICENSE b/cmd/broker/filter/kodata/LICENSE new file mode 120000 index 00000000000..14776154326 --- /dev/null +++ b/cmd/broker/filter/kodata/LICENSE @@ -0,0 +1 @@ +../../../../LICENSE \ No newline at end of file diff --git a/cmd/broker/filter/kodata/VENDOR-LICENSE b/cmd/broker/filter/kodata/VENDOR-LICENSE new file mode 120000 index 00000000000..7322c09d957 --- /dev/null +++ b/cmd/broker/filter/kodata/VENDOR-LICENSE @@ -0,0 +1 @@ +../../../../third_party/VENDOR-LICENSE \ No newline at end of file diff --git a/cmd/broker/filter/main.go b/cmd/broker/filter/main.go new file mode 100644 index 00000000000..f60b33340e0 --- /dev/null +++ b/cmd/broker/filter/main.go @@ -0,0 +1,85 @@ +/* + * Copyright 2019 The Knative 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 main + +import ( + "flag" + "log" + "os" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/broker" + "github.com/knative/eventing/pkg/provisioners" + "github.com/knative/pkg/signals" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +const ( + NAMESPACE = "NAMESPACE" +) + +func main() { + logConfig := provisioners.NewLoggingConfig() + logConfig.LoggingLevel["provisioner"] = zapcore.DebugLevel + logger := provisioners.NewProvisionerLoggerFromConfig(logConfig).Desugar() + defer logger.Sync() + + flag.Parse() + + logger.Info("Starting...") + + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{ + Namespace: getRequiredEnv(NAMESPACE), + }) + if err != nil { + logger.Fatal("Error starting up.", zap.Error(err)) + } + + if err = eventingv1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + logger.Fatal("Unable to add eventingv1alpha1 scheme", zap.Error(err)) + } + + // We are running both the receiver (takes messages in from the Broker) and the dispatcher (send + // the messages to the triggers' subscribers) in this binary. + _, runnable := broker.New(logger, mgr.GetClient()) + err = mgr.Add(runnable) + if err != nil { + logger.Fatal("Unable to start the receivers runnable", zap.Error(err), zap.Any("runnable", runnable)) + } + + // Set up signals so we handle the first shutdown signal gracefully. + stopCh := signals.SetupSignalHandler() + + // Start blocks forever. + logger.Info("Manager starting...") + err = mgr.Start(stopCh) + if err != nil { + logger.Fatal("Manager.Start() returned an error", zap.Error(err)) + } + logger.Info("Exiting...") +} + +func getRequiredEnv(envKey string) string { + val, defined := os.LookupEnv(envKey) + if !defined { + log.Fatalf("required environment variable not defined '%s'", envKey) + } + return val +} diff --git a/cmd/broker/ingress/kodata/HEAD b/cmd/broker/ingress/kodata/HEAD new file mode 120000 index 00000000000..481bd4eff49 --- /dev/null +++ b/cmd/broker/ingress/kodata/HEAD @@ -0,0 +1 @@ +../../../../.git/HEAD \ No newline at end of file diff --git a/cmd/broker/ingress/kodata/LICENSE b/cmd/broker/ingress/kodata/LICENSE new file mode 120000 index 00000000000..14776154326 --- /dev/null +++ b/cmd/broker/ingress/kodata/LICENSE @@ -0,0 +1 @@ +../../../../LICENSE \ No newline at end of file diff --git a/cmd/broker/ingress/kodata/VENDOR-LICENSE b/cmd/broker/ingress/kodata/VENDOR-LICENSE new file mode 120000 index 00000000000..7322c09d957 --- /dev/null +++ b/cmd/broker/ingress/kodata/VENDOR-LICENSE @@ -0,0 +1 @@ +../../../../third_party/VENDOR-LICENSE \ No newline at end of file diff --git a/cmd/broker/ingress/main.go b/cmd/broker/ingress/main.go new file mode 100644 index 00000000000..6027e82d5d9 --- /dev/null +++ b/cmd/broker/ingress/main.go @@ -0,0 +1,158 @@ +/* + * Copyright 2019 The Knative 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 main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/provisioners" + "github.com/knative/pkg/signals" + "go.uber.org/zap" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +var ( + port = 8080 + + readTimeout = 1 * time.Minute + writeTimeout = 1 * time.Minute +) + +func main() { + logConfig := provisioners.NewLoggingConfig() + logger := provisioners.NewProvisionerLoggerFromConfig(logConfig).Desugar() + defer logger.Sync() + flag.Parse() + + logger.Info("Starting...") + + mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{}) + if err != nil { + logger.Fatal("Error starting up.", zap.Error(err)) + } + + if err = eventingv1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + logger.Fatal("Unable to add eventingv1alpha1 scheme", zap.Error(err)) + } + + c := getRequiredEnv("CHANNEL") + + h := NewHandler(logger, c) + + s := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: h, + ErrorLog: zap.NewStdLog(logger), + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + } + + err = mgr.Add(&runnableServer{ + logger: logger, + s: s, + }) + if err != nil { + logger.Fatal("Unable to add runnableServer", zap.Error(err)) + } + + // Set up signals so we handle the first shutdown signal gracefully. + stopCh := signals.SetupSignalHandler() + // Start blocks forever. + if err = mgr.Start(stopCh); err != nil { + logger.Error("manager.Start() returned an error", zap.Error(err)) + } + logger.Info("Exiting...") + + ctx, cancel := context.WithTimeout(context.Background(), writeTimeout) + defer cancel() + if err = s.Shutdown(ctx); err != nil { + logger.Error("Shutdown returned an error", zap.Error(err)) + } +} + +func getRequiredEnv(envKey string) string { + val, defined := os.LookupEnv(envKey) + if !defined { + log.Fatalf("required environment variable not defined '%s'", envKey) + } + return val +} + +// http.Handler that takes a single request in and sends it out to a single destination. +type Handler struct { + receiver *provisioners.MessageReceiver + dispatcher *provisioners.MessageDispatcher + destination string + + logger *zap.Logger +} + +// NewHandler creates a new ingress.Handler. +func NewHandler(logger *zap.Logger, destination string) *Handler { + handler := &Handler{ + logger: logger, + dispatcher: provisioners.NewMessageDispatcher(logger.Sugar()), + destination: fmt.Sprintf("http://%s", destination), + } + // The receiver function needs to point back at the handler itself, so set it up after + // initialization. + handler.receiver = provisioners.NewMessageReceiver(createReceiverFunction(handler), logger.Sugar()) + + return handler +} + +func createReceiverFunction(f *Handler) func(provisioners.ChannelReference, *provisioners.Message) error { + return func(_ provisioners.ChannelReference, m *provisioners.Message) error { + // TODO Filter. + return f.dispatch(m) + } +} + +// http.Handler interface. +func (f *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.receiver.HandleRequest(w, r) +} + +// dispatch takes the request, and sends it out the f.destination. If the dispatched +// request returns successfully, then return nil. Else, return an error. +func (f *Handler) dispatch(msg *provisioners.Message) error { + err := f.dispatcher.DispatchMessage(msg, f.destination, "", provisioners.DispatchDefaults{}) + if err != nil { + f.logger.Error("Error dispatching message", zap.String("destination", f.destination)) + } + return err +} + +// runnableServer is a small wrapper around http.Server so that it matches the manager.Runnable +// interface. +type runnableServer struct { + logger *zap.Logger + s *http.Server +} + +func (r *runnableServer) Start(<-chan struct{}) error { + r.logger.Info("Ingress Listening...", zap.String("Address", r.s.Addr)) + return r.s.ListenAndServe() +} diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 888686f109d..51339cc291a 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -21,10 +21,14 @@ import ( "flag" "log" "net/http" + "os" "time" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker" "github.com/knative/eventing/pkg/reconciler/v1alpha1/channel" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/namespace" "github.com/knative/eventing/pkg/reconciler/v1alpha1/subscription" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/trigger" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -111,7 +115,7 @@ func main() { eventingv1alpha1.AddToScheme, } for _, schemeFunc := range schemeFuncs { - if err := schemeFunc(mgr.GetScheme()); err != nil { + if err = schemeFunc(mgr.GetScheme()); err != nil { logger.Fatalf("Error adding type to manager's scheme: %v", err) } } @@ -121,17 +125,26 @@ func main() { providers := []ProvideFunc{ subscription.ProvideController, channel.ProvideController, + broker.ProvideController( + broker.ReconcilerArgs{ + IngressImage: getRequiredEnv("BROKER_INGRESS_IMAGE"), + IngressServiceAccountName: getRequiredEnv("BROKER_INGRESS_SERVICE_ACCOUNT"), + FilterImage: getRequiredEnv("BROKER_FILTER_IMAGE"), + FilterServiceAccountName: getRequiredEnv("BROKER_FILTER_SERVICE_ACCOUNT"), + }), + trigger.ProvideController, + namespace.ProvideController, } for _, provider := range providers { - if _, err := provider(mgr, logger.Desugar()); err != nil { + if _, err = provider(mgr, logger.Desugar()); err != nil { logger.Fatalf("Error adding controller to manager: %v", err) } } // Start the Manager go func() { - if err := mgr.Start(stopCh); err != nil { - logger.Fatalf("Error starting manager: %v", err) + if localErr := mgr.Start(stopCh); localErr != nil { + logger.Fatalf("Error starting manager: %v", localErr) } }() @@ -140,8 +153,8 @@ func main() { http.Handle(metricsScrapePath, promhttp.Handler()) go func() { logger.Infof("Starting metrics listener at %s", metricsScrapeAddr) - if err := srv.ListenAndServe(); err != nil { - logger.Infof("Httpserver: ListenAndServe() finished with error: %s", err) + if localErr := srv.ListenAndServe(); localErr != nil { + logger.Infof("Httpserver: ListenAndServe() finished with error: %s", localErr) } }() @@ -190,3 +203,11 @@ func getLoggingConfigOrDie() map[string]string { return cm } } + +func getRequiredEnv(envKey string) string { + val, defined := os.LookupEnv(envKey) + if !defined { + log.Fatalf("required environment variable not defined '%s'", envKey) + } + return val +} diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index e6f2ef0d55e..1524e87c029 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -53,19 +53,19 @@ func main() { defer logger.Sync() logger = logger.With(zap.String(logkey.ControllerType, logconfig.Webhook)) - logger.Info("Starting the Eventing Webhook") + logger.Infow("Starting the Eventing Webhook") // set up signals so we handle the first shutdown signal gracefully stopCh := signals.SetupSignalHandler() clusterConfig, err := rest.InClusterConfig() if err != nil { - logger.Fatal("Failed to get in cluster config", zap.Error(err)) + logger.Fatalw("Failed to get in cluster config", zap.Error(err)) } kubeClient, err := kubernetes.NewForConfig(clusterConfig) if err != nil { - logger.Fatal("Failed to get the client set", zap.Error(err)) + logger.Fatalw("Failed to get the client set", zap.Error(err)) } // Watch the logging config map and dynamically update logging levels. @@ -96,14 +96,19 @@ func main() { Options: options, Handlers: map[schema.GroupVersionKind]webhook.GenericCRD{ // For group eventing.knative.dev, + eventingv1alpha1.SchemeGroupVersion.WithKind("Broker"): &eventingv1alpha1.Broker{}, eventingv1alpha1.SchemeGroupVersion.WithKind("Channel"): &eventingv1alpha1.Channel{}, eventingv1alpha1.SchemeGroupVersion.WithKind("ClusterChannelProvisioner"): &eventingv1alpha1.ClusterChannelProvisioner{}, eventingv1alpha1.SchemeGroupVersion.WithKind("Subscription"): &eventingv1alpha1.Subscription{}, + eventingv1alpha1.SchemeGroupVersion.WithKind("Trigger"): &eventingv1alpha1.Trigger{}, }, Logger: logger, } if err != nil { - logger.Fatal("Failed to create the admission controller", zap.Error(err)) + logger.Fatalw("Failed to create the admission controller", zap.Error(err)) } - controller.Run(stopCh) + if err = controller.Run(stopCh); err != nil { + logger.Errorw("controller.Run() failed", zap.Error(err)) + } + logger.Infow("Webhook stopping") } diff --git a/config/200-broker-clusterrole.yaml b/config/200-broker-clusterrole.yaml new file mode 100644 index 00000000000..fc430c0dc8f --- /dev/null +++ b/config/200-broker-clusterrole.yaml @@ -0,0 +1,28 @@ +# Copyright 2019 The Knative 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. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: eventing-broker-filter +rules: + - apiGroups: + - eventing.knative.dev + resources: + - triggers + - triggers/status + verbs: + - get + - list + - watch diff --git a/config/300-broker.yaml b/config/300-broker.yaml new file mode 100644 index 00000000000..b47e44068f0 --- /dev/null +++ b/config/300-broker.yaml @@ -0,0 +1,42 @@ +# Copyright 2019 The Knative 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. + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: brokers.eventing.knative.dev +spec: + group: eventing.knative.dev + version: v1alpha1 + names: + kind: Broker + plural: brokers + singular: broker + categories: + - all + - knative + - eventing + scope: Namespaced + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].status" + - name: Reason + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason" + - name: Hostname + type: string + JSONPath: .status.address.hostname diff --git a/config/300-trigger.yaml b/config/300-trigger.yaml new file mode 100644 index 00000000000..65a15407433 --- /dev/null +++ b/config/300-trigger.yaml @@ -0,0 +1,45 @@ +# Copyright 2019 The Knative 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. + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: triggers.eventing.knative.dev +spec: + group: eventing.knative.dev + version: v1alpha1 + names: + kind: Trigger + plural: triggers + singular: trigger + categories: + - all + - knative + - eventing + scope: Namespaced + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].status" + - name: Reason + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason" + - name: Broker + type: string + JSONPath: .spec.broker + - name: Subscriber_URI + type: string + JSONPath: .status.subscriberURI diff --git a/config/500-controller.yaml b/config/500-controller.yaml index 00068085069..3cc82cb672c 100644 --- a/config/500-controller.yaml +++ b/config/500-controller.yaml @@ -34,19 +34,27 @@ spec: "-logtostderr", "-stderrthreshold", "INFO" ] + env: + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: BROKER_INGRESS_IMAGE + value: github.com/knative/eventing/cmd/broker/ingress + - name: BROKER_INGRESS_SERVICE_ACCOUNT + value: default + - name: BROKER_FILTER_IMAGE + value: github.com/knative/eventing/cmd/broker/filter + - name: BROKER_FILTER_SERVICE_ACCOUNT + value: eventing-broker-filter ports: - containerPort: 9090 name: metrics volumeMounts: - name: config-logging mountPath: /etc/config-logging - env: - - name: SYSTEM_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: CONFIG_LOGGING_NAME - value: config-logging volumes: - name: config-logging configMap: diff --git a/docs/broker/README.md b/docs/broker/README.md new file mode 100644 index 00000000000..0ba25b67ec6 --- /dev/null +++ b/docs/broker/README.md @@ -0,0 +1,297 @@ +# Broker and Trigger CRDs + +The Broker and Trigger CRDs, both in `eventing.knative.dev/v1alpha1`, are +interdependent. + +## Broker + +Broker represents an 'event mesh'. Events are sent to the Broker's ingress and +are then sent to any subscribers that are interested in that event. Once inside +a Broker, all metadata other than the CloudEvent is stripped away (e.g. unless +set as a CloudEvent attribute, there is no concept of how this event entered the +Broker). + +Example: + +```yaml +apiVersion: eventing.knative.dev/v1alpha1 +kind: Broker +metadata: + name: default +spec: + channelTemplate: + provisioner: + apiVersion: eventing.knative.dev/v1alpha1 + kind: ClusterChannelProvisioner + name: gcp-pubsub +``` + +## Trigger + +Trigger represents a desire to subscribe to events from a specific Broker. Basic +filtering on the types of events is provided. + +Example: + +```yaml +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: my-service-trigger +spec: + filter: + sourceAndType: + type: dev.knative.foo.bar + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: my-service +``` + +## Usage + +### Broker + +There are two ways to create a Broker, via [namespace annotation](#annotation) or [manual setup](#manual-setup). + +Normally the [namespace annotation](#annotation) is used to do this setup. + +#### Annotation + +The easiest way to get started, is to annotate your namespace (replace `default` +with the desired namespace): + +```shell +kubectl label namespace default knative-eventing-injection=enabled +``` + +This should automatically create the `default` `Broker` in that namespace. + +```shell +kubectl -n default get broker default +``` + +#### Manual Setup + +In order to setup a `Broker` manually, we must first create the required +`ServiceAccount` and give it the proper RBAC permissions. This setup is required +once per namespace. These instructions will use the `default` namespace, but you +can replace it with any namespace you want to install a `Broker` into. + +Create the `ServiceAccount`. + +```shell +kubectl -n default create serviceaccount eventing-broker-filter +``` + +Then give it the needed RBAC permissions: + +```shell +kubectl -n default create rolebinding eventing-broker-filter \ + --clusterrole=eventing-broker-filter \ + --user=eventing-broker-filter +``` + +Note that the previous commands uses three different objects, all named +`eventing-broker-filter`. The `ClusterRole` is installed with Knative Eventing +[here](../../config/200-broker-clusterrole.yaml). The `ServiceAccount` was +created two commands prior. The `RoleBinding` is created with this command. + +Now we can create the `Broker`. Note that this example uses the name `default`, +but could be replaced by any other valid name. + +```shell +cat << EOF | kubectl apply -f - +apiVersion: eventing.knative.dev/v1alpha1 +kind: Broker +metadata: + namespace: default + name: default +EOF +``` + +### Subscriber + +Now create some function that wants to receive those events. This document will +assume the following, but it could be anything that is `Addressable`. + +```yaml +apiVersion: serving.knative.dev/v1alpha1 +kind: Service +metadata: + name: my-service + namespace: default +spec: + runLatest: + configuration: + revisionTemplate: + spec: + container: + # This corresponds to + # https://github.com/knative/eventing-sources/blob/v0.2.1/cmd/message_dumper/dumper.go. + image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/message_dumper@sha256:ab5391755f11a5821e7263686564b3c3cd5348522f5b31509963afb269ddcd63 +``` + +### Trigger + +Create a `Trigger` that sends only events of a particular type to `my-service`: + +```yaml +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: my-service-trigger + namespace: default +spec: + filter: + sourceAndType: + type: dev.knative.foo.bar + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: my-service +``` + +#### Defaulting + +The Webhook will default certain unspecified fields. For example if +`spec.broker` is unspecified, it will default to `default`. If +`spec.filter.sourceAndType.type` or `spec.filter.sourceAndType.Source` are +unspecified, then they will default to the special value `Any`, which matches +everything. + +The Webhook will default the YAML above to: + +```yaml +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: my-service-trigger + namespace: default +spec: + broker: default # Defaulted by the Webhook. + filter: + sourceAndType: + type: dev.knative.foo.bar + source: Any # Defaulted by the Webhook. + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: my-service +``` + +You can make multiple `Trigger`s on the same `Broker` corresponding to different +types, sources, and subscribers. + +### Source + +Now have something emit an event of the correct type (`dev.knative.foo.bar`) +into the `Broker`. We can either do this manually or with a normal Knative +Source. + +#### Manual + +The `Broker`'s address is well known, it will always be +`-broker..svc.`. In our case, it is +`default-broker.default.svc.cluster.local`. + +While SSHed into a `Pod` with the Istio sidecar, run: + +```shell +curl -v "http://default-broker.default.svc.cluster.local/" \ + -X POST \ + -H "X-B3-Flags: 1" \ + -H "CE-CloudEventsVersion: 0.1" \ + -H "CE-EventType: dev.knative.foo.bar" \ + -H "CE-EventTime: 2018-04-05T03:56:24Z" \ + -H "CE-EventID: 45a8b444-3213-4758-be3f-540bf93f85ff" \ + -H "CE-Source: dev.knative.example" \ + -H 'Content-Type: application/json' \ + -d '{ "much": "wow" }' +``` + +#### Knative Source + +Provide the Knative Source the `default` `Broker` as its sink: + +```yaml +apiVersion: sources.eventing.knative.dev/v1alpha1 +kind: ContainerSource +metadata: + name: heartbeats-sender +spec: + image: github.com/knative/eventing-sources/cmd/heartbeats/ + sink: + apiVersion: eventing.knative.dev/v1alpha1 + kind: Broker + name: default +``` + +## Implementation + +Broker and Trigger are intended to be black boxes. How they are implemented +should not matter to the end user. This section describes the specific +implementation that is currently in the repository. However, **the implmentation +may change at any time, absolutely no guarantees are made about the +implmentation**. + +### Namespace + +Namespaces are reconciled by the +[Namespace Reconciler](../../pkg/reconciler/v1alpha1/namespace). The `Namespace +Reconciler` looks for all `namespace`s that have the label +`knative-eventing-injection: enabled`. If that label is present, then the +`Namespace Reconciler` reconciles: + +1. Creates the Broker Filter's `ServiceAccount`, `eventing-broker-filter`. +1. Ensures that `ServiceAccount` has the requisite RBAC permissions by giving + it the [`eventing-broker-filter`](../../config/200-broker-clusterrole.yaml) + `Role`. +1. Creates a `Broker` named `default`. + +### Broker + +`Broker`s are reconciled by the +[Broker Reconciler](../../pkg/reconciler/v1alpha1/broker). For each `Broker`, it +reconciles: + +1. The 'everything' `Channel`. This is a `Channel` that all events in the + `Broker` are sent to. Anything that passes the `Broker`'s Ingress is sent to + this `Channel`. All `Trigger`s subscribe to this `Channel`. +1. The 'filter' `Deployment`. The `Deployment` runs + [cmd/broker/filter](../../cmd/broker/filter). Its purpose is the data plane + for all `Trigger`s related to this `Broker`. + - This piece is very similar to the existing Channel dispatchers, in that + all `Trigger`s for a given `Broker` route to this single `Deployment`. + The code inspects the Host header to determine which `Trigger` the + request is related to and then carries it out. + - Internally this binary uses the [pkg/broker](../../pkg/broker) library. +1. The 'filter' Kubernetes `Service`. This `Service` points to the 'filter' + `Deployment`. +1. The 'ingress' `Deployment`. The `Deployment` runs + [cmd/broker/ingress](../../cmd/broker/ingress). Its purpose is to inspect + all events that are entering the `Broker`. +1. The 'ingress' Kubernetes `Service`. This `Service` points to the 'ingress' + `Deployment`. This `Service`'s address is the address given for the + `Broker`. + +### Trigger + +`Trigger`s are reconciled by the +[Trigger Reconciler](../../pkg/reconciler/v1alpha1/trigger). For each `Trigger`, +it reconciles: + +1. Determines the subscriber's URI. + - Currently uses the same logic as the `Subscription` Reconciler, so + supports Addressables and Kubernetes `Service`s. +1. Creates a Kubernetes `Service` and Istio `VirtualService` pair. This allows + all Istio enabled `Pod`s to send to the `Trigger`'s address. + - This is the same as the current `Channel` implementation. The `Service` + points nowhere. The `VirtualService` reroutes requests that originally + went to the `Service`, to instead go to the `Broker`'s 'filter' + `Service`. +1. Creates `Subscription` from the `Broker`'s 'everything' `Channel` to the + `Trigger`'s Kubernetes `Service`. diff --git a/docs/broker/example_brokers.yaml b/docs/broker/example_brokers.yaml new file mode 100644 index 00000000000..2b2b87c4654 --- /dev/null +++ b/docs/broker/example_brokers.yaml @@ -0,0 +1,44 @@ +# Copyright 2019 The Knative 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. + +# This file is a list of example Brokers. Each could be used independently. + +--- + +# By not specifying a spec, the default Channel for the namespace will be used. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Broker +metadata: + name: default-channel + +--- + +# By specifying spec.channelTemplate, we guarantee the Channel implementation +# used, and thus guarantee the durability of the events that are sent to this +# Broker. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Broker +metadata: + name: pubsub-channel +spec: + channelTemplate: + provisioner: + apiVersion: eventing.knative.dev/v1alpha1 + kind: ClusterChannelProvisioner + name: gcp-pubsub + + + diff --git a/docs/broker/example_triggers.yaml b/docs/broker/example_triggers.yaml new file mode 100644 index 00000000000..5dd4bb7b169 --- /dev/null +++ b/docs/broker/example_triggers.yaml @@ -0,0 +1,129 @@ +# Copyright 2019 The Knative 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. + +# This file is a list of example Triggers. Each could be used independently. + +--- + +# By not specifying spec.broker, this Trigger will associate with the Broker +# named 'default'. In addition, by not specifying spec.filter, this Trigger +# will match all events sent through the Broker. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: knative-service-default-broker +spec: + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: message-dumper + +--- + +# K8s Service is also a valid Subscriber. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: k8s-service-default-broker +spec: + subscriber: + ref: + apiVersion: v1 + kind: Service + name: message-dumper + +--- + +# By specifying spec.broker, this Trigger will match events sent through the +# 'my-other-broker' Broker (instead of the 'default' Broker). + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: knative-service-my-other-broker +spec: + broker: my-other-broker + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: message-dumper + +--- + +# We can filter on either the event's type, source, or both. The special value +# 'Any' matches everything. If either is not specified, it defaults to 'Any'. + +--- + +# This Trigger matches all events of type 'dev.knative.foo', regardless of +# source, that are sent to the 'default' Broker. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: filtering-type +spec: + filter: + sourceAndType: + type: dev.knative.foo + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: message-dumper + +--- + +# This Trigger matches all events of source 'dev.knative.bar', regardless of +# type, that are sent to the 'default' Broker. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: filtering-source +spec: + filter: + sourceAndType: + source: dev.knative.bar + # The Webhook will default this in, but it does not hurt to specify it. + type: Any + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: message-dumper + +--- + +# This Trigger matches all events of type 'dev.knative.foo' and source +# 'dev.knative.bar', that are sent to the 'default' Broker. + +apiVersion: eventing.knative.dev/v1alpha1 +kind: Trigger +metadata: + name: filtering-type-and-source +spec: + filter: + sourceAndType: + type: dev.knative.foo + source: dev.knative.bar + subscriber: + ref: + apiVersion: serving.knative.dev/v1alpha1 + kind: Service + name: message-dumper diff --git a/pkg/apis/eventing/v1alpha1/broker_defaults.go b/pkg/apis/eventing/v1alpha1/broker_defaults.go new file mode 100644 index 00000000000..54df6bed298 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_defaults.go @@ -0,0 +1,25 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +func (b *Broker) SetDefaults() { + b.Spec.SetDefaults() +} + +func (bs *BrokerSpec) SetDefaults() { + // None +} diff --git a/pkg/apis/eventing/v1alpha1/broker_defaults_test.go b/pkg/apis/eventing/v1alpha1/broker_defaults_test.go new file mode 100644 index 00000000000..b27c6b2d7c6 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_defaults_test.go @@ -0,0 +1,25 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import "testing" + +// No-op test because method does nothing. +func TestBrokerDefaults(t *testing.T) { + b := Broker{} + b.SetDefaults() +} diff --git a/pkg/apis/eventing/v1alpha1/broker_types.go b/pkg/apis/eventing/v1alpha1/broker_types.go new file mode 100644 index 00000000000..ecdf36cf841 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_types.go @@ -0,0 +1,165 @@ +/* + * Copyright 2019 The Knative 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 v1alpha1 + +import ( + "github.com/knative/pkg/apis" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + "github.com/knative/pkg/webhook" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type Broker struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of the Broker. + Spec BrokerSpec `json:"spec,omitempty"` + + // Status represents the current state of the Broker. This data may be out of + // date. + // +optional + Status BrokerStatus `json:"status,omitempty"` +} + +// Check that Broker can be validated, can be defaulted, and has immutable fields. +var _ apis.Validatable = (*Broker)(nil) +var _ apis.Defaultable = (*Broker)(nil) +var _ apis.Immutable = (*Broker)(nil) +var _ runtime.Object = (*Broker)(nil) +var _ webhook.GenericCRD = (*Broker)(nil) + +type BrokerSpec struct { + // TODO By enabling the status subresource metadata.generation should increment + // thus making this property obsolete. + // + // We should be able to drop this property with a CRD conversion webhook + // in the future + // + // +optional + DeprecatedGeneration int64 `json:"generation,omitempty"` + + // ChannelTemplate, if specified will be used to create all the Channels used internally by the + // Broker. Only Provisioner and Arguments may be specified. If left unspecified, the default + // Channel for the namespace will be used. + // + // +optional + ChannelTemplate *ChannelSpec `json:"channelTemplate,omitempty"` +} + +var brokerCondSet = duckv1alpha1.NewLivingConditionSet(BrokerConditionIngress, BrokerConditionChannel, BrokerConditionFilter, BrokerConditionAddressable) + +// BrokerStatus represents the current state of a Broker. +type BrokerStatus struct { + // ObservedGeneration is the most recent generation observed for this Broker. + // It corresponds to the Broker's generation, which is updated on mutation by + // the API Server. + // TODO: The above comment is only true once + // https://github.com/kubernetes/kubernetes/issues/58778 is fixed. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Represents the latest available observations of a broker's current state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions duckv1alpha1.Conditions `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // Broker is Addressable. It currently exposes the endpoint as a + // fully-qualified DNS name which will distribute traffic over the + // provided targets from inside the cluster. + // + // It generally has the form {broker}-router.{namespace}.svc.{cluster domain name} + Address duckv1alpha1.Addressable `json:"address,omitempty"` +} + +const ( + BrokerConditionReady = duckv1alpha1.ConditionReady + + BrokerConditionIngress duckv1alpha1.ConditionType = "IngressReady" + + BrokerConditionChannel duckv1alpha1.ConditionType = "ChannelReady" + + BrokerConditionFilter duckv1alpha1.ConditionType = "FilterReady" + + BrokerConditionAddressable duckv1alpha1.ConditionType = "Addressable" +) + +// GetCondition returns the condition currently associated with the given type, or nil. +func (bs *BrokerStatus) GetCondition(t duckv1alpha1.ConditionType) *duckv1alpha1.Condition { + return brokerCondSet.Manage(bs).GetCondition(t) +} + +// IsReady returns true if the resource is ready overall. +func (bs *BrokerStatus) IsReady() bool { + return brokerCondSet.Manage(bs).IsHappy() +} + +// InitializeConditions sets relevant unset conditions to Unknown state. +func (bs *BrokerStatus) InitializeConditions() { + brokerCondSet.Manage(bs).InitializeConditions() +} + +func (bs *BrokerStatus) MarkIngressReady() { + brokerCondSet.Manage(bs).MarkTrue(BrokerConditionIngress) +} + +func (bs *BrokerStatus) MarkIngressFailed(err error) { + brokerCondSet.Manage(bs).MarkFalse(BrokerConditionIngress, "failed", "%v", err) +} + +func (bs *BrokerStatus) MarkChannelReady() { + brokerCondSet.Manage(bs).MarkTrue(BrokerConditionChannel) +} + +func (bs *BrokerStatus) MarkChannelFailed(err error) { + brokerCondSet.Manage(bs).MarkFalse(BrokerConditionChannel, "failed", "%v", err) +} + +func (bs *BrokerStatus) MarkFilterReady() { + brokerCondSet.Manage(bs).MarkTrue(BrokerConditionFilter) +} + +func (bs *BrokerStatus) MarkFilterFailed(err error) { + brokerCondSet.Manage(bs).MarkFalse(BrokerConditionFilter, "failed", "%v", err) +} + +// SetAddress makes this Broker addressable by setting the hostname. It also +// sets the BrokerConditionAddressable to true. +func (bs *BrokerStatus) SetAddress(hostname string) { + bs.Address.Hostname = hostname + if hostname != "" { + brokerCondSet.Manage(bs).MarkTrue(BrokerConditionAddressable) + } else { + brokerCondSet.Manage(bs).MarkFalse(BrokerConditionAddressable, "emptyHostname", "hostname is the empty string") + } +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BrokerList is a collection of Brokers. +type BrokerList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + Items []Broker `json:"items"` +} diff --git a/pkg/apis/eventing/v1alpha1/broker_types_test.go b/pkg/apis/eventing/v1alpha1/broker_types_test.go new file mode 100644 index 00000000000..3970fbcf56a --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_types_test.go @@ -0,0 +1,273 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +var ( + brokerConditionReady = duckv1alpha1.Condition{ + Type: BrokerConditionReady, + Status: corev1.ConditionTrue, + } + + brokerConditionIngress = duckv1alpha1.Condition{ + Type: BrokerConditionIngress, + Status: corev1.ConditionTrue, + } + + brokerConditionChannel = duckv1alpha1.Condition{ + Type: BrokerConditionChannel, + Status: corev1.ConditionTrue, + } + + brokerConditionFilter = duckv1alpha1.Condition{ + Type: BrokerConditionFilter, + Status: corev1.ConditionTrue, + } + + brokerConditionAddressable = duckv1alpha1.Condition{ + Type: BrokerConditionAddressable, + Status: corev1.ConditionFalse, + } +) + +func TestBrokerGetCondition(t *testing.T) { + tests := []struct { + name string + bs *BrokerStatus + condQuery duckv1alpha1.ConditionType + want *duckv1alpha1.Condition + }{{ + name: "single condition", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{ + brokerConditionReady, + }, + }, + condQuery: duckv1alpha1.ConditionReady, + want: &brokerConditionReady, + }, { + name: "multiple conditions", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{ + brokerConditionIngress, + brokerConditionChannel, + brokerConditionFilter, + }, + }, + condQuery: BrokerConditionFilter, + want: &brokerConditionFilter, + }, { + name: "multiple conditions, condition false", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{ + brokerConditionChannel, + brokerConditionFilter, + brokerConditionAddressable, + }, + }, + condQuery: BrokerConditionAddressable, + want: &brokerConditionAddressable, + }, { + name: "unknown condition", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{ + brokerConditionAddressable, + brokerConditionReady, + }, + }, + condQuery: duckv1alpha1.ConditionType("foo"), + want: nil, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.bs.GetCondition(test.condQuery) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("unexpected condition (-want, +got) = %v", diff) + } + }) + } +} + +func TestBrokerInitializeConditions(t *testing.T) { + tests := []struct { + name string + bs *BrokerStatus + want *BrokerStatus + }{{ + name: "empty", + bs: &BrokerStatus{}, + want: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: BrokerConditionAddressable, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionChannel, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionFilter, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionIngress, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, { + name: "one false", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: BrokerConditionChannel, + Status: corev1.ConditionFalse, + }}, + }, + want: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: BrokerConditionAddressable, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionChannel, + Status: corev1.ConditionFalse, + }, { + Type: BrokerConditionFilter, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionIngress, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, { + name: "one true", + bs: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: BrokerConditionFilter, + Status: corev1.ConditionTrue, + }}, + }, + want: &BrokerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: BrokerConditionAddressable, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionChannel, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionFilter, + Status: corev1.ConditionTrue, + }, { + Type: BrokerConditionIngress, + Status: corev1.ConditionUnknown, + }, { + Type: BrokerConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.bs.InitializeConditions() + if diff := cmp.Diff(test.want, test.bs, ignoreAllButTypeAndStatus); diff != "" { + t.Errorf("unexpected conditions (-want, +got) = %v", diff) + } + }) + } +} + +func TestBrokerIsReady(t *testing.T) { + tests := []struct { + name string + markChannelReady bool + markFilterReady bool + markIngressReady bool + address string + wantReady bool + }{{ + name: "all happy", + markChannelReady: true, + markFilterReady: true, + markIngressReady: true, + address: "hostname", + wantReady: true, + }, { + name: "channel sad", + markChannelReady: false, + markFilterReady: true, + markIngressReady: true, + address: "hostname", + wantReady: false, + }, { + name: "filter sad", + markChannelReady: true, + markFilterReady: false, + markIngressReady: true, + address: "hostname", + wantReady: false, + }, { + name: "ingress sad", + markChannelReady: true, + markFilterReady: true, + markIngressReady: false, + address: "hostname", + wantReady: false, + }, { + name: "addressable sad", + markChannelReady: true, + markFilterReady: true, + markIngressReady: true, + address: "", + wantReady: false, + }, { + name: "all sad", + markChannelReady: false, + markFilterReady: false, + markIngressReady: false, + address: "", + wantReady: false, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts := &BrokerStatus{} + if test.markChannelReady { + ts.MarkChannelReady() + } + if test.markFilterReady { + ts.MarkFilterReady() + } + if test.markIngressReady { + ts.MarkIngressReady() + } + ts.SetAddress(test.address) + got := ts.IsReady() + if test.wantReady != got { + t.Errorf("unexpected readiness: want %v, got %v", test.wantReady, got) + } + }) + } +} diff --git a/pkg/apis/eventing/v1alpha1/broker_validation.go b/pkg/apis/eventing/v1alpha1/broker_validation.go new file mode 100644 index 00000000000..39b495ef31a --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_validation.go @@ -0,0 +1,38 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "github.com/knative/pkg/apis" +) + +func (b *Broker) Validate() *apis.FieldError { + return b.Spec.Validate().ViaField("spec") +} + +func (bs *BrokerSpec) Validate() *apis.FieldError { + // TODO validate that the channelTemplate only specifies the provisioner and arguments. + return nil +} + +func (b *Broker) CheckImmutableFields(og apis.Immutable) *apis.FieldError { + // Currently there are no immutable fields. We could make spec.channelTemplate immutable, as + // changing it will normally not have the desired effect of changing the Channel inside the + // Broker. It would have an effect if the existing Channel was then deleted, the newly created + // Channel would use the new spec.channelTemplate. + return nil +} diff --git a/pkg/apis/eventing/v1alpha1/broker_validation_test.go b/pkg/apis/eventing/v1alpha1/broker_validation_test.go new file mode 100644 index 00000000000..b7842e869bb --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/broker_validation_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "testing" +) + +// No-op test because method does nothing. +func TestBrokerValidation(t *testing.T) { + b := Broker{} + _ = b.Validate() +} + +// No-op test because method does nothing. +func TestBrokerSpecValidation(t *testing.T) { + bs := BrokerSpec{} + _ = bs.Validate() +} + +// No-op test because method does nothing. +func TestBrokerImmutableFields(t *testing.T) { + original := &Broker{} + current := &Broker{} + _ = current.CheckImmutableFields(original) +} diff --git a/pkg/apis/eventing/v1alpha1/register.go b/pkg/apis/eventing/v1alpha1/register.go index df6338f66c1..fb3a5292623 100644 --- a/pkg/apis/eventing/v1alpha1/register.go +++ b/pkg/apis/eventing/v1alpha1/register.go @@ -45,12 +45,16 @@ var ( // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &Broker{}, + &BrokerList{}, &Channel{}, &ChannelList{}, &ClusterChannelProvisioner{}, &ClusterChannelProvisionerList{}, &Subscription{}, &SubscriptionList{}, + &Trigger{}, + &TriggerList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/eventing/v1alpha1/trigger_defaults.go b/pkg/apis/eventing/v1alpha1/trigger_defaults.go new file mode 100644 index 00000000000..2e14b5f28ca --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_defaults.go @@ -0,0 +1,43 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +func (t *Trigger) SetDefaults() { + t.Spec.SetDefaults() +} + +func (ts *TriggerSpec) SetDefaults() { + if ts.Broker == "" { + ts.Broker = "default" + } + // Make a default filter that allows anything. + if ts.Filter == nil { + ts.Filter = &TriggerFilter{} + } + + // Note that this logic will need to change once there are other filtering options, as it should + // only apply if no other filter is applied. + if ts.Filter.SourceAndType == nil { + ts.Filter.SourceAndType = &TriggerFilterSourceAndType{} + } + if ts.Filter.SourceAndType.Type == "" { + ts.Filter.SourceAndType.Type = TriggerAnyFilter + } + if ts.Filter.SourceAndType.Source == "" { + ts.Filter.SourceAndType.Source = TriggerAnyFilter + } +} diff --git a/pkg/apis/eventing/v1alpha1/trigger_defaults_test.go b/pkg/apis/eventing/v1alpha1/trigger_defaults_test.go new file mode 100644 index 00000000000..a3dab313fb7 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_defaults_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +var ( + defaultBroker = "default" + otherBroker = "other_broker" + defaultTriggerFilter = &TriggerFilter{ + SourceAndType: &TriggerFilterSourceAndType{ + Type: TriggerAnyFilter, + Source: TriggerAnyFilter}, + } + otherTriggerFilter = &TriggerFilter{ + SourceAndType: &TriggerFilterSourceAndType{ + Type: "other_type", + Source: "other_source"}, + } + defaultTrigger = Trigger{ + Spec: TriggerSpec{ + Broker: defaultBroker, + Filter: defaultTriggerFilter, + }, + } +) + +func TestTriggerDefaults(t *testing.T) { + testCases := map[string]struct { + initial Trigger + expected Trigger + }{ + "nil broker": { + initial: Trigger{Spec: TriggerSpec{Filter: otherTriggerFilter}}, + expected: Trigger{Spec: TriggerSpec{Broker: defaultBroker, Filter: otherTriggerFilter}}, + }, + "nil filter": { + initial: Trigger{Spec: TriggerSpec{Broker: otherBroker}}, + expected: Trigger{Spec: TriggerSpec{Broker: otherBroker, Filter: defaultTriggerFilter}}, + }, + "nil broker and nil filter": { + initial: Trigger{}, + expected: defaultTrigger, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + tc.initial.SetDefaults() + if diff := cmp.Diff(tc.expected, tc.initial); diff != "" { + t.Fatalf("Unexpected defaults (-want, +got): %s", diff) + } + }) + } +} diff --git a/pkg/apis/eventing/v1alpha1/trigger_types.go b/pkg/apis/eventing/v1alpha1/trigger_types.go new file mode 100644 index 00000000000..b3742caf954 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_types.go @@ -0,0 +1,171 @@ +/* + * Copyright 2019 The Knative 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 v1alpha1 + +import ( + "github.com/knative/pkg/apis" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + "github.com/knative/pkg/webhook" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type Trigger struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of the Trigger. + Spec TriggerSpec `json:"spec,omitempty"` + + // Status represents the current state of the Trigger. This data may be out of + // date. + // +optional + Status TriggerStatus `json:"status,omitempty"` +} + +// Check that Trigger can be validated, can be defaulted, and has immutable fields. +var _ apis.Validatable = (*Trigger)(nil) +var _ apis.Defaultable = (*Trigger)(nil) +var _ apis.Immutable = (*Trigger)(nil) +var _ runtime.Object = (*Trigger)(nil) +var _ webhook.GenericCRD = (*Trigger)(nil) + +type TriggerSpec struct { + // TODO By enabling the status subresource metadata.generation should increment + // thus making this property obsolete. + // + // We should be able to drop this property with a CRD conversion webhook + // in the future + // + // +optional + DeprecatedGeneration int64 `json:"generation,omitempty"` + + // Broker is the broker that this trigger receives events from. If not specified, will default + // to 'default'. + Broker string `json:"broker,omitempty"` + + // Filter is the filter to apply against all events from the Broker. Only events that pass this + // filter will be sent to the Subscriber. If not specified, will default to allowing all events. + // + // +optional + Filter *TriggerFilter `json:"filter,omitempty"` + + // Subscriber is the addressable that receives events from the Broker that pass the Filter. It + // is required. + Subscriber *SubscriberSpec `json:"subscriber,omitempty"` +} + +type TriggerFilter struct { + SourceAndType *TriggerFilterSourceAndType `json:"sourceAndType,omitempty"` +} + +// TriggerFilterSourceAndType filters events based on exact matches on the cloud event's type and +// source attributes. Only exact matches will pass the filter. Either or both type and source can +// use the value 'Any' to indicate all strings match. +type TriggerFilterSourceAndType struct { + Type string `json:"type,omitempty"` + Source string `json:"source,omitempty"` +} + +var triggerCondSet = duckv1alpha1.NewLivingConditionSet(TriggerConditionBrokerExists, TriggerConditionKubernetesService, TriggerConditionVirtualService, TriggerConditionSubscribed) + +// TriggerStatus represents the current state of a Trigger. +type TriggerStatus struct { + // ObservedGeneration is the most recent generation observed for this Trigger. + // It corresponds to the Trigger's generation, which is updated on mutation by + // the API Server. + // TODO: The above comment is only true once + // https://github.com/kubernetes/kubernetes/issues/58778 is fixed. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Represents the latest available observations of a trigger's current state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions duckv1alpha1.Conditions `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + SubscriberURI string `json:"subscriberURI,omitempty"` +} + +const ( + TriggerConditionReady = duckv1alpha1.ConditionReady + + TriggerConditionBrokerExists duckv1alpha1.ConditionType = "BrokerExists" + + TriggerConditionKubernetesService duckv1alpha1.ConditionType = "KubernetesServiceReady" + + TriggerConditionVirtualService duckv1alpha1.ConditionType = "VirtualServiceReady" + + TriggerConditionSubscribed duckv1alpha1.ConditionType = "Subscribed" + + // Constant to represent that we should allow anything. + TriggerAnyFilter = "Any" +) + +// GetCondition returns the condition currently associated with the given type, or nil. +func (ts *TriggerStatus) GetCondition(t duckv1alpha1.ConditionType) *duckv1alpha1.Condition { + return triggerCondSet.Manage(ts).GetCondition(t) +} + +// IsReady returns true if the resource is ready overall. +func (ts *TriggerStatus) IsReady() bool { + return triggerCondSet.Manage(ts).IsHappy() +} + +// InitializeConditions sets relevant unset conditions to Unknown state. +func (ts *TriggerStatus) InitializeConditions() { + triggerCondSet.Manage(ts).InitializeConditions() +} + +func (ts *TriggerStatus) MarkBrokerExists() { + triggerCondSet.Manage(ts).MarkTrue(TriggerConditionBrokerExists) +} + +func (ts *TriggerStatus) MarkBrokerDoesNotExist() { + triggerCondSet.Manage(ts).MarkFalse(TriggerConditionBrokerExists, "doesNotExist", "Broker does not exist") +} + +func (ts *TriggerStatus) MarkKubernetesServiceExists() { + triggerCondSet.Manage(ts).MarkTrue(TriggerConditionKubernetesService) +} + +func (ts *TriggerStatus) MarkVirtualServiceExists() { + triggerCondSet.Manage(ts).MarkTrue(TriggerConditionVirtualService) +} + +func (ts *TriggerStatus) MarkSubscribed() { + triggerCondSet.Manage(ts).MarkTrue(TriggerConditionSubscribed) +} + +func (ts *TriggerStatus) MarkNotSubscribed(reason, messageFormat string, messageA ...interface{}) { + triggerCondSet.Manage(ts).MarkFalse(TriggerConditionSubscribed, reason, messageFormat, messageA...) +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TriggerList is a collection of Triggers. +type TriggerList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + Items []Trigger `json:"items"` +} diff --git a/pkg/apis/eventing/v1alpha1/trigger_types_test.go b/pkg/apis/eventing/v1alpha1/trigger_types_test.go new file mode 100644 index 00000000000..3c3c37525d5 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_types_test.go @@ -0,0 +1,274 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" +) + +var ( + triggerConditionReady = duckv1alpha1.Condition{ + Type: TriggerConditionReady, + Status: corev1.ConditionTrue, + } + + triggerConditionBrokerExists = duckv1alpha1.Condition{ + Type: TriggerConditionBrokerExists, + Status: corev1.ConditionTrue, + } + + triggerConditionKubernetesService = duckv1alpha1.Condition{ + Type: TriggerConditionKubernetesService, + Status: corev1.ConditionTrue, + } + + triggerConditionVirtualService = duckv1alpha1.Condition{ + Type: TriggerConditionVirtualService, + Status: corev1.ConditionTrue, + } + + triggerConditionSubscribed = duckv1alpha1.Condition{ + Type: TriggerConditionSubscribed, + Status: corev1.ConditionFalse, + } +) + +func TestTriggerGetCondition(t *testing.T) { + tests := []struct { + name string + ts *TriggerStatus + condQuery duckv1alpha1.ConditionType + want *duckv1alpha1.Condition + }{{ + name: "single condition", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{ + triggerConditionReady, + }, + }, + condQuery: duckv1alpha1.ConditionReady, + want: &triggerConditionReady, + }, { + name: "multiple conditions", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{ + triggerConditionBrokerExists, + triggerConditionKubernetesService, + }, + }, + condQuery: TriggerConditionKubernetesService, + want: &triggerConditionKubernetesService, + }, { + name: "multiple conditions, condition false", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{ + triggerConditionBrokerExists, + triggerConditionKubernetesService, + triggerConditionSubscribed, + }, + }, + condQuery: TriggerConditionSubscribed, + want: &triggerConditionSubscribed, + }, { + name: "unknown condition", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{ + triggerConditionVirtualService, + triggerConditionSubscribed, + }, + }, + condQuery: duckv1alpha1.ConditionType("foo"), + want: nil, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.ts.GetCondition(test.condQuery) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("unexpected condition (-want, +got) = %v", diff) + } + }) + } +} + +func TestTriggerInitializeConditions(t *testing.T) { + tests := []struct { + name string + ts *TriggerStatus + want *TriggerStatus + }{{ + name: "empty", + ts: &TriggerStatus{}, + want: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: TriggerConditionBrokerExists, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionKubernetesService, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionReady, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionSubscribed, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionVirtualService, + Status: corev1.ConditionUnknown, + }}, + }, + }, { + name: "one false", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: TriggerConditionVirtualService, + Status: corev1.ConditionFalse, + }}, + }, + want: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: TriggerConditionBrokerExists, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionKubernetesService, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionReady, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionSubscribed, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionVirtualService, + Status: corev1.ConditionFalse, + }}, + }, + }, { + name: "one true", + ts: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: TriggerConditionSubscribed, + Status: corev1.ConditionTrue, + }}, + }, + want: &TriggerStatus{ + Conditions: []duckv1alpha1.Condition{{ + Type: TriggerConditionBrokerExists, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionKubernetesService, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionReady, + Status: corev1.ConditionUnknown, + }, { + Type: TriggerConditionSubscribed, + Status: corev1.ConditionTrue, + }, { + Type: TriggerConditionVirtualService, + Status: corev1.ConditionUnknown, + }}, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.ts.InitializeConditions() + if diff := cmp.Diff(test.want, test.ts, ignoreAllButTypeAndStatus); diff != "" { + t.Errorf("unexpected conditions (-want, +got) = %v", diff) + } + }) + } +} + +func TestTriggerIsReady(t *testing.T) { + tests := []struct { + name string + markBrokerExists bool + markKubernetesServiceExists bool + markVirtualServiceExists bool + markSubscribed bool + wantReady bool + }{{ + name: "all happy", + markBrokerExists: true, + markKubernetesServiceExists: true, + markVirtualServiceExists: true, + markSubscribed: true, + wantReady: true, + }, { + name: "broker sad", + markBrokerExists: false, + markKubernetesServiceExists: true, + markVirtualServiceExists: true, + markSubscribed: true, + wantReady: false, + }, { + name: "k8s service sad", + markBrokerExists: true, + markKubernetesServiceExists: false, + markVirtualServiceExists: true, + markSubscribed: true, + wantReady: false, + }, { + name: "virtual service sad", + markBrokerExists: true, + markKubernetesServiceExists: true, + markVirtualServiceExists: false, + markSubscribed: true, + wantReady: false, + }, { + name: "subscribed sad", + markBrokerExists: true, + markKubernetesServiceExists: true, + markVirtualServiceExists: true, + markSubscribed: false, + wantReady: false, + }, { + name: "all sad", + markBrokerExists: false, + markKubernetesServiceExists: false, + markVirtualServiceExists: false, + markSubscribed: false, + wantReady: false, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts := &TriggerStatus{} + if test.markBrokerExists { + ts.MarkBrokerExists() + } + if test.markKubernetesServiceExists { + ts.MarkKubernetesServiceExists() + } + if test.markVirtualServiceExists { + ts.MarkVirtualServiceExists() + } + if test.markSubscribed { + ts.MarkSubscribed() + } + got := ts.IsReady() + if test.wantReady != got { + t.Errorf("unexpected readiness: want %v, got %v", test.wantReady, got) + } + }) + } +} diff --git a/pkg/apis/eventing/v1alpha1/trigger_validation.go b/pkg/apis/eventing/v1alpha1/trigger_validation.go new file mode 100644 index 00000000000..ae57a5bf4e6 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_validation.go @@ -0,0 +1,73 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "github.com/google/go-cmp/cmp" + "github.com/knative/pkg/apis" +) + +func (t *Trigger) Validate() *apis.FieldError { + return t.Spec.Validate().ViaField("spec") +} + +func (ts *TriggerSpec) Validate() *apis.FieldError { + var errs *apis.FieldError + if ts.Broker == "" { + fe := apis.ErrMissingField("broker") + errs = errs.Also(fe) + } + + if ts.Filter == nil { + fe := apis.ErrMissingField("filter") + errs = errs.Also(fe) + } + + if ts.Filter != nil && ts.Filter.SourceAndType == nil { + fe := apis.ErrMissingField("filter.sourceAndType") + errs = errs.Also(fe) + } + + if isSubscriberSpecNilOrEmpty(ts.Subscriber) { + fe := apis.ErrMissingField("subscriber") + errs = errs.Also(fe) + } else if fe := isValidSubscriberSpec(*ts.Subscriber); fe != nil { + errs = errs.Also(fe.ViaField("subscriber")) + } + + return errs +} + +func (t *Trigger) CheckImmutableFields(og apis.Immutable) *apis.FieldError { + if og == nil { + return nil + } + + original, ok := og.(*Trigger) + if !ok { + return &apis.FieldError{Message: "The provided original was not a Trigger"} + } + + if diff := cmp.Diff(original.Spec.Broker, t.Spec.Broker); diff != "" { + return &apis.FieldError{ + Message: "Immutable fields changed (-old +new)", + Paths: []string{"spec", "broker"}, + Details: diff, + } + } + return nil +} diff --git a/pkg/apis/eventing/v1alpha1/trigger_validation_test.go b/pkg/apis/eventing/v1alpha1/trigger_validation_test.go new file mode 100644 index 00000000000..313f8132708 --- /dev/null +++ b/pkg/apis/eventing/v1alpha1/trigger_validation_test.go @@ -0,0 +1,227 @@ +/* +Copyright 2019 The Knative 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 v1alpha1 + +import ( + "testing" + + "github.com/knative/pkg/apis" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" +) + +var ( + validTriggerFilter = &TriggerFilter{ + SourceAndType: &TriggerFilterSourceAndType{ + Type: "other_type", + Source: "other_source"}, + } + validSubscriber = &SubscriberSpec{ + Ref: &corev1.ObjectReference{ + Name: "subscriber_test", + Kind: "Service", + APIVersion: "serving.knative.dev/v1alpha1", + }, + } + invalidSubscriber = &SubscriberSpec{ + Ref: &corev1.ObjectReference{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1alpha1", + }, + } +) + +func TestTriggerValidation(t *testing.T) { + name := "invalid trigger spec" + trigger := &Trigger{Spec: TriggerSpec{}} + + want := &apis.FieldError{ + Paths: []string{"spec.broker", "spec.filter", "spec.subscriber"}, + Message: "missing field(s)", + } + + t.Run(name, func(t *testing.T) { + got := trigger.Validate() + if diff := cmp.Diff(want.Error(), got.Error()); diff != "" { + t.Errorf("Trigger.Validate (-want, +got) = %v", diff) + } + }) +} + +func TestTriggerSpecValidation(t *testing.T) { + tests := []struct { + name string + ts *TriggerSpec + want *apis.FieldError + }{{ + name: "invalid trigger spec", + ts: &TriggerSpec{}, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("broker", "filter", "subscriber") + return fe + }(), + }, { + name: "missing broker", + ts: &TriggerSpec{ + Broker: "", + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("broker") + return fe + }(), + }, { + name: "missing filter", + ts: &TriggerSpec{ + Broker: "test_broker", + Subscriber: validSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("filter") + return fe + }(), + }, { + name: "missing filter.sourceAndType", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: &TriggerFilter{}, + Subscriber: validSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("filter.sourceAndType") + return fe + }(), + }, { + name: "missing subscriber", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + return fe + }(), + }, { + name: "missing subscriber.ref.name", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + Subscriber: invalidSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber.ref.name") + return fe + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.ts.Validate() + if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" { + t.Errorf("%s: Validate TriggerSpec (-want, +got) = %v", test.name, diff) + } + }) + } +} + +func TestTriggerImmutableFields(t *testing.T) { + tests := []struct { + name string + current apis.Immutable + original apis.Immutable + want *apis.FieldError + }{{ + name: "good (no change)", + current: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + original: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + want: nil, + }, { + name: "new nil is ok", + current: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + original: nil, + want: nil, + }, { + name: "invalid type", + current: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + original: &Broker{}, + want: &apis.FieldError{ + Message: "The provided original was not a Trigger", + }, + }, { + name: "good (filter change)", + current: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + original: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + Filter: validTriggerFilter, + }, + }, + want: nil, + }, { + name: "bad (broker change)", + current: &Trigger{ + Spec: TriggerSpec{ + Broker: "broker", + }, + }, + original: &Trigger{ + Spec: TriggerSpec{ + Broker: "original_broker", + }, + }, + want: &apis.FieldError{ + Message: "Immutable fields changed (-old +new)", + Paths: []string{"spec", "broker"}, + Details: `{string}: + -: "original_broker" + +: "broker" +`, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.current.CheckImmutableFields(test.original) + if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" { + t.Errorf("CheckImmutableFields (-want, +got) = %v", diff) + } + }) + } +} diff --git a/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go index d43df63bdb2..44ba2ee9196 100644 --- a/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go @@ -21,12 +21,122 @@ limitations under the License. package v1alpha1 import ( - duck_v1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" - apis_duck_v1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + apis_duck_v1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + duck_v1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Broker) DeepCopyInto(out *Broker) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Broker. +func (in *Broker) DeepCopy() *Broker { + if in == nil { + return nil + } + out := new(Broker) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Broker) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BrokerList) DeepCopyInto(out *BrokerList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Broker, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrokerList. +func (in *BrokerList) DeepCopy() *BrokerList { + if in == nil { + return nil + } + out := new(BrokerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BrokerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BrokerSpec) DeepCopyInto(out *BrokerSpec) { + *out = *in + if in.ChannelTemplate != nil { + in, out := &in.ChannelTemplate, &out.ChannelTemplate + if *in == nil { + *out = nil + } else { + *out = new(ChannelSpec) + (*in).DeepCopyInto(*out) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrokerSpec. +func (in *BrokerSpec) DeepCopy() *BrokerSpec { + if in == nil { + return nil + } + out := new(BrokerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BrokerStatus) DeepCopyInto(out *BrokerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(duck_v1alpha1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Address = in.Address + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BrokerStatus. +func (in *BrokerStatus) DeepCopy() *BrokerStatus { + if in == nil { + return nil + } + out := new(BrokerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Channel) DeepCopyInto(out *Channel) { *out = *in @@ -114,7 +224,7 @@ func (in *ChannelSpec) DeepCopyInto(out *ChannelSpec) { if *in == nil { *out = nil } else { - *out = new(duck_v1alpha1.Subscribable) + *out = new(apis_duck_v1alpha1.Subscribable) (*in).DeepCopyInto(*out) } } @@ -137,7 +247,7 @@ func (in *ChannelStatus) DeepCopyInto(out *ChannelStatus) { out.Address = in.Address if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(apis_duck_v1alpha1.Conditions, len(*in)) + *out = make(duck_v1alpha1.Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -246,7 +356,7 @@ func (in *ClusterChannelProvisionerStatus) DeepCopyInto(out *ClusterChannelProvi *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(apis_duck_v1alpha1.Conditions, len(*in)) + *out = make(duck_v1alpha1.Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -424,7 +534,7 @@ func (in *SubscriptionStatus) DeepCopyInto(out *SubscriptionStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(apis_duck_v1alpha1.Conditions, len(*in)) + *out = make(duck_v1alpha1.Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -458,3 +568,162 @@ func (in *SubscriptionStatusPhysicalSubscription) DeepCopy() *SubscriptionStatus in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Trigger) DeepCopyInto(out *Trigger) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Trigger. +func (in *Trigger) DeepCopy() *Trigger { + if in == nil { + return nil + } + out := new(Trigger) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Trigger) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerFilter) DeepCopyInto(out *TriggerFilter) { + *out = *in + if in.SourceAndType != nil { + in, out := &in.SourceAndType, &out.SourceAndType + if *in == nil { + *out = nil + } else { + *out = new(TriggerFilterSourceAndType) + **out = **in + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerFilter. +func (in *TriggerFilter) DeepCopy() *TriggerFilter { + if in == nil { + return nil + } + out := new(TriggerFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerFilterSourceAndType) DeepCopyInto(out *TriggerFilterSourceAndType) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerFilterSourceAndType. +func (in *TriggerFilterSourceAndType) DeepCopy() *TriggerFilterSourceAndType { + if in == nil { + return nil + } + out := new(TriggerFilterSourceAndType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerList) DeepCopyInto(out *TriggerList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Trigger, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerList. +func (in *TriggerList) DeepCopy() *TriggerList { + if in == nil { + return nil + } + out := new(TriggerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TriggerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerSpec) DeepCopyInto(out *TriggerSpec) { + *out = *in + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + if *in == nil { + *out = nil + } else { + *out = new(TriggerFilter) + (*in).DeepCopyInto(*out) + } + } + if in.Subscriber != nil { + in, out := &in.Subscriber, &out.Subscriber + if *in == nil { + *out = nil + } else { + *out = new(SubscriberSpec) + (*in).DeepCopyInto(*out) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerSpec. +func (in *TriggerSpec) DeepCopy() *TriggerSpec { + if in == nil { + return nil + } + out := new(TriggerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggerStatus) DeepCopyInto(out *TriggerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(duck_v1alpha1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggerStatus. +func (in *TriggerStatus) DeepCopy() *TriggerStatus { + if in == nil { + return nil + } + out := new(TriggerStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/broker/receiver.go b/pkg/broker/receiver.go new file mode 100644 index 00000000000..c1d4b22578b --- /dev/null +++ b/pkg/broker/receiver.go @@ -0,0 +1,143 @@ +/* + * Copyright 2019 The Knative 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 broker + +import ( + "context" + "errors" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/provisioners" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// Receiver parses Cloud Events, determines if they pass a filter, and sends them to a subscriber. +type Receiver struct { + logger *zap.Logger + client client.Client + + dispatcher provisioners.Dispatcher +} + +// New creates a new Receiver and its associated MessageReceiver. The caller is responsible for +// Start()ing the returned MessageReceiver. +func New(logger *zap.Logger, client client.Client) (*Receiver, manager.Runnable) { + r := &Receiver{ + logger: logger, + client: client, + dispatcher: provisioners.NewMessageDispatcher(logger.Sugar()), + } + return r, r.newMessageReceiver() +} + +func (r *Receiver) newMessageReceiver() *provisioners.MessageReceiver { + if err := r.initClient(); err != nil { + r.logger.Warn("Failed to initialize client", zap.Error(err)) + } + return provisioners.NewMessageReceiver(r.sendEvent, r.logger.Sugar()) +} + +// sendEvent sends an event to a subscriber if the trigger filter passes. +func (r *Receiver) sendEvent(trigger provisioners.ChannelReference, message *provisioners.Message) error { + r.logger.Debug("Received message", zap.Any("triggerRef", trigger)) + ctx := context.Background() + + t, err := r.getTrigger(ctx, trigger) + if err != nil { + r.logger.Info("Unable to get the Trigger", zap.Error(err), zap.Any("triggerRef", trigger)) + return err + } + + subscriberURI := t.Status.SubscriberURI + if subscriberURI == "" { + r.logger.Error("Unable to read subscriberURI") + return errors.New("unable to read subscriberURI") + } + + if !r.shouldSendMessage(&t.Spec, message) { + r.logger.Debug("Message did not pass filter", zap.Any("triggerRef", trigger)) + return nil + } + + err = r.dispatcher.DispatchMessage(message, subscriberURI, "", provisioners.DispatchDefaults{}) + if err != nil { + r.logger.Info("Failed to dispatch message", zap.Error(err), zap.Any("triggerRef", trigger)) + return err + } + r.logger.Debug("Successfully sent message", zap.Any("triggerRef", trigger)) + return nil +} + +// Initialize the client. Mainly intended to create the informer/indexer in order not to drop messages. +func (r *Receiver) initClient() error { + // We list triggers so that we do not drop messages. Otherwise, on receiving an event, it + // may not find the trigger and would return an error. + opts := &client.ListOptions{} + tl := &eventingv1alpha1.TriggerList{} + if err := r.client.List(context.TODO(), opts, tl); err != nil { + return err + } + return nil +} + +func (r *Receiver) getTrigger(ctx context.Context, ref provisioners.ChannelReference) (*eventingv1alpha1.Trigger, error) { + t := &eventingv1alpha1.Trigger{} + err := r.client.Get(ctx, + types.NamespacedName{ + Namespace: ref.Namespace, + Name: ref.Name, + }, + t) + return t, err +} + +// shouldSendMessage determines whether message 'm' should be sent based on the triggerSpec 'ts'. +// Currently it supports exact matching on type and/or source of events. +func (r *Receiver) shouldSendMessage(ts *eventingv1alpha1.TriggerSpec, m *provisioners.Message) bool { + if ts.Filter == nil || ts.Filter.SourceAndType == nil { + r.logger.Error("No filter specified") + return false + } + filterType := ts.Filter.SourceAndType.Type + // TODO the inspection of Headers should be removed once we start using the cloud events SDK. + cloudEventType := "" + if et, ok := m.Headers["Ce-Eventtype"]; ok { + // cloud event spec v0.1. + cloudEventType = et + } else if et, ok := m.Headers["Ce-Type"]; ok { + // cloud event spec v0.2. + cloudEventType = et + } + if filterType != eventingv1alpha1.TriggerAnyFilter && filterType != cloudEventType { + r.logger.Debug("Wrong type", zap.String("trigger.spec.filter.sourceAndType.type", filterType), zap.String("message.type", cloudEventType)) + return false + } + filterSource := ts.Filter.SourceAndType.Source + cloudEventSource := "" + // cloud event spec v0.1 and v0.2. + if es, ok := m.Headers["Ce-Source"]; ok { + cloudEventSource = es + } + if filterSource != eventingv1alpha1.TriggerAnyFilter && filterSource != cloudEventSource { + r.logger.Debug("Wrong source", zap.String("trigger.spec.filter.sourceAndType.source", filterSource), zap.String("message.source", cloudEventSource)) + return false + } + return true +} diff --git a/pkg/broker/receiver_test.go b/pkg/broker/receiver_test.go new file mode 100644 index 00000000000..d98edb6be45 --- /dev/null +++ b/pkg/broker/receiver_test.go @@ -0,0 +1,210 @@ +/* + * Copyright 2019 The Knative 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 broker + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/knative/eventing/pkg/utils" + + "github.com/knative/eventing/pkg/provisioners" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "k8s.io/client-go/kubernetes/scheme" + + "go.uber.org/zap" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testNS = "test-namespace" + triggerName = "test-trigger" + eventType = `"com.example.someevent"` + eventSource = `"/mycontext"` +) + +func init() { + // Add types to scheme. + eventingv1alpha1.AddToScheme(scheme.Scheme) +} + +func TestReceiver(t *testing.T) { + testCases := map[string]struct { + initialState []runtime.Object + dispatchErr error + expectedErr bool + expectedDispatch bool + }{ + "Trigger.Get fails": { + // No trigger exists, so the Get will fail. + expectedErr: true, + }, + "Trigger doesn't have SubscriberURI": { + initialState: []runtime.Object{ + makeTriggerWithoutSubscriberURI(), + }, + expectedErr: true, + }, + "Trigger without a Filter": { + initialState: []runtime.Object{ + makeTriggerWithoutFilter(), + }, + }, + "Wrong type": { + initialState: []runtime.Object{ + makeTrigger("some-other-type", "Any"), + }, + }, + "Wrong source": { + initialState: []runtime.Object{ + makeTrigger("Any", "some-other-source"), + }, + }, + "Dispatch failed": { + initialState: []runtime.Object{ + makeTrigger("Any", "Any"), + }, + dispatchErr: errors.New("test error dispatching"), + expectedErr: true, + expectedDispatch: true, + }, + "Dispatch succeeded - Any": { + initialState: []runtime.Object{ + makeTrigger("Any", "Any"), + }, + expectedDispatch: true, + }, + "Dispatch succeeded - Specific": { + initialState: []runtime.Object{ + makeTrigger(eventType, eventSource), + }, + expectedDispatch: true, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + // Support cloud spec v0.1 and v0.2 requests. + requests := [2]*http.Request{makeV01Request(), makeV02Request()} + for _, request := range requests { + mr, _ := New( + zap.NewNop(), + fake.NewFakeClient(tc.initialState...)) + fd := &fakeDispatcher{ + err: tc.dispatchErr, + } + mr.dispatcher = fd + + resp := httptest.NewRecorder() + mr.newMessageReceiver().HandleRequest(resp, request) + if tc.expectedErr { + if resp.Result().StatusCode >= 200 && resp.Result().StatusCode < 300 { + t.Errorf("Expected an error. Actual: %v", resp.Result()) + } + } else { + if resp.Result().StatusCode < 200 || resp.Result().StatusCode >= 300 { + t.Errorf("Expected success. Actual: %v", resp.Result()) + } + } + if tc.expectedDispatch != fd.requestReceived { + t.Errorf("Incorrect dispatch. Expected %v, Actual %v", tc.expectedDispatch, fd.requestReceived) + } + } + }) + } +} + +type fakeDispatcher struct { + err error + requestReceived bool +} + +func (d *fakeDispatcher) DispatchMessage(_ *provisioners.Message, _, _ string, _ provisioners.DispatchDefaults) error { + d.requestReceived = true + return d.err +} + +func makeTrigger(t, s string) *eventingv1alpha1.Trigger { + return &eventingv1alpha1.Trigger{ + TypeMeta: v1.TypeMeta{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "Trigger", + }, + ObjectMeta: v1.ObjectMeta{ + Namespace: testNS, + Name: triggerName, + }, + Spec: eventingv1alpha1.TriggerSpec{ + Filter: &eventingv1alpha1.TriggerFilter{ + SourceAndType: &eventingv1alpha1.TriggerFilterSourceAndType{ + Type: t, + Source: s, + }, + }, + }, + Status: eventingv1alpha1.TriggerStatus{ + SubscriberURI: "subscriberURI", + }, + } +} + +func makeTriggerWithoutFilter() *eventingv1alpha1.Trigger { + t := makeTrigger("Any", "Any") + t.Spec.Filter = nil + return t +} + +func makeTriggerWithoutSubscriberURI() *eventingv1alpha1.Trigger { + t := makeTrigger("Any", "Any") + t.Status = eventingv1alpha1.TriggerStatus{} + return t +} + +func makeRequest(cloudEventVersionValue, eventTypeVersionValue, eventTypeKey, eventSourceKey string) *http.Request { + req := httptest.NewRequest("POST", "/", strings.NewReader(``)) + req.Host = fmt.Sprintf("%s.%s.triggers.%s", triggerName, testNS, utils.GetClusterDomainName()) + + eventAttributes := map[string]string{ + "CE-CloudEventsVersion": cloudEventVersionValue, + eventTypeKey: eventType, + "CE-EventTypeVersion": eventTypeVersionValue, + eventSourceKey: eventSource, + "CE-EventID": `"A234-1234-1234"`, + "CE-EventTime": `"2018-04-05T17:31:00Z"`, + "contentType": "text/xml", + } + for k, v := range eventAttributes { + req.Header.Set(k, v) + } + return req +} + +func makeV01Request() *http.Request { + return makeRequest(`"0.1"`, `"1.0"`, "CE-EventType", "CE-Source") +} + +func makeV02Request() *http.Request { + return makeRequest(`"0.2"`, `"2.0"`, "ce-type", "ce-source") +} diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/broker.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/broker.go new file mode 100644 index 00000000000..7ae965feb0e --- /dev/null +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/broker.go @@ -0,0 +1,174 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + scheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// BrokersGetter has a method to return a BrokerInterface. +// A group's client should implement this interface. +type BrokersGetter interface { + Brokers(namespace string) BrokerInterface +} + +// BrokerInterface has methods to work with Broker resources. +type BrokerInterface interface { + Create(*v1alpha1.Broker) (*v1alpha1.Broker, error) + Update(*v1alpha1.Broker) (*v1alpha1.Broker, error) + UpdateStatus(*v1alpha1.Broker) (*v1alpha1.Broker, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.Broker, error) + List(opts v1.ListOptions) (*v1alpha1.BrokerList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Broker, err error) + BrokerExpansion +} + +// brokers implements BrokerInterface +type brokers struct { + client rest.Interface + ns string +} + +// newBrokers returns a Brokers +func newBrokers(c *EventingV1alpha1Client, namespace string) *brokers { + return &brokers{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the broker, and returns the corresponding broker object, and an error if there is any. +func (c *brokers) Get(name string, options v1.GetOptions) (result *v1alpha1.Broker, err error) { + result = &v1alpha1.Broker{} + err = c.client.Get(). + Namespace(c.ns). + Resource("brokers"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Brokers that match those selectors. +func (c *brokers) List(opts v1.ListOptions) (result *v1alpha1.BrokerList, err error) { + result = &v1alpha1.BrokerList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("brokers"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested brokers. +func (c *brokers) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("brokers"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a broker and creates it. Returns the server's representation of the broker, and an error, if there is any. +func (c *brokers) Create(broker *v1alpha1.Broker) (result *v1alpha1.Broker, err error) { + result = &v1alpha1.Broker{} + err = c.client.Post(). + Namespace(c.ns). + Resource("brokers"). + Body(broker). + Do(). + Into(result) + return +} + +// Update takes the representation of a broker and updates it. Returns the server's representation of the broker, and an error, if there is any. +func (c *brokers) Update(broker *v1alpha1.Broker) (result *v1alpha1.Broker, err error) { + result = &v1alpha1.Broker{} + err = c.client.Put(). + Namespace(c.ns). + Resource("brokers"). + Name(broker.Name). + Body(broker). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *brokers) UpdateStatus(broker *v1alpha1.Broker) (result *v1alpha1.Broker, err error) { + result = &v1alpha1.Broker{} + err = c.client.Put(). + Namespace(c.ns). + Resource("brokers"). + Name(broker.Name). + SubResource("status"). + Body(broker). + Do(). + Into(result) + return +} + +// Delete takes name of the broker and deletes it. Returns an error if one occurs. +func (c *brokers) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("brokers"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *brokers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("brokers"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched broker. +func (c *brokers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Broker, err error) { + result = &v1alpha1.Broker{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("brokers"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/eventing_client.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/eventing_client.go index ef337f70bf0..753d1081b84 100644 --- a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/eventing_client.go +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/eventing_client.go @@ -27,9 +27,11 @@ import ( type EventingV1alpha1Interface interface { RESTClient() rest.Interface + BrokersGetter ChannelsGetter ClusterChannelProvisionersGetter SubscriptionsGetter + TriggersGetter } // EventingV1alpha1Client is used to interact with features provided by the eventing.knative.dev group. @@ -37,6 +39,10 @@ type EventingV1alpha1Client struct { restClient rest.Interface } +func (c *EventingV1alpha1Client) Brokers(namespace string) BrokerInterface { + return newBrokers(c, namespace) +} + func (c *EventingV1alpha1Client) Channels(namespace string) ChannelInterface { return newChannels(c, namespace) } @@ -49,6 +55,10 @@ func (c *EventingV1alpha1Client) Subscriptions(namespace string) SubscriptionInt return newSubscriptions(c, namespace) } +func (c *EventingV1alpha1Client) Triggers(namespace string) TriggerInterface { + return newTriggers(c, namespace) +} + // NewForConfig creates a new EventingV1alpha1Client for the given config. func NewForConfig(c *rest.Config) (*EventingV1alpha1Client, error) { config := *c diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_broker.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_broker.go new file mode 100644 index 00000000000..c7b30557ec1 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_broker.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeBrokers implements BrokerInterface +type FakeBrokers struct { + Fake *FakeEventingV1alpha1 + ns string +} + +var brokersResource = schema.GroupVersionResource{Group: "eventing.knative.dev", Version: "v1alpha1", Resource: "brokers"} + +var brokersKind = schema.GroupVersionKind{Group: "eventing.knative.dev", Version: "v1alpha1", Kind: "Broker"} + +// Get takes name of the broker, and returns the corresponding broker object, and an error if there is any. +func (c *FakeBrokers) Get(name string, options v1.GetOptions) (result *v1alpha1.Broker, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(brokersResource, c.ns, name), &v1alpha1.Broker{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Broker), err +} + +// List takes label and field selectors, and returns the list of Brokers that match those selectors. +func (c *FakeBrokers) List(opts v1.ListOptions) (result *v1alpha1.BrokerList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(brokersResource, brokersKind, c.ns, opts), &v1alpha1.BrokerList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.BrokerList{ListMeta: obj.(*v1alpha1.BrokerList).ListMeta} + for _, item := range obj.(*v1alpha1.BrokerList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested brokers. +func (c *FakeBrokers) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(brokersResource, c.ns, opts)) + +} + +// Create takes the representation of a broker and creates it. Returns the server's representation of the broker, and an error, if there is any. +func (c *FakeBrokers) Create(broker *v1alpha1.Broker) (result *v1alpha1.Broker, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(brokersResource, c.ns, broker), &v1alpha1.Broker{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Broker), err +} + +// Update takes the representation of a broker and updates it. Returns the server's representation of the broker, and an error, if there is any. +func (c *FakeBrokers) Update(broker *v1alpha1.Broker) (result *v1alpha1.Broker, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(brokersResource, c.ns, broker), &v1alpha1.Broker{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Broker), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeBrokers) UpdateStatus(broker *v1alpha1.Broker) (*v1alpha1.Broker, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(brokersResource, "status", c.ns, broker), &v1alpha1.Broker{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Broker), err +} + +// Delete takes name of the broker and deletes it. Returns an error if one occurs. +func (c *FakeBrokers) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(brokersResource, c.ns, name), &v1alpha1.Broker{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeBrokers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(brokersResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.BrokerList{}) + return err +} + +// Patch applies the patch and returns the patched broker. +func (c *FakeBrokers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Broker, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(brokersResource, c.ns, name, data, subresources...), &v1alpha1.Broker{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Broker), err +} diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_eventing_client.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_eventing_client.go index 0080c07b8d4..4362e785f5a 100644 --- a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_eventing_client.go +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_eventing_client.go @@ -28,6 +28,10 @@ type FakeEventingV1alpha1 struct { *testing.Fake } +func (c *FakeEventingV1alpha1) Brokers(namespace string) v1alpha1.BrokerInterface { + return &FakeBrokers{c, namespace} +} + func (c *FakeEventingV1alpha1) Channels(namespace string) v1alpha1.ChannelInterface { return &FakeChannels{c, namespace} } @@ -40,6 +44,10 @@ func (c *FakeEventingV1alpha1) Subscriptions(namespace string) v1alpha1.Subscrip return &FakeSubscriptions{c, namespace} } +func (c *FakeEventingV1alpha1) Triggers(namespace string) v1alpha1.TriggerInterface { + return &FakeTriggers{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeEventingV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_trigger.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_trigger.go new file mode 100644 index 00000000000..5e4b588c6b1 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/fake/fake_trigger.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeTriggers implements TriggerInterface +type FakeTriggers struct { + Fake *FakeEventingV1alpha1 + ns string +} + +var triggersResource = schema.GroupVersionResource{Group: "eventing.knative.dev", Version: "v1alpha1", Resource: "triggers"} + +var triggersKind = schema.GroupVersionKind{Group: "eventing.knative.dev", Version: "v1alpha1", Kind: "Trigger"} + +// Get takes name of the trigger, and returns the corresponding trigger object, and an error if there is any. +func (c *FakeTriggers) Get(name string, options v1.GetOptions) (result *v1alpha1.Trigger, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(triggersResource, c.ns, name), &v1alpha1.Trigger{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Trigger), err +} + +// List takes label and field selectors, and returns the list of Triggers that match those selectors. +func (c *FakeTriggers) List(opts v1.ListOptions) (result *v1alpha1.TriggerList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(triggersResource, triggersKind, c.ns, opts), &v1alpha1.TriggerList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.TriggerList{ListMeta: obj.(*v1alpha1.TriggerList).ListMeta} + for _, item := range obj.(*v1alpha1.TriggerList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested triggers. +func (c *FakeTriggers) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(triggersResource, c.ns, opts)) + +} + +// Create takes the representation of a trigger and creates it. Returns the server's representation of the trigger, and an error, if there is any. +func (c *FakeTriggers) Create(trigger *v1alpha1.Trigger) (result *v1alpha1.Trigger, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(triggersResource, c.ns, trigger), &v1alpha1.Trigger{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Trigger), err +} + +// Update takes the representation of a trigger and updates it. Returns the server's representation of the trigger, and an error, if there is any. +func (c *FakeTriggers) Update(trigger *v1alpha1.Trigger) (result *v1alpha1.Trigger, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(triggersResource, c.ns, trigger), &v1alpha1.Trigger{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Trigger), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeTriggers) UpdateStatus(trigger *v1alpha1.Trigger) (*v1alpha1.Trigger, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(triggersResource, "status", c.ns, trigger), &v1alpha1.Trigger{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Trigger), err +} + +// Delete takes name of the trigger and deletes it. Returns an error if one occurs. +func (c *FakeTriggers) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(triggersResource, c.ns, name), &v1alpha1.Trigger{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeTriggers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(triggersResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.TriggerList{}) + return err +} + +// Patch applies the patch and returns the patched trigger. +func (c *FakeTriggers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Trigger, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(triggersResource, c.ns, name, data, subresources...), &v1alpha1.Trigger{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Trigger), err +} diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/generated_expansion.go index fcef70e765c..2f88fb3320b 100644 --- a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/generated_expansion.go @@ -18,8 +18,12 @@ limitations under the License. package v1alpha1 +type BrokerExpansion interface{} + type ChannelExpansion interface{} type ClusterChannelProvisionerExpansion interface{} type SubscriptionExpansion interface{} + +type TriggerExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/eventing/v1alpha1/trigger.go b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/trigger.go new file mode 100644 index 00000000000..72207e79d34 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/eventing/v1alpha1/trigger.go @@ -0,0 +1,174 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + scheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// TriggersGetter has a method to return a TriggerInterface. +// A group's client should implement this interface. +type TriggersGetter interface { + Triggers(namespace string) TriggerInterface +} + +// TriggerInterface has methods to work with Trigger resources. +type TriggerInterface interface { + Create(*v1alpha1.Trigger) (*v1alpha1.Trigger, error) + Update(*v1alpha1.Trigger) (*v1alpha1.Trigger, error) + UpdateStatus(*v1alpha1.Trigger) (*v1alpha1.Trigger, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.Trigger, error) + List(opts v1.ListOptions) (*v1alpha1.TriggerList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Trigger, err error) + TriggerExpansion +} + +// triggers implements TriggerInterface +type triggers struct { + client rest.Interface + ns string +} + +// newTriggers returns a Triggers +func newTriggers(c *EventingV1alpha1Client, namespace string) *triggers { + return &triggers{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the trigger, and returns the corresponding trigger object, and an error if there is any. +func (c *triggers) Get(name string, options v1.GetOptions) (result *v1alpha1.Trigger, err error) { + result = &v1alpha1.Trigger{} + err = c.client.Get(). + Namespace(c.ns). + Resource("triggers"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Triggers that match those selectors. +func (c *triggers) List(opts v1.ListOptions) (result *v1alpha1.TriggerList, err error) { + result = &v1alpha1.TriggerList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("triggers"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested triggers. +func (c *triggers) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("triggers"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a trigger and creates it. Returns the server's representation of the trigger, and an error, if there is any. +func (c *triggers) Create(trigger *v1alpha1.Trigger) (result *v1alpha1.Trigger, err error) { + result = &v1alpha1.Trigger{} + err = c.client.Post(). + Namespace(c.ns). + Resource("triggers"). + Body(trigger). + Do(). + Into(result) + return +} + +// Update takes the representation of a trigger and updates it. Returns the server's representation of the trigger, and an error, if there is any. +func (c *triggers) Update(trigger *v1alpha1.Trigger) (result *v1alpha1.Trigger, err error) { + result = &v1alpha1.Trigger{} + err = c.client.Put(). + Namespace(c.ns). + Resource("triggers"). + Name(trigger.Name). + Body(trigger). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *triggers) UpdateStatus(trigger *v1alpha1.Trigger) (result *v1alpha1.Trigger, err error) { + result = &v1alpha1.Trigger{} + err = c.client.Put(). + Namespace(c.ns). + Resource("triggers"). + Name(trigger.Name). + SubResource("status"). + Body(trigger). + Do(). + Into(result) + return +} + +// Delete takes name of the trigger and deletes it. Returns an error if one occurs. +func (c *triggers) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("triggers"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *triggers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("triggers"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched trigger. +func (c *triggers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Trigger, err error) { + result = &v1alpha1.Trigger{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("triggers"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/eventing/v1alpha1/broker.go b/pkg/client/informers/externalversions/eventing/v1alpha1/broker.go new file mode 100644 index 00000000000..f9dcaf534b3 --- /dev/null +++ b/pkg/client/informers/externalversions/eventing/v1alpha1/broker.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + eventing_v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + versioned "github.com/knative/eventing/pkg/client/clientset/versioned" + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// BrokerInformer provides access to a shared informer and lister for +// Brokers. +type BrokerInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.BrokerLister +} + +type brokerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewBrokerInformer constructs a new informer for Broker type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewBrokerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredBrokerInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredBrokerInformer constructs a new informer for Broker type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredBrokerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.EventingV1alpha1().Brokers(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.EventingV1alpha1().Brokers(namespace).Watch(options) + }, + }, + &eventing_v1alpha1.Broker{}, + resyncPeriod, + indexers, + ) +} + +func (f *brokerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredBrokerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *brokerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&eventing_v1alpha1.Broker{}, f.defaultInformer) +} + +func (f *brokerInformer) Lister() v1alpha1.BrokerLister { + return v1alpha1.NewBrokerLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/eventing/v1alpha1/interface.go b/pkg/client/informers/externalversions/eventing/v1alpha1/interface.go index 15bebacbe7f..29ad6f191f0 100644 --- a/pkg/client/informers/externalversions/eventing/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/eventing/v1alpha1/interface.go @@ -24,12 +24,16 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Brokers returns a BrokerInformer. + Brokers() BrokerInformer // Channels returns a ChannelInformer. Channels() ChannelInformer // ClusterChannelProvisioners returns a ClusterChannelProvisionerInformer. ClusterChannelProvisioners() ClusterChannelProvisionerInformer // Subscriptions returns a SubscriptionInformer. Subscriptions() SubscriptionInformer + // Triggers returns a TriggerInformer. + Triggers() TriggerInformer } type version struct { @@ -43,6 +47,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Brokers returns a BrokerInformer. +func (v *version) Brokers() BrokerInformer { + return &brokerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Channels returns a ChannelInformer. func (v *version) Channels() ChannelInformer { return &channelInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} @@ -57,3 +66,8 @@ func (v *version) ClusterChannelProvisioners() ClusterChannelProvisionerInformer func (v *version) Subscriptions() SubscriptionInformer { return &subscriptionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// Triggers returns a TriggerInformer. +func (v *version) Triggers() TriggerInformer { + return &triggerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/eventing/v1alpha1/trigger.go b/pkg/client/informers/externalversions/eventing/v1alpha1/trigger.go new file mode 100644 index 00000000000..c1b01ef002b --- /dev/null +++ b/pkg/client/informers/externalversions/eventing/v1alpha1/trigger.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + eventing_v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + versioned "github.com/knative/eventing/pkg/client/clientset/versioned" + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TriggerInformer provides access to a shared informer and lister for +// Triggers. +type TriggerInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.TriggerLister +} + +type triggerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewTriggerInformer constructs a new informer for Trigger type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTriggerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTriggerInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredTriggerInformer constructs a new informer for Trigger type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTriggerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.EventingV1alpha1().Triggers(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.EventingV1alpha1().Triggers(namespace).Watch(options) + }, + }, + &eventing_v1alpha1.Trigger{}, + resyncPeriod, + indexers, + ) +} + +func (f *triggerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTriggerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *triggerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&eventing_v1alpha1.Trigger{}, f.defaultInformer) +} + +func (f *triggerInformer) Lister() v1alpha1.TriggerLister { + return v1alpha1.NewTriggerLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index eb07d8f78ef..6c22f720af9 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -53,12 +53,16 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=eventing.knative.dev, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("brokers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Brokers().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("channels"): return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Channels().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("clusterchannelprovisioners"): return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().ClusterChannelProvisioners().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("subscriptions"): return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Subscriptions().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("triggers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Triggers().Informer()}, nil } diff --git a/pkg/client/listers/eventing/v1alpha1/broker.go b/pkg/client/listers/eventing/v1alpha1/broker.go new file mode 100644 index 00000000000..4916e953b2f --- /dev/null +++ b/pkg/client/listers/eventing/v1alpha1/broker.go @@ -0,0 +1,94 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// BrokerLister helps list Brokers. +type BrokerLister interface { + // List lists all Brokers in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.Broker, err error) + // Brokers returns an object that can list and get Brokers. + Brokers(namespace string) BrokerNamespaceLister + BrokerListerExpansion +} + +// brokerLister implements the BrokerLister interface. +type brokerLister struct { + indexer cache.Indexer +} + +// NewBrokerLister returns a new BrokerLister. +func NewBrokerLister(indexer cache.Indexer) BrokerLister { + return &brokerLister{indexer: indexer} +} + +// List lists all Brokers in the indexer. +func (s *brokerLister) List(selector labels.Selector) (ret []*v1alpha1.Broker, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Broker)) + }) + return ret, err +} + +// Brokers returns an object that can list and get Brokers. +func (s *brokerLister) Brokers(namespace string) BrokerNamespaceLister { + return brokerNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// BrokerNamespaceLister helps list and get Brokers. +type BrokerNamespaceLister interface { + // List lists all Brokers in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.Broker, err error) + // Get retrieves the Broker from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.Broker, error) + BrokerNamespaceListerExpansion +} + +// brokerNamespaceLister implements the BrokerNamespaceLister +// interface. +type brokerNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Brokers in the indexer for a given namespace. +func (s brokerNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Broker, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Broker)) + }) + return ret, err +} + +// Get retrieves the Broker from the indexer for a given namespace and name. +func (s brokerNamespaceLister) Get(name string) (*v1alpha1.Broker, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("broker"), name) + } + return obj.(*v1alpha1.Broker), nil +} diff --git a/pkg/client/listers/eventing/v1alpha1/expansion_generated.go b/pkg/client/listers/eventing/v1alpha1/expansion_generated.go index ce1c8af2399..2a49e1b434b 100644 --- a/pkg/client/listers/eventing/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/eventing/v1alpha1/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v1alpha1 +// BrokerListerExpansion allows custom methods to be added to +// BrokerLister. +type BrokerListerExpansion interface{} + +// BrokerNamespaceListerExpansion allows custom methods to be added to +// BrokerNamespaceLister. +type BrokerNamespaceListerExpansion interface{} + // ChannelListerExpansion allows custom methods to be added to // ChannelLister. type ChannelListerExpansion interface{} @@ -37,3 +45,11 @@ type SubscriptionListerExpansion interface{} // SubscriptionNamespaceListerExpansion allows custom methods to be added to // SubscriptionNamespaceLister. type SubscriptionNamespaceListerExpansion interface{} + +// TriggerListerExpansion allows custom methods to be added to +// TriggerLister. +type TriggerListerExpansion interface{} + +// TriggerNamespaceListerExpansion allows custom methods to be added to +// TriggerNamespaceLister. +type TriggerNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/eventing/v1alpha1/trigger.go b/pkg/client/listers/eventing/v1alpha1/trigger.go new file mode 100644 index 00000000000..56c323a8238 --- /dev/null +++ b/pkg/client/listers/eventing/v1alpha1/trigger.go @@ -0,0 +1,94 @@ +/* +Copyright 2018 The Knative 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// TriggerLister helps list Triggers. +type TriggerLister interface { + // List lists all Triggers in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.Trigger, err error) + // Triggers returns an object that can list and get Triggers. + Triggers(namespace string) TriggerNamespaceLister + TriggerListerExpansion +} + +// triggerLister implements the TriggerLister interface. +type triggerLister struct { + indexer cache.Indexer +} + +// NewTriggerLister returns a new TriggerLister. +func NewTriggerLister(indexer cache.Indexer) TriggerLister { + return &triggerLister{indexer: indexer} +} + +// List lists all Triggers in the indexer. +func (s *triggerLister) List(selector labels.Selector) (ret []*v1alpha1.Trigger, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Trigger)) + }) + return ret, err +} + +// Triggers returns an object that can list and get Triggers. +func (s *triggerLister) Triggers(namespace string) TriggerNamespaceLister { + return triggerNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// TriggerNamespaceLister helps list and get Triggers. +type TriggerNamespaceLister interface { + // List lists all Triggers in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.Trigger, err error) + // Get retrieves the Trigger from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.Trigger, error) + TriggerNamespaceListerExpansion +} + +// triggerNamespaceLister implements the TriggerNamespaceLister +// interface. +type triggerNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Triggers in the indexer for a given namespace. +func (s triggerNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Trigger, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Trigger)) + }) + return ret, err +} + +// Get retrieves the Trigger from the indexer for a given namespace and name. +func (s triggerNamespaceLister) Get(name string) (*v1alpha1.Trigger, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("trigger"), name) + } + return obj.(*v1alpha1.Trigger), nil +} diff --git a/pkg/reconciler/v1alpha1/broker/broker.go b/pkg/reconciler/v1alpha1/broker/broker.go new file mode 100644 index 00000000000..343abfc699f --- /dev/null +++ b/pkg/reconciler/v1alpha1/broker/broker.go @@ -0,0 +1,436 @@ +/* +Copyright 2018 The Knative 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 broker + +import ( + "context" + "fmt" + "time" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/logging" + "github.com/knative/eventing/pkg/reconciler/names" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker/resources" + "go.uber.org/zap" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "broker-controller" + + // Name of the corev1.Events emitted from the reconciliation process. + brokerReconciled = "BrokerReconciled" + brokerUpdateStatusFailed = "BrokerUpdateStatusFailed" +) + +type reconciler struct { + client client.Client + recorder record.EventRecorder + + logger *zap.Logger + + ingressImage string + ingressServiceAccountName string + filterImage string + filterServiceAccountName string +} + +// Verify the struct implements reconcile.Reconciler. +var _ reconcile.Reconciler = &reconciler{} + +type ReconcilerArgs struct { + IngressImage string + IngressServiceAccountName string + FilterImage string + FilterServiceAccountName string +} + +// ProvideController returns a function that returns a Broker controller. +func ProvideController(args ReconcilerArgs) func(manager.Manager, *zap.Logger) (controller.Controller, error) { + return func(mgr manager.Manager, logger *zap.Logger) (controller.Controller, error) { + // Setup a new controller to Reconcile Brokers. + c, err := controller.New(controllerAgentName, mgr, controller.Options{ + Reconciler: &reconciler{ + recorder: mgr.GetRecorder(controllerAgentName), + logger: logger, + + ingressImage: args.IngressImage, + ingressServiceAccountName: args.IngressServiceAccountName, + filterImage: args.FilterImage, + filterServiceAccountName: args.FilterServiceAccountName, + }, + }) + if err != nil { + return nil, err + } + + // Watch Brokers. + if err = c.Watch(&source.Kind{Type: &v1alpha1.Broker{}}, &handler.EnqueueRequestForObject{}); err != nil { + return nil, err + } + + // Watch all the resources that the Broker reconciles. + for _, t := range []runtime.Object{&v1alpha1.Channel{}, &corev1.Service{}, &v1.Deployment{}} { + err = c.Watch(&source.Kind{Type: t}, &handler.EnqueueRequestForOwner{OwnerType: &v1alpha1.Broker{}, IsController: true}) + if err != nil { + return nil, err + } + } + + return c, nil + } +} + +func (r *reconciler) InjectClient(c client.Client) error { + r.client = c + return nil +} + +// Reconcile compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the Broker resource +// with the current status of the resource. +func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + ctx := context.TODO() + ctx = logging.WithLogger(ctx, r.logger.With(zap.Any("request", request))) + + broker := &v1alpha1.Broker{} + err := r.client.Get(ctx, request.NamespacedName, broker) + + if errors.IsNotFound(err) { + logging.FromContext(ctx).Info("Could not find Broker") + return reconcile.Result{}, nil + } + + if err != nil { + logging.FromContext(ctx).Error("Could not Get Broker", zap.Error(err)) + return reconcile.Result{}, err + } + + // Reconcile this copy of the Broker and then write back any status updates regardless of + // whether the reconcile error out. + result, reconcileErr := r.reconcile(ctx, broker) + if reconcileErr != nil { + logging.FromContext(ctx).Error("Error reconciling Broker", zap.Error(reconcileErr)) + } else if result.Requeue || result.RequeueAfter > 0 { + logging.FromContext(ctx).Debug("Broker reconcile requeuing") + } else { + logging.FromContext(ctx).Debug("Broker reconciled") + r.recorder.Event(broker, corev1.EventTypeNormal, brokerReconciled, "Broker reconciled") + } + + if _, err = r.updateStatus(broker); err != nil { + logging.FromContext(ctx).Error("Failed to update Broker status", zap.Error(err)) + r.recorder.Eventf(broker, corev1.EventTypeWarning, brokerUpdateStatusFailed, "Failed to update Broker's status: %v", err) + return reconcile.Result{}, err + } + + // Requeue if the resource is not ready: + return result, reconcileErr +} + +func (r *reconciler) reconcile(ctx context.Context, b *v1alpha1.Broker) (reconcile.Result, error) { + b.Status.InitializeConditions() + + // 1. Channel is created for all events. + // 2. Filter Deployment. + // 3. Ingress Deployment. + // 4. K8s Services that point at the Deployments. + + if b.DeletionTimestamp != nil { + // Everything is cleaned up by the garbage collector. + return reconcile.Result{}, nil + } + + c, err := r.reconcileChannel(ctx, b) + if err != nil { + logging.FromContext(ctx).Error("Problem reconciling the channel", zap.Error(err)) + b.Status.MarkChannelFailed(err) + return reconcile.Result{}, err + } else if c.Status.Address.Hostname == "" { + logging.FromContext(ctx).Info("Channel is not yet ready", zap.Any("c", c)) + // Give the Channel some time to get its address. One second was chosen arbitrarily. + return reconcile.Result{RequeueAfter: time.Second}, nil + } + b.Status.MarkChannelReady() + + _, err = r.reconcileFilterDeployment(ctx, b) + if err != nil { + logging.FromContext(ctx).Error("Problem reconciling filter Deployment", zap.Error(err)) + b.Status.MarkFilterFailed(err) + return reconcile.Result{}, err + } + _, err = r.reconcileFilterService(ctx, b) + if err != nil { + logging.FromContext(ctx).Error("Problem reconciling filter Service", zap.Error(err)) + b.Status.MarkFilterFailed(err) + return reconcile.Result{}, err + } + b.Status.MarkFilterReady() + + _, err = r.reconcileIngressDeployment(ctx, b, c) + if err != nil { + logging.FromContext(ctx).Error("Problem reconciling ingress Deployment", zap.Error(err)) + b.Status.MarkIngressFailed(err) + return reconcile.Result{}, err + } + + svc, err := r.reconcileIngressService(ctx, b) + if err != nil { + logging.FromContext(ctx).Error("Problem reconciling ingress Service", zap.Error(err)) + b.Status.MarkIngressFailed(err) + return reconcile.Result{}, err + } + b.Status.MarkIngressReady() + b.Status.SetAddress(names.ServiceHostName(svc.Name, svc.Namespace)) + + return reconcile.Result{}, nil +} + +// updateStatus may in fact update the broker's finalizers in addition to the status. +func (r *reconciler) updateStatus(broker *v1alpha1.Broker) (*v1alpha1.Broker, error) { + ctx := context.TODO() + objectKey := client.ObjectKey{Namespace: broker.Namespace, Name: broker.Name} + latestBroker := &v1alpha1.Broker{} + + if err := r.client.Get(ctx, objectKey, latestBroker); err != nil { + return nil, err + } + + brokerChanged := false + + if !equality.Semantic.DeepEqual(latestBroker.Finalizers, broker.Finalizers) { + latestBroker.SetFinalizers(broker.ObjectMeta.Finalizers) + if err := r.client.Update(ctx, latestBroker); err != nil { + return nil, err + } + brokerChanged = true + } + + if equality.Semantic.DeepEqual(latestBroker.Status, broker.Status) { + return latestBroker, nil + } + + if brokerChanged { + // Re-fetch. + latestBroker = &v1alpha1.Broker{} + if err := r.client.Get(ctx, objectKey, latestBroker); err != nil { + return nil, err + } + } + + latestBroker.Status = broker.Status + if err := r.client.Status().Update(ctx, latestBroker); err != nil { + return nil, err + } + + return latestBroker, nil +} + +// reconcileFilterDeployment reconciles Broker's 'b' filter deployment. +func (r *reconciler) reconcileFilterDeployment(ctx context.Context, b *v1alpha1.Broker) (*v1.Deployment, error) { + expected := resources.MakeFilterDeployment(&resources.FilterArgs{ + Broker: b, + Image: r.filterImage, + ServiceAccountName: r.filterServiceAccountName, + }) + return r.reconcileDeployment(ctx, expected) +} + +// reconcileFilterService reconciles Broker's 'b' filter service. +func (r *reconciler) reconcileFilterService(ctx context.Context, b *v1alpha1.Broker) (*corev1.Service, error) { + expected := resources.MakeFilterService(b) + return r.reconcileService(ctx, expected) +} + +// reconcileChannel reconciles Broker's 'b' underlying channel. +func (r *reconciler) reconcileChannel(ctx context.Context, b *v1alpha1.Broker) (*v1alpha1.Channel, error) { + c, err := r.getChannel(ctx, b) + // If the resource doesn't exist, we'll create it + if k8serrors.IsNotFound(err) { + c = newChannel(b) + err = r.client.Create(ctx, c) + if err != nil { + return nil, err + } + return c, nil + } else if err != nil { + return nil, err + } + + // TODO Determine if we want to update spec (maybe just args?). + // Update Channel if it has changed. Note that we need to both ignore the real Channel's + // subscribable section and if we need to update the real Channel, retain it. + //expected.Spec.Subscribable = c.Spec.Subscribable + //if !equality.Semantic.DeepDerivative(expected.Spec, c.Spec) { + // c.Spec = expected.Spec + // err = r.client.Update(ctx, c) + // if err != nil { + // return nil, err + // } + //} + return c, nil +} + +// getChannel returns the Channel object for Broker 'b' if exists, otherwise it returns an error. +func (r *reconciler) getChannel(ctx context.Context, b *v1alpha1.Broker) (*v1alpha1.Channel, error) { + list := &v1alpha1.ChannelList{} + opts := &runtimeclient.ListOptions{ + Namespace: b.Namespace, + LabelSelector: labels.SelectorFromSet(ChannelLabels(b)), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + + err := r.client.List(ctx, opts, list) + if err != nil { + return nil, err + } + for _, c := range list.Items { + if metav1.IsControlledBy(&c, b) { + return &c, nil + } + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "") +} + +// newChannel creates a new Channel for Broker 'b'. +func newChannel(b *v1alpha1.Broker) *v1alpha1.Channel { + var spec v1alpha1.ChannelSpec + if b.Spec.ChannelTemplate != nil { + spec = *b.Spec.ChannelTemplate + } + + return &v1alpha1.Channel{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.Namespace, + GenerateName: fmt.Sprintf("%s-broker-", b.Name), + Labels: ChannelLabels(b), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(b, schema.GroupVersionKind{ + Group: v1alpha1.SchemeGroupVersion.Group, + Version: v1alpha1.SchemeGroupVersion.Version, + Kind: "Broker", + }), + }, + }, + Spec: spec, + } +} + +func ChannelLabels(b *v1alpha1.Broker) map[string]string { + return map[string]string{ + "eventing.knative.dev/broker": b.Name, + "eventing.knative.dev/brokerEverything": "true", + } +} + +// reconcileDeployment reconciles the K8s Deployment 'd'. +func (r *reconciler) reconcileDeployment(ctx context.Context, d *v1.Deployment) (*v1.Deployment, error) { + name := types.NamespacedName{ + Namespace: d.Namespace, + Name: d.Name, + } + current := &v1.Deployment{} + err := r.client.Get(ctx, name, current) + if k8serrors.IsNotFound(err) { + err = r.client.Create(ctx, d) + if err != nil { + return nil, err + } + return d, nil + } else if err != nil { + return nil, err + } + + if !equality.Semantic.DeepDerivative(d.Spec, current.Spec) { + current.Spec = d.Spec + err = r.client.Update(ctx, current) + if err != nil { + return nil, err + } + } + return current, nil +} + +// reconcileService reconciles the K8s Service 'svc'. +func (r *reconciler) reconcileService(ctx context.Context, svc *corev1.Service) (*corev1.Service, error) { + name := types.NamespacedName{ + Namespace: svc.Namespace, + Name: svc.Name, + } + current := &corev1.Service{} + err := r.client.Get(ctx, name, current) + if k8serrors.IsNotFound(err) { + err = r.client.Create(ctx, svc) + if err != nil { + return nil, err + } + return svc, nil + } else if err != nil { + return nil, err + } + + // spec.clusterIP is immutable and is set on existing services. If we don't set this to the same value, we will + // encounter an error while updating. + svc.Spec.ClusterIP = current.Spec.ClusterIP + if !equality.Semantic.DeepDerivative(svc.Spec, current.Spec) { + current.Spec = svc.Spec + err = r.client.Update(ctx, current) + if err != nil { + return nil, err + } + } + return current, nil +} + +// reconcileIngressDeployment reconciles the Ingress Deployment. +func (r *reconciler) reconcileIngressDeployment(ctx context.Context, b *v1alpha1.Broker, c *v1alpha1.Channel) (*v1.Deployment, error) { + expected := resources.MakeIngress(&resources.IngressArgs{ + Broker: b, + Image: r.ingressImage, + ServiceAccountName: r.ingressServiceAccountName, + ChannelAddress: c.Status.Address.Hostname, + }) + return r.reconcileDeployment(ctx, expected) +} + +// reconcileIngressService reconciles the Ingress Service. +func (r *reconciler) reconcileIngressService(ctx context.Context, b *v1alpha1.Broker) (*corev1.Service, error) { + expected := resources.MakeIngressService(b) + return r.reconcileService(ctx, expected) +} diff --git a/pkg/reconciler/v1alpha1/broker/broker_test.go b/pkg/reconciler/v1alpha1/broker/broker_test.go new file mode 100644 index 00000000000..42d4fa7958f --- /dev/null +++ b/pkg/reconciler/v1alpha1/broker/broker_test.go @@ -0,0 +1,717 @@ +/* +Copyright 2019 The Knative 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 broker + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/knative/eventing/pkg/utils" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + controllertesting "github.com/knative/eventing/pkg/reconciler/testing" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker/resources" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + testNS = "test-namespace" + brokerName = "test-broker" + + filterImage = "filter-image" + filterSA = "filter-SA" + ingressImage = "ingress-image" + ingressSA = "ingress-SA" +) + +var ( + trueVal = true + + channelProvisioner = &corev1.ObjectReference{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "ClusterChannelProvisioner", + Name: "my-provisioner", + } + + channelHostname = fmt.Sprintf("foo.bar.svc.%s", utils.GetClusterDomainName()) + + // deletionTime is used when objects are marked as deleted. Rfc3339Copy() + // truncates to seconds to match the loss of precision during serialization. + deletionTime = metav1.Now().Rfc3339Copy() +) + +func init() { + // Add types to scheme + _ = v1alpha1.AddToScheme(scheme.Scheme) +} + +func TestProvideController(t *testing.T) { + // TODO(grantr) This needs a mock of manager.Manager. Creating a manager + // with a fake Config fails because the Manager tries to contact the + // apiserver. + + // cfg := &rest.Config{ + // Host: "http://foo:80", + // } + // + // mgr, err := manager.New(cfg, manager.Options{}) + // if err != nil { + // t.Fatalf("Error creating manager: %v", err) + // } + // + // _, err = ProvideController(mgr) + // if err != nil { + // t.Fatalf("Error in ProvideController: %v", err) + // } +} + +func TestInjectClient(t *testing.T) { + r := &reconciler{} + orig := r.client + n := fake.NewFakeClient() + if orig == n { + t.Errorf("Original and new clients are identical: %v", orig) + } + err := r.InjectClient(n) + if err != nil { + t.Errorf("Unexpected error injecting the client: %v", err) + } + if n != r.client { + t.Errorf("Unexpected client. Expected: '%v'. Actual: '%v'", n, r.client) + } +} + +func TestReconcile(t *testing.T) { + testCases := []controllertesting.TestCase{ + { + Name: "Broker not found", + }, + { + Name: "Broker.Get fails", + Scheme: scheme.Scheme, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error getting the Broker") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting the Broker", + }, + { + Name: "Broker is being deleted", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeDeletingBroker(), + }, + WantEvent: []corev1.Event{ + { + Reason: brokerReconciled, Type: corev1.EventTypeNormal, + }, + }, + }, + { + Name: "Channel.List error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + }, + Mocks: controllertesting.Mocks{ + MockLists: []controllertesting.MockList{ + func(_ client.Client, _ context.Context, _ *client.ListOptions, list runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := list.(*v1alpha1.ChannelList); ok { + return controllertesting.Handled, errors.New("test error listing channels") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error listing channels", + }, + { + Name: "Channel.Create error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Channel); ok { + return controllertesting.Handled, errors.New("test error creating Channel") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating Channel", + }, + { + Name: "Channel is different than expected", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeDifferentChannel(), + }, + WantPresent: []runtime.Object{ + // This is special because the Channel is not updated, unlike most things that + // differ from expected. + // TODO uncomment the following line once our test framework supports searching for + // GenerateName. + // makeDifferentChannel(), + }, + WantEvent: []corev1.Event{ + { + Reason: brokerReconciled, Type: corev1.EventTypeNormal, + }, + }, + }, + { + Name: "Channel is not yet Addressable", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeNonAddressableChannel(), + }, + WantResult: reconcile.Result{RequeueAfter: time.Second}, + }, + { + Name: "Filter Deployment.Get error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, key client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*appsv1.Deployment); ok { + if strings.Contains(key.Name, "filter") { + return controllertesting.Handled, errors.New("test error getting filter Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting filter Deployment", + }, + { + Name: "Filter Deployment.Create error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if d, ok := obj.(*appsv1.Deployment); ok { + if d.Labels["eventing.knative.dev/brokerRole"] == "filter" { + return controllertesting.Handled, errors.New("test error creating filter Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating filter Deployment", + }, + { + Name: "Filter Deployment.Update error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + makeDifferentFilterDeployment(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if d, ok := obj.(*appsv1.Deployment); ok { + if d.Labels["eventing.knative.dev/brokerRole"] == "filter" { + return controllertesting.Handled, errors.New("test error updating filter Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating filter Deployment", + }, + { + Name: "Filter Service.Get error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, key client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*corev1.Service); ok { + if strings.Contains(key.Name, "filter") { + return controllertesting.Handled, errors.New("test error getting filter Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting filter Service", + }, + { + Name: "Filter Service.Create error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if svc, ok := obj.(*corev1.Service); ok { + if svc.Labels["eventing.knative.dev/brokerRole"] == "filter" { + return controllertesting.Handled, errors.New("test error creating filter Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating filter Service", + }, + { + Name: "Filter Service.Update error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + makeDifferentFilterService(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if svc, ok := obj.(*corev1.Service); ok { + if svc.Labels["eventing.knative.dev/brokerRole"] == "filter" { + return controllertesting.Handled, errors.New("test error updating filter Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating filter Service", + }, + { + Name: "Ingress Deployment.Get error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, key client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*appsv1.Deployment); ok { + if strings.Contains(key.Name, "ingress") { + return controllertesting.Handled, errors.New("test error getting ingress Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting ingress Deployment", + }, + { + Name: "Ingress Deployment.Create error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if d, ok := obj.(*appsv1.Deployment); ok { + if d.Labels["eventing.knative.dev/brokerRole"] == "ingress" { + return controllertesting.Handled, errors.New("test error creating ingress Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating ingress Deployment", + }, + { + Name: "Ingress Deployment.Update error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + makeDifferentIngressDeployment(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if d, ok := obj.(*appsv1.Deployment); ok { + if d.Labels["eventing.knative.dev/brokerRole"] == "ingress" { + return controllertesting.Handled, errors.New("test error updating ingress Deployment") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating ingress Deployment", + }, + { + Name: "Ingress Service.Get error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, key client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*corev1.Service); ok { + if key.Name == fmt.Sprintf("%s-broker", brokerName) { + return controllertesting.Handled, errors.New("test error getting ingress Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting ingress Service", + }, + { + Name: "Ingress Service.Create error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if svc, ok := obj.(*corev1.Service); ok { + if svc.Labels["eventing.knative.dev/brokerRole"] == "ingress" { + return controllertesting.Handled, errors.New("test error creating ingress Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating ingress Service", + }, + { + Name: "Ingress Service.Update error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + makeDifferentIngressService(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if svc, ok := obj.(*corev1.Service); ok { + if svc.Labels["eventing.knative.dev/brokerRole"] == "ingress" { + return controllertesting.Handled, errors.New("test error updating ingress Service") + } + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating ingress Service", + }, + { + Name: "Broker.Get for status update fails", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + // The first Get works. + func(innerClient client.Client, ctx context.Context, key client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, innerClient.Get(ctx, key, obj) + } + return controllertesting.Unhandled, nil + }, + // The second Get fails. + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error getting the Broker for status update") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting the Broker for status update", + WantEvent: []corev1.Event{ + { + Reason: brokerReconciled, Type: corev1.EventTypeNormal, + }, + { + Reason: brokerUpdateStatusFailed, Type: corev1.EventTypeWarning, + }, + }, + }, + { + Name: "Broker.Status.Update error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + makeChannel(), + }, + Mocks: controllertesting.Mocks{ + MockStatusUpdates: []controllertesting.MockStatusUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error updating the Broker status") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating the Broker status", + WantEvent: []corev1.Event{ + { + Reason: brokerReconciled, Type: corev1.EventTypeNormal, + }, + { + Reason: brokerUpdateStatusFailed, Type: corev1.EventTypeWarning, + }, + }, + }, + { + Name: "Successful reconcile", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeBroker(), + // The Channel needs to be addressable for the reconcile to succeed. + makeChannel(), + }, + WantPresent: []runtime.Object{ + makeReadyBroker(), + // TODO Uncomment makeChannel() when our test framework handles generateName. + // makeChannel(), + makeFilterDeployment(), + makeFilterService(), + makeIngressDeployment(), + makeIngressService(), + }, + WantEvent: []corev1.Event{ + { + Reason: brokerReconciled, Type: corev1.EventTypeNormal, + }, + }, + }, + } + for _, tc := range testCases { + c := tc.GetClient() + recorder := tc.GetEventRecorder() + + r := &reconciler{ + client: c, + recorder: recorder, + logger: zap.NewNop(), + + filterImage: filterImage, + filterServiceAccountName: filterSA, + ingressImage: ingressImage, + ingressServiceAccountName: ingressSA, + } + tc.ReconcileKey = fmt.Sprintf("%s/%s", testNS, brokerName) + tc.IgnoreTimes = true + t.Run(tc.Name, tc.Runner(t, r, c, recorder)) + } +} + +func makeBroker() *v1alpha1.Broker { + return &v1alpha1.Broker{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "Broker", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + Name: brokerName, + }, + Spec: v1alpha1.BrokerSpec{ + ChannelTemplate: &v1alpha1.ChannelSpec{ + Provisioner: channelProvisioner, + }, + }, + } +} + +func makeReadyBroker() *v1alpha1.Broker { + b := makeBroker() + b.Status.InitializeConditions() + b.Status.MarkChannelReady() + b.Status.SetAddress(fmt.Sprintf("%s-broker.%s.svc.%s", brokerName, testNS, utils.GetClusterDomainName())) + b.Status.MarkFilterReady() + b.Status.MarkIngressReady() + return b +} + +func makeDeletingBroker() *v1alpha1.Broker { + b := makeReadyBroker() + b.DeletionTimestamp = &deletionTime + return b +} + +func makeChannel() *v1alpha1.Channel { + return &v1alpha1.Channel{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + GenerateName: fmt.Sprintf("%s-broker-", brokerName), + Labels: map[string]string{ + "eventing.knative.dev/broker": brokerName, + "eventing.knative.dev/brokerEverything": "true", + }, + OwnerReferences: []metav1.OwnerReference{ + getOwnerReference(), + }, + }, + Spec: v1alpha1.ChannelSpec{ + Provisioner: channelProvisioner, + }, + Status: v1alpha1.ChannelStatus{ + Address: duckv1alpha1.Addressable{ + Hostname: channelHostname, + }, + }, + } +} + +func makeNonAddressableChannel() *v1alpha1.Channel { + c := makeChannel() + c.Status.Address = duckv1alpha1.Addressable{} + return c +} + +func makeDifferentChannel() *v1alpha1.Channel { + c := makeChannel() + c.Spec.Provisioner.Name = "some-other-provisioner" + return c +} + +func makeFilterDeployment() *appsv1.Deployment { + d := resources.MakeFilterDeployment(&resources.FilterArgs{ + Broker: makeBroker(), + Image: filterImage, + ServiceAccountName: filterSA, + }) + d.TypeMeta = metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + } + return d +} + +func makeDifferentFilterDeployment() *appsv1.Deployment { + d := makeFilterDeployment() + d.Spec.Template.Spec.Containers[0].Image = "some-other-image" + return d +} + +func makeFilterService() *corev1.Service { + svc := resources.MakeFilterService(makeBroker()) + svc.TypeMeta = metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + } + return svc +} + +func makeDifferentFilterService() *corev1.Service { + s := makeFilterService() + s.Spec.Selector["eventing.knative.dev/broker"] = "some-other-value" + return s +} + +func makeIngressDeployment() *appsv1.Deployment { + d := resources.MakeIngress(&resources.IngressArgs{ + Broker: makeBroker(), + Image: ingressImage, + ServiceAccountName: ingressSA, + ChannelAddress: channelHostname, + }) + d.TypeMeta = metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + } + return d +} + +func makeDifferentIngressDeployment() *appsv1.Deployment { + d := makeIngressDeployment() + d.Spec.Template.Spec.Containers[0].Image = "some-other-image" + return d +} + +func makeIngressService() *corev1.Service { + svc := resources.MakeIngressService(makeBroker()) + svc.TypeMeta = metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + } + return svc +} + +func makeDifferentIngressService() *corev1.Service { + s := makeIngressService() + s.Spec.Selector["eventing.knative.dev/broker"] = "some-other-value" + return s +} + +func getOwnerReference() metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "Broker", + Name: brokerName, + Controller: &trueVal, + BlockOwnerDeletion: &trueVal, + } +} diff --git a/pkg/reconciler/v1alpha1/broker/resources/filter.go b/pkg/reconciler/v1alpha1/broker/resources/filter.go new file mode 100644 index 00000000000..f51847c90a8 --- /dev/null +++ b/pkg/reconciler/v1alpha1/broker/resources/filter.go @@ -0,0 +1,118 @@ +/* +Copyright 2019 The Knative 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 resources + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type FilterArgs struct { + Broker *eventingv1alpha1.Broker + Image string + ServiceAccountName string +} + +func MakeFilterDeployment(args *FilterArgs) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: args.Broker.Namespace, + Name: fmt.Sprintf("%s-broker-filter", args.Broker.Name), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(args.Broker, schema.GroupVersionKind{ + Group: eventingv1alpha1.SchemeGroupVersion.Group, + Version: eventingv1alpha1.SchemeGroupVersion.Version, + Kind: "Broker", + }), + }, + Labels: filterLabels(args.Broker), + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: filterLabels(args.Broker), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: filterLabels(args.Broker), + Annotations: map[string]string{ + "sidecar.istio.io/inject": "true", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: args.ServiceAccountName, + Containers: []corev1.Container{ + { + Name: "filter", + Image: args.Image, + Env: []corev1.EnvVar{ + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func MakeFilterService(b *eventingv1alpha1.Broker) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.Namespace, + Name: fmt.Sprintf("%s-broker-filter", b.Name), + Labels: filterLabels(b), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(b, schema.GroupVersionKind{ + Group: eventingv1alpha1.SchemeGroupVersion.Group, + Version: eventingv1alpha1.SchemeGroupVersion.Version, + Kind: "Broker", + }), + }, + }, + Spec: corev1.ServiceSpec{ + Selector: filterLabels(b), + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + } +} + +func filterLabels(b *eventingv1alpha1.Broker) map[string]string { + return map[string]string{ + "eventing.knative.dev/broker": b.Name, + "eventing.knative.dev/brokerRole": "filter", + } +} diff --git a/pkg/reconciler/v1alpha1/broker/resources/ingress.go b/pkg/reconciler/v1alpha1/broker/resources/ingress.go new file mode 100644 index 00000000000..1ebf8957cec --- /dev/null +++ b/pkg/reconciler/v1alpha1/broker/resources/ingress.go @@ -0,0 +1,119 @@ +/* +Copyright 2019 The Knative 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 resources + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type IngressArgs struct { + Broker *eventingv1alpha1.Broker + Image string + ServiceAccountName string + ChannelAddress string +} + +func MakeIngress(args *IngressArgs) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: args.Broker.Namespace, + Name: fmt.Sprintf("%s-broker-ingress", args.Broker.Name), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(args.Broker, schema.GroupVersionKind{ + Group: eventingv1alpha1.SchemeGroupVersion.Group, + Version: eventingv1alpha1.SchemeGroupVersion.Version, + Kind: "Broker", + }), + }, + Labels: ingressLabels(args.Broker), + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: ingressLabels(args.Broker), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ingressLabels(args.Broker), + Annotations: map[string]string{ + "sidecar.istio.io/inject": "true", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: args.ServiceAccountName, + Containers: []corev1.Container{ + { + Image: args.Image, + Name: "ingress", + Env: []corev1.EnvVar{ + { + Name: "FILTER", + Value: "", // TODO Add one. + }, + { + Name: "CHANNEL", + Value: args.ChannelAddress, + }, + }, + }, + }, + }, + }, + }, + } +} + +func MakeIngressService(b *eventingv1alpha1.Broker) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: b.Namespace, + Name: fmt.Sprintf("%s-broker", b.Name), + Labels: ingressLabels(b), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(b, schema.GroupVersionKind{ + Group: eventingv1alpha1.SchemeGroupVersion.Group, + Version: eventingv1alpha1.SchemeGroupVersion.Version, + Kind: "Broker", + }), + }, + }, + Spec: corev1.ServiceSpec{ + Selector: ingressLabels(b), + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + } +} + +func ingressLabels(b *eventingv1alpha1.Broker) map[string]string { + return map[string]string{ + "eventing.knative.dev/broker": b.Name, + "eventing.knative.dev/brokerRole": "ingress", + } +} diff --git a/pkg/reconciler/v1alpha1/namespace/namespace.go b/pkg/reconciler/v1alpha1/namespace/namespace.go new file mode 100644 index 00000000000..8e065de6b93 --- /dev/null +++ b/pkg/reconciler/v1alpha1/namespace/namespace.go @@ -0,0 +1,354 @@ +/* +Copyright 2019 The Knative 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 namespace + +import ( + "context" + "fmt" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/logging" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "knative-eventing-namespace-controller" + + // Label to enable knative-eventing in a namespace. + knativeEventingLabelKey = "knative-eventing-injection" + knativeEventingLabelValue = "enabled" + + defaultBroker = "default" + brokerFilterSA = "eventing-broker-filter" + brokerFilterRB = "eventing-broker-filter" + brokerFilterClusterRole = "eventing-broker-filter" + + // Name of the corev1.Events emitted from the reconciliation process. + brokerCreated = "BrokerCreated" + serviceAccountCreated = "BrokerFilterServiceAccountCreated" + serviceAccountRBACCreated = "BrokerFilterServiceAccountRBACCreated" +) + +type reconciler struct { + client client.Client + recorder record.EventRecorder + + logger *zap.Logger +} + +// Verify the struct implements reconcile.Reconciler +var _ reconcile.Reconciler = &reconciler{} + +// ProvideController returns a function that returns a Namespace controller. +func ProvideController(mgr manager.Manager, logger *zap.Logger) (controller.Controller, error) { + // Setup a new controller to Reconcile Namespaces. + r := &reconciler{ + recorder: mgr.GetRecorder(controllerAgentName), + logger: logger, + } + c, err := controller.New(controllerAgentName, mgr, controller.Options{ + Reconciler: r, + }) + if err != nil { + return nil, err + } + + // Watch Namespaces. + if err = c.Watch(&source.Kind{Type: &v1.Namespace{}}, &handler.EnqueueRequestForObject{}); err != nil { + return nil, err + } + + // Watch all the resources that this reconciler reconciles. This is a map from resource type to + // the name of the resource of that type we care about (i.e. only if the resource of the given + // type and with the given name changes, do we reconcile the Namespace). + resources := map[runtime.Object]string{ + &corev1.ServiceAccount{}: brokerFilterSA, + &rbacv1.RoleBinding{}: brokerFilterRB, + &v1alpha1.Broker{}: defaultBroker, + } + for t, n := range resources { + nm := &namespaceMapper{ + name: n, + } + err = c.Watch(&source.Kind{Type: t}, &handler.EnqueueRequestsFromMapFunc{ToRequests: nm}) + if err != nil { + return nil, err + } + } + + return c, nil +} + +type namespaceMapper struct { + name string +} + +var _ handler.Mapper = &namespaceMapper{} + +func (m *namespaceMapper) Map(o handler.MapObject) []reconcile.Request { + if o.Meta.GetName() == m.name { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "", + Name: o.Meta.GetNamespace(), + }, + }, + } + } + return []reconcile.Request{} +} + +func (r *reconciler) InjectClient(c client.Client) error { + r.client = c + return nil +} + +// Reconcile compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the Namespace resource +// with the current status of the resource. +func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + ctx := context.TODO() + ctx = logging.WithLogger(ctx, r.logger.With(zap.Any("request", request))) + + ns := &corev1.Namespace{} + err := r.client.Get(ctx, request.NamespacedName, ns) + + if errors.IsNotFound(err) { + logging.FromContext(ctx).Info("Could not find Namespace") + return reconcile.Result{}, nil + } + + if err != nil { + logging.FromContext(ctx).Error("Could not Get Namespace", zap.Error(err)) + return reconcile.Result{}, err + } + + if ns.Labels[knativeEventingLabelKey] != knativeEventingLabelValue { + logging.FromContext(ctx).Debug("Not reconciling Namespace") + return reconcile.Result{}, nil + } + + // Reconcile this copy of the Namespace and then write back any status updates regardless of + // whether the reconcile error out. + reconcileErr := r.reconcile(ctx, ns) + if reconcileErr != nil { + logging.FromContext(ctx).Error("Error reconciling Namespace", zap.Error(reconcileErr)) + } else { + logging.FromContext(ctx).Debug("Namespace reconciled") + } + + // Requeue if the resource is not ready: + return reconcile.Result{}, reconcileErr +} + +func (r *reconciler) reconcile(ctx context.Context, ns *corev1.Namespace) error { + // No need for a finalizer, because everything reconciled is created inside the Namespace. If + // the Namespace is being deleted, then all the reconciled objects will be too. + + if ns.DeletionTimestamp != nil { + return nil + } + + sa, err := r.reconcileBrokerFilterServiceAccount(ctx, ns) + if err != nil { + logging.FromContext(ctx).Error("Unable to reconcile the Broker Filter Service Account for the namespace", zap.Error(err)) + return err + } + _, err = r.reconcileBrokerFilterRBAC(ctx, ns, sa) + if err != nil { + logging.FromContext(ctx).Error("Unable to reconcile the Broker Filter Service Account RBAC for the namespace", zap.Error(err)) + return err + } + _, err = r.reconcileBroker(ctx, ns) + if err != nil { + logging.FromContext(ctx).Error("Unable to reconcile Broker for the namespace", zap.Error(err)) + return err + } + + return nil +} + +// reconcileBrokerFilterServiceAccount reconciles the Broker's filter service account for Namespace 'ns'. +func (r *reconciler) reconcileBrokerFilterServiceAccount(ctx context.Context, ns *corev1.Namespace) (*corev1.ServiceAccount, error) { + current, err := r.getBrokerFilterServiceAccount(ctx, ns) + + // If the resource doesn't exist, we'll create it. + if k8serrors.IsNotFound(err) { + sa := newBrokerFilterServiceAccount(ns) + err = r.client.Create(ctx, sa) + if err != nil { + return nil, err + } + r.recorder.Event(ns, + corev1.EventTypeNormal, + serviceAccountCreated, + fmt.Sprintf("Service account created for the Broker '%s'", sa.Name)) + return sa, nil + } else if err != nil { + return nil, err + } + // Don't update anything that is already present. + return current, nil +} + +// getBrokerFilterServiceAccount returns the Broker's filter service account for Namespace 'ns' if exists, +// otherwise it returns an error. +func (r *reconciler) getBrokerFilterServiceAccount(ctx context.Context, ns *corev1.Namespace) (*corev1.ServiceAccount, error) { + sa := &corev1.ServiceAccount{} + name := types.NamespacedName{ + Namespace: ns.Name, + Name: brokerFilterSA, + } + err := r.client.Get(ctx, name, sa) + return sa, err +} + +// newBrokerFilterServiceAccount creates a ServiceAccount object for the Namespace 'ns'. +func newBrokerFilterServiceAccount(ns *corev1.Namespace) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: brokerFilterSA, + Labels: injectedLabels(), + }, + } +} + +func injectedLabels() map[string]string { + return map[string]string{ + "eventing.knative.dev/namespaceInjected": "true", + } +} + +// reconcileBrokerFilterRBAC reconciles the Broker's filter service account RBAC for the Namespace 'ns'. +func (r *reconciler) reconcileBrokerFilterRBAC(ctx context.Context, ns *corev1.Namespace, sa *corev1.ServiceAccount) (*rbacv1.RoleBinding, error) { + current, err := r.getBrokerFilterRBAC(ctx, ns) + + // If the resource doesn't exist, we'll create it. + if k8serrors.IsNotFound(err) { + rbac := newBrokerFilterRBAC(ns, sa) + err = r.client.Create(ctx, rbac) + if err != nil { + return nil, err + } + r.recorder.Event(ns, + corev1.EventTypeNormal, + serviceAccountRBACCreated, + fmt.Sprintf("Service account RBAC created for the Broker Filter '%s'", rbac.Name)) + return rbac, nil + } else if err != nil { + return nil, err + } + // Don't update anything that is already present. + return current, nil +} + +// getBrokerFilterRBAC returns the Broker's filter role binding for Namespace 'ns' if exists, +// otherwise it returns an error. +func (r *reconciler) getBrokerFilterRBAC(ctx context.Context, ns *corev1.Namespace) (*rbacv1.RoleBinding, error) { + rb := &rbacv1.RoleBinding{} + name := types.NamespacedName{ + Namespace: ns.Name, + Name: brokerFilterRB, + } + err := r.client.Get(ctx, name, rb) + return rb, err +} + +// newBrokerFilterRBAC creates a RpleBinding object for the Broker's filter service account 'sa' in the Namespace 'ns'. +func newBrokerFilterRBAC(ns *corev1.Namespace, sa *corev1.ServiceAccount) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: brokerFilterRB, + Labels: injectedLabels(), + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: brokerFilterClusterRole, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Namespace: ns.Name, + Name: sa.Name, + }, + }, + } +} + +// getBroker returns the default broker for Namespace 'ns' if it exists, otherwise it returns an +// error. +func (r *reconciler) getBroker(ctx context.Context, ns *corev1.Namespace) (*v1alpha1.Broker, error) { + b := &v1alpha1.Broker{} + name := types.NamespacedName{ + Namespace: ns.Name, + Name: defaultBroker, + } + err := r.client.Get(ctx, name, b) + return b, err +} + +// reconcileBroker reconciles the default Broker for the Namespace 'ns'. +func (r *reconciler) reconcileBroker(ctx context.Context, ns *corev1.Namespace) (*v1alpha1.Broker, error) { + current, err := r.getBroker(ctx, ns) + + // If the resource doesn't exist, we'll create it. + if k8serrors.IsNotFound(err) { + b := newBroker(ns) + err = r.client.Create(ctx, b) + if err != nil { + return nil, err + } + r.recorder.Event(ns, corev1.EventTypeNormal, brokerCreated, "Default eventing.knative.dev Broker created.") + return b, nil + } else if err != nil { + return nil, err + } + // Don't update anything that is already present. + return current, nil +} + +// newBroker creates a placeholder default Broker object for Namespace 'ns'. +func newBroker(ns *corev1.Namespace) *v1alpha1.Broker { + return &v1alpha1.Broker{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: defaultBroker, + Labels: injectedLabels(), + }, + } +} diff --git a/pkg/reconciler/v1alpha1/namespace/namespace_test.go b/pkg/reconciler/v1alpha1/namespace/namespace_test.go new file mode 100644 index 00000000000..be60e4a9828 --- /dev/null +++ b/pkg/reconciler/v1alpha1/namespace/namespace_test.go @@ -0,0 +1,293 @@ +/* +Copyright 2019 The Knative 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 namespace + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + controllertesting "github.com/knative/eventing/pkg/reconciler/testing" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + testNS = "test-namespace" + brokerName = "default" +) + +var ( + disabled = "disabled" + enabled = "enabled" + + // deletionTime is used when objects are marked as deleted. Rfc3339Copy() + // truncates to seconds to match the loss of precision during serialization. + deletionTime = metav1.Now().Rfc3339Copy() + + // map of events to set test cases' expectations easier + events = map[string]corev1.Event{ + brokerCreated: {Reason: brokerCreated, Type: corev1.EventTypeNormal}, + serviceAccountCreated: {Reason: serviceAccountCreated, Type: corev1.EventTypeNormal}, + serviceAccountRBACCreated: {Reason: serviceAccountRBACCreated, Type: corev1.EventTypeNormal}, + } +) + +func init() { + // Add types to scheme + _ = v1alpha1.AddToScheme(scheme.Scheme) +} + +func TestProvideController(t *testing.T) { + // TODO(grantr) This needs a mock of manager.Manager. Creating a manager + // with a fake Config fails because the Manager tries to contact the + // apiserver. + + // cfg := &rest.Config{ + // Host: "http://foo:80", + // } + // + // mgr, err := manager.New(cfg, manager.Options{}) + // if err != nil { + // t.Fatalf("Error creating manager: %v", err) + // } + // + // _, err = ProvideController(mgr) + // if err != nil { + // t.Fatalf("Error in ProvideController: %v", err) + // } +} + +func TestInjectClient(t *testing.T) { + r := &reconciler{} + orig := r.client + n := fake.NewFakeClient() + if orig == n { + t.Errorf("Original and new clients are identical: %v", orig) + } + err := r.InjectClient(n) + if err != nil { + t.Errorf("Unexpected error injecting the client: %v", err) + } + if n != r.client { + t.Errorf("Unexpected client. Expected: '%v'. Actual: '%v'", n, r.client) + } +} + +func TestNamespaceMapper_Map(t *testing.T) { + m := &namespaceMapper{ + name: makeBroker().Name, + } + + req := handler.MapObject{ + Meta: makeBroker().GetObjectMeta(), + Object: makeBroker(), + } + actual := m.Map(req) + expected := []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: "", + Name: testNS, + }, + }, + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("Unexpected reconcile requests (-want +got): %v", diff) + } +} + +func TestReconcile(t *testing.T) { + testCases := []controllertesting.TestCase{ + { + Name: "Namespace not found", + }, + { + Name: "Namespace.Get fails", + Scheme: scheme.Scheme, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*corev1.Namespace); ok { + return controllertesting.Handled, errors.New("test error getting the NS") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting the NS", + }, + { + Name: "Namespace is not labeled", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(nil), + }, + WantAbsent: []runtime.Object{ + makeBroker(), + }, + }, + { + Name: "Namespace is labeled disabled", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(&disabled), + }, + WantAbsent: []runtime.Object{ + makeBroker(), + }, + }, + { + Name: "Namespace is being deleted", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeDeletingNamespace(), + }, + WantAbsent: []runtime.Object{ + makeBroker(), + }, + }, + { + Name: "Broker.Get fails", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(&enabled), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error getting the Broker") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting the Broker", + WantAbsent: []runtime.Object{ + makeBroker(), + }, + WantEvent: []corev1.Event{events[serviceAccountCreated], events[serviceAccountRBACCreated]}, + }, + { + Name: "Broker Found", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(&enabled), + makeBroker(), + }, + WantEvent: []corev1.Event{events[serviceAccountCreated], events[serviceAccountRBACCreated]}, + }, + { + Name: "Broker.Create fails", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(&enabled), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error creating the Broker") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating the Broker", + WantEvent: []corev1.Event{events[serviceAccountCreated], events[serviceAccountRBACCreated]}, + }, + { + Name: "Broker created", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeNamespace(&enabled), + }, + WantPresent: []runtime.Object{ + makeBroker(), + }, + WantEvent: []corev1.Event{ + events[serviceAccountCreated], + events[serviceAccountRBACCreated], + events[brokerCreated]}, + }, + } + for _, tc := range testCases { + c := tc.GetClient() + recorder := tc.GetEventRecorder() + + r := &reconciler{ + client: c, + recorder: recorder, + logger: zap.NewNop(), + } + tc.ReconcileKey = fmt.Sprintf("%s/%s", "", testNS) + tc.IgnoreTimes = true + t.Run(tc.Name, tc.Runner(t, r, c, recorder)) + } +} + +func makeNamespace(labelValue *string) *corev1.Namespace { + labels := map[string]string{} + if labelValue != nil { + labels["knative-eventing-injection"] = *labelValue + } + + return &corev1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: labels, + }, + } +} + +func makeDeletingNamespace() *corev1.Namespace { + ns := makeNamespace(&enabled) + ns.DeletionTimestamp = &deletionTime + return ns +} + +func makeBroker() *v1alpha1.Broker { + return &v1alpha1.Broker{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "Broker", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + Name: brokerName, + Labels: map[string]string{ + "eventing.knative.dev/namespaceInjected": "true", + }, + }, + } +} diff --git a/pkg/reconciler/v1alpha1/trigger/trigger.go b/pkg/reconciler/v1alpha1/trigger/trigger.go new file mode 100644 index 00000000000..5d3f4b71012 --- /dev/null +++ b/pkg/reconciler/v1alpha1/trigger/trigger.go @@ -0,0 +1,633 @@ +/* +Copyright 2018 The Knative 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 trigger + +import ( + "context" + "fmt" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/logging" + "github.com/knative/eventing/pkg/reconciler/names" + "github.com/knative/eventing/pkg/reconciler/v1alpha1/broker" + "github.com/knative/eventing/pkg/utils" + "github.com/knative/eventing/pkg/utils/resolve" + istiov1alpha3 "github.com/knative/pkg/apis/istio/v1alpha3" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +const ( + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "trigger-controller" + + // Name of the corev1.Events emitted from the reconciliation process. + triggerReconciled = "TriggerReconciled" + triggerReconcileFailed = "TriggerReconcileFailed" + triggerUpdateStatusFailed = "TriggerUpdateStatusFailed" + subscriptionDeleteFailed = "SubscriptionDeleteFailed" + subscriptionCreateFailed = "SubscriptionCreateFailed" +) + +type reconciler struct { + client client.Client + dynamicClient dynamic.Interface + recorder record.EventRecorder + + logger *zap.Logger +} + +// Verify the struct implements reconcile.Reconciler. +var _ reconcile.Reconciler = &reconciler{} + +// ProvideController returns a function that returns a Trigger controller. +func ProvideController(mgr manager.Manager, logger *zap.Logger) (controller.Controller, error) { + // Setup a new controller to Reconcile Triggers. + r := &reconciler{ + recorder: mgr.GetRecorder(controllerAgentName), + logger: logger, + } + c, err := controller.New(controllerAgentName, mgr, controller.Options{ + Reconciler: r, + }) + if err != nil { + return nil, err + } + + // Watch Triggers. + if err = c.Watch(&source.Kind{Type: &v1alpha1.Trigger{}}, &handler.EnqueueRequestForObject{}); err != nil { + return nil, err + } + + // Watch all the resources that the Trigger reconciles. + for _, t := range []runtime.Object{&corev1.Service{}, &istiov1alpha3.VirtualService{}, &v1alpha1.Subscription{}} { + err = c.Watch(&source.Kind{Type: t}, &handler.EnqueueRequestForOwner{OwnerType: &v1alpha1.Trigger{}, IsController: true}) + if err != nil { + return nil, err + } + } + + // Watch for Broker changes. E.g. if the Broker is deleted and recreated, we need to reconcile + // the Trigger again. + if err = c.Watch(&source.Kind{Type: &v1alpha1.Broker{}}, &handler.EnqueueRequestsFromMapFunc{ToRequests: &mapBrokerToTriggers{r: r}}); err != nil { + return nil, err + } + + // TODO reconcile after a change to the subscriber. I'm not sure how this is possible, but we should do it if we + // can find a way. + + return c, nil +} + +// mapBrokerToTriggers maps Broker changes to all the Triggers that correspond to that Broker. +type mapBrokerToTriggers struct { + r *reconciler +} + +func (b *mapBrokerToTriggers) Map(o handler.MapObject) []reconcile.Request { + ctx := context.Background() + triggers := make([]reconcile.Request, 0) + + opts := &client.ListOptions{ + Namespace: o.Meta.GetNamespace(), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + for { + tl := &v1alpha1.TriggerList{} + if err := b.r.client.List(ctx, opts, tl); err != nil { + b.r.logger.Error("Error listing Triggers when Broker changed. Some Triggers may not be reconciled.", zap.Error(err), zap.Any("broker", o)) + return triggers + } + + for _, t := range tl.Items { + if t.Spec.Broker == o.Meta.GetName() { + triggers = append(triggers, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: t.Namespace, + Name: t.Name, + }, + }) + } + } + if tl.Continue != "" { + opts.Raw.Continue = tl.Continue + } else { + return triggers + } + } +} + +func (r *reconciler) InjectClient(c client.Client) error { + r.client = c + return nil +} + +func (r *reconciler) InjectConfig(c *rest.Config) error { + var err error + r.dynamicClient, err = dynamic.NewForConfig(c) + return err +} + +// Reconcile compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the Trigger resource +// with the current status of the resource. +func (r *reconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + ctx := context.TODO() + ctx = logging.WithLogger(ctx, r.logger.With(zap.Any("request", request))) + + trigger := &v1alpha1.Trigger{} + err := r.client.Get(ctx, request.NamespacedName, trigger) + + if errors.IsNotFound(err) { + logging.FromContext(ctx).Info("Could not find Trigger") + return reconcile.Result{}, nil + } + + if err != nil { + logging.FromContext(ctx).Error("Could not get Trigger", zap.Error(err)) + return reconcile.Result{}, err + } + + // Reconcile this copy of the Trigger and then write back any status updates regardless of + // whether the reconcile error out. + reconcileErr := r.reconcile(ctx, trigger) + if reconcileErr != nil { + logging.FromContext(ctx).Error("Error reconciling Trigger", zap.Error(reconcileErr)) + r.recorder.Eventf(trigger, corev1.EventTypeWarning, triggerReconcileFailed, "Trigger reconciliation failed: %v", reconcileErr) + } else { + logging.FromContext(ctx).Debug("Trigger reconciled") + r.recorder.Event(trigger, corev1.EventTypeNormal, triggerReconciled, "Trigger reconciled") + } + + if _, err = r.updateStatus(trigger); err != nil { + logging.FromContext(ctx).Error("Failed to update Trigger status", zap.Error(err)) + r.recorder.Eventf(trigger, corev1.EventTypeWarning, triggerUpdateStatusFailed, "Failed to update Trigger's status: %v", err) + return reconcile.Result{}, err + } + + // Requeue if the resource is not ready + return reconcile.Result{}, reconcileErr +} + +func (r *reconciler) reconcile(ctx context.Context, t *v1alpha1.Trigger) error { + t.Status.InitializeConditions() + + // 1. Verify the Broker exists. + // 2. Find the Subscriber's URI. + // 2. Creates a K8s Service uniquely named for this Trigger. + // 3. Creates a VirtualService that routes the K8s Service to the Broker's filter service on an identifiable host name. + // 4. Creates a Subscription from the Broker's single Channel to this Trigger's K8s Service, with reply set to the Broker. + + if t.DeletionTimestamp != nil { + // Everything is cleaned up by the garbage collector. + return nil + } + + b, err := r.getBroker(ctx, t) + if err != nil { + logging.FromContext(ctx).Error("Unable to get the Broker", zap.Error(err)) + t.Status.MarkBrokerDoesNotExist() + return err + } + t.Status.MarkBrokerExists() + + c, err := r.getBrokerChannel(ctx, b) + if err != nil { + logging.FromContext(ctx).Error("Unable to get the Broker's Channel", zap.Error(err)) + return err + } + + subscriberURI, err := resolve.SubscriberSpec(ctx, r.dynamicClient, t.Namespace, t.Spec.Subscriber) + if err != nil { + logging.FromContext(ctx).Error("Unable to get the Subscriber's URI", zap.Error(err)) + return err + } + t.Status.SubscriberURI = subscriberURI + + svc, err := r.reconcileK8sService(ctx, t) + if err != nil { + logging.FromContext(ctx).Error("Unable to reconcile the K8s Service", zap.Error(err)) + return err + } + t.Status.MarkKubernetesServiceExists() + + _, err = r.reconcileVirtualService(ctx, t, svc) + if err != nil { + logging.FromContext(ctx).Error("Unable to reconcile the VirtualService", zap.Error(err)) + return err + } + t.Status.MarkVirtualServiceExists() + + _, err = r.subscribeToBrokerChannel(ctx, t, c, svc) + if err != nil { + logging.FromContext(ctx).Error("Unable to Subscribe", zap.Error(err)) + t.Status.MarkNotSubscribed("notSubscribed", "%v", err) + return err + } + t.Status.MarkSubscribed() + + return nil +} + +// updateStatus may in fact update the trigger's finalizers in addition to the status. +func (r *reconciler) updateStatus(trigger *v1alpha1.Trigger) (*v1alpha1.Trigger, error) { + ctx := context.TODO() + objectKey := client.ObjectKey{Namespace: trigger.Namespace, Name: trigger.Name} + latestTrigger := &v1alpha1.Trigger{} + + if err := r.client.Get(ctx, objectKey, latestTrigger); err != nil { + return nil, err + } + + triggerChanged := false + + if !equality.Semantic.DeepEqual(latestTrigger.Finalizers, trigger.Finalizers) { + latestTrigger.SetFinalizers(trigger.ObjectMeta.Finalizers) + if err := r.client.Update(ctx, latestTrigger); err != nil { + return nil, err + } + triggerChanged = true + } + + if equality.Semantic.DeepEqual(latestTrigger.Status, trigger.Status) { + return latestTrigger, nil + } + + if triggerChanged { + // Refetch + latestTrigger = &v1alpha1.Trigger{} + if err := r.client.Get(ctx, objectKey, latestTrigger); err != nil { + return nil, err + } + } + + latestTrigger.Status = trigger.Status + if err := r.client.Status().Update(ctx, latestTrigger); err != nil { + return nil, err + } + + return latestTrigger, nil +} + +// getBroker returns the Broker for Trigger 't' if exists, otherwise it returns an error. +func (r *reconciler) getBroker(ctx context.Context, t *v1alpha1.Trigger) (*v1alpha1.Broker, error) { + b := &v1alpha1.Broker{} + name := types.NamespacedName{ + Namespace: t.Namespace, + Name: t.Spec.Broker, + } + err := r.client.Get(ctx, name, b) + return b, err +} + +// getBrokerChannel returns the Broker's channel if exists, otherwise it returns an error. +func (r *reconciler) getBrokerChannel(ctx context.Context, b *v1alpha1.Broker) (*v1alpha1.Channel, error) { + list := &v1alpha1.ChannelList{} + opts := &runtimeclient.ListOptions{ + Namespace: b.Namespace, + LabelSelector: labels.SelectorFromSet(broker.ChannelLabels(b)), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + + err := r.client.List(ctx, opts, list) + if err != nil { + return nil, err + } + for _, c := range list.Items { + if metav1.IsControlledBy(&c, b) { + return &c, nil + } + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "") +} + +// getK8sService returns the K8s service for trigger 't' if exists, +// otherwise it returns an error. +func (r *reconciler) getK8sService(ctx context.Context, t *v1alpha1.Trigger) (*corev1.Service, error) { + list := &corev1.ServiceList{} + opts := &runtimeclient.ListOptions{ + Namespace: t.Namespace, + LabelSelector: labels.SelectorFromSet(k8sServiceLabels(t)), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + + err := r.client.List(ctx, opts, list) + if err != nil { + return nil, err + } + for _, svc := range list.Items { + if metav1.IsControlledBy(&svc, t) { + return &svc, nil + } + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "") +} + +// reconcileK8sService reconciles the K8s service for trigger 't'. +func (r *reconciler) reconcileK8sService(ctx context.Context, t *v1alpha1.Trigger) (*corev1.Service, error) { + current, err := r.getK8sService(ctx, t) + + // If the resource doesn't exist, we'll create it + if k8serrors.IsNotFound(err) { + svc := newK8sService(t) + err = r.client.Create(ctx, svc) + if err != nil { + return nil, err + } + return svc, nil + } else if err != nil { + return nil, err + } + + expected := newK8sService(t) + // spec.clusterIP is immutable and is set on existing services. If we don't set this to the same value, we will + // encounter an error while updating. + expected.Spec.ClusterIP = current.Spec.ClusterIP + if !equality.Semantic.DeepDerivative(expected.Spec, current.Spec) { + current.Spec = expected.Spec + err = r.client.Update(ctx, current) + if err != nil { + return nil, err + } + } + return current, nil +} + +// newK8sService returns a K8s placeholder service for trigger 't'. +func newK8sService(t *v1alpha1.Trigger) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: t.Namespace, + GenerateName: fmt.Sprintf("trigger-%s-", t.Name), + Labels: k8sServiceLabels(t), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(t, schema.GroupVersionKind{ + Group: v1alpha1.SchemeGroupVersion.Group, + Version: v1alpha1.SchemeGroupVersion.Version, + Kind: "Trigger", + }), + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + } +} + +func k8sServiceLabels(t *v1alpha1.Trigger) map[string]string { + return map[string]string{ + "eventing.knative.dev/trigger": t.Name, + } +} + +// getVirtualService returns the virtual service for trigger 't' if exists, +// otherwise it returns an error. +func (r *reconciler) getVirtualService(ctx context.Context, t *v1alpha1.Trigger) (*istiov1alpha3.VirtualService, error) { + list := &istiov1alpha3.VirtualServiceList{} + opts := &runtimeclient.ListOptions{ + Namespace: t.Namespace, + LabelSelector: labels.SelectorFromSet(virtualServiceLabels(t)), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + + err := r.client.List(ctx, opts, list) + if err != nil { + return nil, err + } + for _, vs := range list.Items { + if metav1.IsControlledBy(&vs, t) { + return &vs, nil + } + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "") +} + +// reconcileVirtualService reconciles the virtual service for trigger 't' and service 'svc'. +func (r *reconciler) reconcileVirtualService(ctx context.Context, t *v1alpha1.Trigger, svc *corev1.Service) (*istiov1alpha3.VirtualService, error) { + virtualService, err := r.getVirtualService(ctx, t) + + // If the resource doesn't exist, we'll create it + if k8serrors.IsNotFound(err) { + virtualService = newVirtualService(t, svc) + err = r.client.Create(ctx, virtualService) + if err != nil { + return nil, err + } + return virtualService, nil + } else if err != nil { + return nil, err + } + + expected := newVirtualService(t, svc) + if !equality.Semantic.DeepDerivative(expected.Spec, virtualService.Spec) { + virtualService.Spec = expected.Spec + err = r.client.Update(ctx, virtualService) + if err != nil { + return nil, err + } + } + return virtualService, nil +} + +// newVirtualService returns a placeholder virtual service object for trigger 't' and service 'svc'. +func newVirtualService(t *v1alpha1.Trigger, svc *corev1.Service) *istiov1alpha3.VirtualService { + destinationHost := fmt.Sprintf("%s-broker-filter.%s.svc.%s", t.Spec.Broker, t.Namespace, utils.GetClusterDomainName()) + return &istiov1alpha3.VirtualService{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", t.Name), + Namespace: t.Namespace, + Labels: virtualServiceLabels(t), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(t, schema.GroupVersionKind{ + Group: v1alpha1.SchemeGroupVersion.Group, + Version: v1alpha1.SchemeGroupVersion.Version, + Kind: "Trigger", + }), + }, + }, + Spec: istiov1alpha3.VirtualServiceSpec{ + Hosts: []string{ + names.ServiceHostName(svc.Name, svc.Namespace), + }, + Http: []istiov1alpha3.HTTPRoute{{ + Rewrite: &istiov1alpha3.HTTPRewrite{ + Authority: fmt.Sprintf("%s.%s.triggers.%s", t.Name, t.Namespace, utils.GetClusterDomainName()), + }, + Route: []istiov1alpha3.DestinationWeight{{ + Destination: istiov1alpha3.Destination{ + Host: destinationHost, + Port: istiov1alpha3.PortSelector{ + Number: 80, + }, + }}, + }}, + }, + }, + } +} + +func virtualServiceLabels(t *v1alpha1.Trigger) map[string]string { + return map[string]string{ + "eventing.knative.dev/trigger": t.Name, + } +} + +// subscribeToBrokerChannel subscribes service 'svc' to Broker's channel 'c'. +func (r *reconciler) subscribeToBrokerChannel(ctx context.Context, t *v1alpha1.Trigger, c *v1alpha1.Channel, svc *corev1.Service) (*v1alpha1.Subscription, error) { + expected := makeSubscription(t, c, svc) + + sub, err := r.getSubscription(ctx, t) + // If the resource doesn't exist, we'll create it + if k8serrors.IsNotFound(err) { + sub = expected + err = r.client.Create(ctx, sub) + if err != nil { + return nil, err + } + return sub, nil + } else if err != nil { + return nil, err + } + + // Update Subscription if it has changed. Ignore the generation. + expected.Spec.DeprecatedGeneration = sub.Spec.DeprecatedGeneration + if !equality.Semantic.DeepDerivative(expected.Spec, sub.Spec) { + // Given that the backing channel spec is immutable, we cannot just update the subscription. + // We delete it instead, and re-create it. + err = r.client.Delete(ctx, sub) + if err != nil { + logging.FromContext(ctx).Info("Cannot delete subscription", zap.Error(err)) + r.recorder.Eventf(t, corev1.EventTypeWarning, subscriptionDeleteFailed, "Delete Trigger's subscription failed: %v", err) + return nil, err + } + sub = expected + err = r.client.Create(ctx, sub) + if err != nil { + logging.FromContext(ctx).Info("Cannot create subscription", zap.Error(err)) + r.recorder.Eventf(t, corev1.EventTypeWarning, subscriptionCreateFailed, "Create Trigger's subscription failed: %v", err) + return nil, err + } + } + return sub, nil +} + +// getSubscription returns the subscription of trigger 't' if exists, +// otherwise it returns an error. +func (r *reconciler) getSubscription(ctx context.Context, t *v1alpha1.Trigger) (*v1alpha1.Subscription, error) { + list := &v1alpha1.SubscriptionList{} + opts := &runtimeclient.ListOptions{ + Namespace: t.Namespace, + LabelSelector: labels.SelectorFromSet(subscriptionLabels(t)), + // Set Raw because if we need to get more than one page, then we will put the continue token + // into opts.Raw.Continue. + Raw: &metav1.ListOptions{}, + } + + err := r.client.List(ctx, opts, list) + if err != nil { + return nil, err + } + for _, s := range list.Items { + if metav1.IsControlledBy(&s, t) { + return &s, nil + } + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{}, "") +} + +// makeSubscription returns a placeholder subscription for trigger 't', channel 'c', and service 'svc'. +func makeSubscription(t *v1alpha1.Trigger, c *v1alpha1.Channel, svc *corev1.Service) *v1alpha1.Subscription { + return &v1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: t.Namespace, + GenerateName: fmt.Sprintf("%s-%s-", t.Spec.Broker, t.Name), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(t, schema.GroupVersionKind{ + Group: v1alpha1.SchemeGroupVersion.Group, + Version: v1alpha1.SchemeGroupVersion.Version, + Kind: "Trigger", + }), + }, + Labels: subscriptionLabels(t), + }, + Spec: v1alpha1.SubscriptionSpec{ + Channel: corev1.ObjectReference{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "Channel", + Name: c.Name, + }, + Subscriber: &v1alpha1.SubscriberSpec{ + Ref: &corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Service", + Name: svc.Name, + }, + }, + // TODO This pushes directly into the Channel, it should probably point at the Broker ingress instead. + Reply: &v1alpha1.ReplyStrategy{ + Channel: &corev1.ObjectReference{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "Channel", + Name: c.Name, + }, + }, + }, + } +} + +func subscriptionLabels(t *v1alpha1.Trigger) map[string]string { + return map[string]string{ + "eventing.knative.dev/broker": t.Spec.Broker, + "eventing.knative.dev/trigger": t.Name, + } +} diff --git a/pkg/reconciler/v1alpha1/trigger/trigger_test.go b/pkg/reconciler/v1alpha1/trigger/trigger_test.go new file mode 100644 index 00000000000..ea05130025a --- /dev/null +++ b/pkg/reconciler/v1alpha1/trigger/trigger_test.go @@ -0,0 +1,629 @@ +/* +Copyright 2019 The Knative 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 trigger + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/reconciler/names" + controllertesting "github.com/knative/eventing/pkg/reconciler/testing" + "github.com/knative/eventing/pkg/utils" + duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" + istiov1alpha3 "github.com/knative/pkg/apis/istio/v1alpha3" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testNS = "test-namespace" + triggerName = "test-trigger" + brokerName = "test-broker" + + subscriberAPIVersion = "v1" + subscriberKind = "Service" + subscriberName = "subscriberName" +) + +var ( + trueVal = true + // deletionTime is used when objects are marked as deleted. Rfc3339Copy() + // truncates to seconds to match the loss of precision during serialization. + deletionTime = metav1.Now().Rfc3339Copy() + + // Map of events to set test cases' expectations easier. + events = map[string]corev1.Event{ + triggerReconciled: {Reason: triggerReconciled, Type: corev1.EventTypeNormal}, + triggerUpdateStatusFailed: {Reason: triggerUpdateStatusFailed, Type: corev1.EventTypeWarning}, + triggerReconcileFailed: {Reason: triggerReconcileFailed, Type: corev1.EventTypeWarning}, + subscriptionDeleteFailed: {Reason: subscriptionDeleteFailed, Type: corev1.EventTypeWarning}, + subscriptionCreateFailed: {Reason: subscriptionCreateFailed, Type: corev1.EventTypeWarning}, + } +) + +func init() { + // Add types to scheme + _ = v1alpha1.AddToScheme(scheme.Scheme) + _ = istiov1alpha3.AddToScheme(scheme.Scheme) +} + +func TestProvideController(t *testing.T) { + // TODO(grantr) This needs a mock of manager.Manager. Creating a manager + // with a fake Config fails because the Manager tries to contact the + // apiserver. + + // cfg := &rest.Config{ + // Host: "http://foo:80", + // } + // + // mgr, err := manager.New(cfg, manager.Options{}) + // if err != nil { + // t.Fatalf("Error creating manager: %v", err) + // } + // + // _, err = ProvideController(mgr) + // if err != nil { + // t.Fatalf("Error in ProvideController: %v", err) + // } +} + +func TestInjectClient(t *testing.T) { + r := &reconciler{} + orig := r.client + n := fake.NewFakeClient() + if orig == n { + t.Errorf("Original and new clients are identical: %v", orig) + } + err := r.InjectClient(n) + if err != nil { + t.Errorf("Unexpected error injecting the client: %v", err) + } + if n != r.client { + t.Errorf("Unexpected client. Expected: '%v'. Actual: '%v'", n, r.client) + } +} + +func TestInjectConfig(t *testing.T) { + r := &reconciler{} + wantCfg := &rest.Config{ + Host: "http://foo", + } + + err := r.InjectConfig(wantCfg) + if err != nil { + t.Fatalf("Unexpected error injecting the config: %v", err) + } + + wantDynClient, err := dynamic.NewForConfig(wantCfg) + if err != nil { + t.Fatalf("Unexpected error generating dynamic client: %v", err) + } + + // Since dynamicClient doesn't export any fields, we can only test its type. + switch r.dynamicClient.(type) { + case dynamic.Interface: + // ok + default: + t.Errorf("Unexpected dynamicClient type. Expected: %T, Got: %T", wantDynClient, r.dynamicClient) + } +} + +func TestReconcile(t *testing.T) { + testCases := []controllertesting.TestCase{ + { + Name: "Trigger not found", + }, + { + Name: "Get Trigger error", + Scheme: scheme.Scheme, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Trigger); ok { + return controllertesting.Handled, errors.New("test error getting the Trigger") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting the Trigger", + }, + { + Name: "Trigger being deleted", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeDeletingTrigger(), + }, + WantEvent: []corev1.Event{events[triggerReconciled]}, + }, + { + Name: "Get Broker error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + }, + Mocks: controllertesting.Mocks{ + MockGets: []controllertesting.MockGet{ + func(_ client.Client, _ context.Context, _ client.ObjectKey, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Broker); ok { + return controllertesting.Handled, errors.New("test error getting broker") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting broker", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Get Broker channel error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + }, + Mocks: controllertesting.Mocks{ + MockLists: []controllertesting.MockList{ + func(_ client.Client, _ context.Context, _ *client.ListOptions, list runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := list.(*v1alpha1.ChannelList); ok { + return controllertesting.Handled, errors.New("test error getting broker's channel") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error getting broker's channel", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Resolve subscriberURI error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + }, + DynamicMocks: controllertesting.DynamicMocks{ + MockGets: []controllertesting.MockDynamicGet{ + func(ctx *controllertesting.MockDynamicContext, name string, options metav1.GetOptions, subresources ...string) (handled controllertesting.MockHandled, i *unstructured.Unstructured, e error) { + if ctx.Resource.Group == "" && ctx.Resource.Version == "v1" && ctx.Resource.Resource == "services" { + + return controllertesting.Handled, nil, errors.New("test error resolving subscriber URI") + } + return controllertesting.Unhandled, nil, nil + }, + }, + }, + WantErrMsg: "test error resolving subscriber URI", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Create K8s Service error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*corev1.Service); ok { + return controllertesting.Handled, errors.New("test error creating k8s service") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating k8s service", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Update K8s Service error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeDifferentK8sService(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*corev1.Service); ok { + return controllertesting.Handled, errors.New("test error updating k8s service") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating k8s service", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Create Virtual Service error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*istiov1alpha3.VirtualService); ok { + return controllertesting.Handled, errors.New("test error creating virtual service") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating virtual service", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Update Virtual Service error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeDifferentVirtualService(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockUpdates: []controllertesting.MockUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*istiov1alpha3.VirtualService); ok { + return controllertesting.Handled, errors.New("test error updating virtual service") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating virtual service", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Create Subscription error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeVirtualService(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Subscription); ok { + return controllertesting.Handled, errors.New("test error creating subscription") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error creating subscription", + WantEvent: []corev1.Event{events[triggerReconcileFailed]}, + }, + { + Name: "Delete Subscription error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeVirtualService(), + makeDifferentSubscription(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockDeletes: []controllertesting.MockDelete{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Subscription); ok { + return controllertesting.Handled, errors.New("test error deleting subscription") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error deleting subscription", + WantEvent: []corev1.Event{events[subscriptionDeleteFailed], events[triggerReconcileFailed]}, + }, + { + Name: "Re-create Subscription error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeVirtualService(), + makeDifferentSubscription(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockCreates: []controllertesting.MockCreate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Subscription); ok { + return controllertesting.Handled, errors.New("test error re-creating subscription") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error re-creating subscription", + WantEvent: []corev1.Event{events[subscriptionCreateFailed], events[triggerReconcileFailed]}, + }, + { + Name: "Update status error", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeVirtualService(), + makeSameSubscription(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + Mocks: controllertesting.Mocks{ + MockStatusUpdates: []controllertesting.MockStatusUpdate{ + func(_ client.Client, _ context.Context, obj runtime.Object) (controllertesting.MockHandled, error) { + if _, ok := obj.(*v1alpha1.Trigger); ok { + return controllertesting.Handled, errors.New("test error updating trigger status") + } + return controllertesting.Unhandled, nil + }, + }, + }, + WantErrMsg: "test error updating trigger status", + WantEvent: []corev1.Event{events[triggerReconciled], events[triggerUpdateStatusFailed]}, + }, + { + Name: "Trigger reconciliation success", + Scheme: scheme.Scheme, + InitialState: []runtime.Object{ + makeTrigger(), + makeBroker(), + makeChannel(), + makeK8sService(), + makeVirtualService(), + makeSameSubscription(), + }, + Objects: []runtime.Object{ + makeSubscriberServiceAsUnstructured(), + }, + WantEvent: []corev1.Event{events[triggerReconciled]}, + WantPresent: []runtime.Object{ + makeReadyTrigger(), + }, + }, + } + for _, tc := range testCases { + c := tc.GetClient() + dc := tc.GetDynamicClient() + recorder := tc.GetEventRecorder() + + r := &reconciler{ + client: c, + dynamicClient: dc, + recorder: recorder, + logger: zap.NewNop(), + } + tc.ReconcileKey = fmt.Sprintf("%s/%s", testNS, triggerName) + tc.IgnoreTimes = true + t.Run(tc.Name, tc.Runner(t, r, c, recorder)) + } +} + +func makeTrigger() *v1alpha1.Trigger { + return &v1alpha1.Trigger{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "Trigger", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + Name: triggerName, + }, + Spec: v1alpha1.TriggerSpec{ + Broker: brokerName, + Filter: &v1alpha1.TriggerFilter{ + SourceAndType: &v1alpha1.TriggerFilterSourceAndType{ + Source: "Any", + Type: "Any", + }, + }, + Subscriber: &v1alpha1.SubscriberSpec{ + Ref: &corev1.ObjectReference{ + Name: subscriberName, + Kind: subscriberKind, + APIVersion: subscriberAPIVersion, + }, + }, + }, + } +} + +func makeReadyTrigger() *v1alpha1.Trigger { + t := makeTrigger() + t.Status.InitializeConditions() + t.Status.MarkBrokerExists() + t.Status.SubscriberURI = fmt.Sprintf("http://%s.%s.svc.%s/", subscriberName, testNS, utils.GetClusterDomainName()) + t.Status.MarkKubernetesServiceExists() + t.Status.MarkVirtualServiceExists() + t.Status.MarkSubscribed() + return t +} + +func makeDeletingTrigger() *v1alpha1.Trigger { + b := makeReadyTrigger() + b.DeletionTimestamp = &deletionTime + return b +} + +func makeBroker() *v1alpha1.Broker { + return &v1alpha1.Broker{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "Broker", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + Name: brokerName, + }, + Spec: v1alpha1.BrokerSpec{ + ChannelTemplate: &v1alpha1.ChannelSpec{ + Provisioner: makeChannelProvisioner(), + }, + }, + } +} + +func makeChannelProvisioner() *corev1.ObjectReference { + return &corev1.ObjectReference{ + APIVersion: "eventing.knative.dev/v1alpha1", + Kind: "ClusterChannelProvisioner", + Name: "my-provisioner", + } +} + +func newChannel(name string) *v1alpha1.Channel { + return &v1alpha1.Channel{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNS, + Name: name, + Labels: map[string]string{ + "eventing.knative.dev/broker": brokerName, + "eventing.knative.dev/brokerEverything": "true", + }, + OwnerReferences: []metav1.OwnerReference{ + getOwnerReference(), + }, + }, + Spec: v1alpha1.ChannelSpec{ + Provisioner: makeChannelProvisioner(), + }, + Status: v1alpha1.ChannelStatus{ + Address: duckv1alpha1.Addressable{ + Hostname: "any-non-empty-string", + }, + }, + } +} + +func makeChannel() *v1alpha1.Channel { + return newChannel(fmt.Sprintf("%s-broker", brokerName)) +} + +func makeDifferentChannel() *v1alpha1.Channel { + return newChannel(fmt.Sprintf("%s-broker-different", brokerName)) +} + +func makeSubscriberServiceAsUnstructured() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "namespace": testNS, + "name": subscriberName, + }, + }, + } +} + +func makeK8sService() *corev1.Service { + return newK8sService(makeTrigger()) +} + +func makeDifferentK8sService() *corev1.Service { + svc := makeK8sService() + svc.Spec.Ports = []corev1.ServicePort{ + { + Name: "http", + Port: 9999, + }, + } + return svc +} + +func makeVirtualService() *istiov1alpha3.VirtualService { + return newVirtualService(makeTrigger(), makeK8sService()) +} + +func makeDifferentVirtualService() *istiov1alpha3.VirtualService { + vsvc := makeVirtualService() + vsvc.Spec.Hosts = []string{ + names.ServiceHostName("other_svc_name", "other_svc_namespace"), + } + return vsvc +} + +func makeSameSubscription() *v1alpha1.Subscription { + return makeSubscription(makeTrigger(), makeChannel(), makeK8sService()) +} + +func makeDifferentSubscription() *v1alpha1.Subscription { + return makeSubscription(makeTrigger(), makeDifferentChannel(), makeK8sService()) +} + +func getOwnerReference() metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "Broker", + Name: brokerName, + Controller: &trueVal, + BlockOwnerDeletion: &trueVal, + } +} diff --git a/test/builders.go b/test/builders.go new file mode 100644 index 00000000000..23d7c6538e3 --- /dev/null +++ b/test/builders.go @@ -0,0 +1,80 @@ +/* +Copyright 2019 The Knative 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 test + +import ( + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Builder for trigger objects. +type TriggerBuilder struct { + *eventingv1alpha1.Trigger +} + +func NewTriggerBuilder(name, namespace string) *TriggerBuilder { + trigger := &eventingv1alpha1.Trigger{ + TypeMeta: metav1.TypeMeta{ + APIVersion: eventingv1alpha1.SchemeGroupVersion.String(), + Kind: "Trigger", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: eventingv1alpha1.TriggerSpec{ + Broker: "default", + Filter: &eventingv1alpha1.TriggerFilter{ + SourceAndType: &eventingv1alpha1.TriggerFilterSourceAndType{ + Type: eventingv1alpha1.TriggerAnyFilter, + Source: eventingv1alpha1.TriggerAnyFilter, + }, + }, + Subscriber: &eventingv1alpha1.SubscriberSpec{}, + }, + } + + return &TriggerBuilder{ + Trigger: trigger, + } +} + +func (b *TriggerBuilder) Build() *eventingv1alpha1.Trigger { + return b.Trigger.DeepCopy() +} + +func (b *TriggerBuilder) EventType(eventType string) *TriggerBuilder { + b.Trigger.Spec.Filter.SourceAndType.Type = eventType + return b +} + +func (b *TriggerBuilder) EventSource(eventSource string) *TriggerBuilder { + b.Trigger.Spec.Filter.SourceAndType.Source = eventSource + return b +} + +func (b *TriggerBuilder) Broker(brokerName string) *TriggerBuilder { + b.Trigger.Spec.Broker = brokerName + return b +} + +func (b *TriggerBuilder) SubscriberSvc(svcName string) *TriggerBuilder { + b.Trigger.Spec.Subscriber.Ref = &corev1.ObjectReference{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + Name: svcName, + } + return b +} diff --git a/test/crd.go b/test/crd.go index cd365342a0e..37016def59b 100644 --- a/test/crd.go +++ b/test/crd.go @@ -166,6 +166,17 @@ func Subscription(name string, namespace string, channel *corev1.ObjectReference } } +// Broker returns a Broker. +func Broker(name string, namespace string) *v1alpha1.Broker { + return &v1alpha1.Broker{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.BrokerSpec{}, + } +} + // CloudEvent specifies the arguments for a CloudEvent sent by the sendevent // binary. type CloudEvent struct { @@ -176,6 +187,12 @@ type CloudEvent struct { Encoding string // binary or structured } +// TypeAndSource specifies the type and source of an Event. +type TypeAndSource struct { + Type string + Source string +} + const ( CloudEventEncodingBinary = "binary" CloudEventEncodingStructured = "structured" diff --git a/test/crd_checks.go b/test/crd_checks.go index e66c7722024..ab933bd394b 100644 --- a/test/crd_checks.go +++ b/test/crd_checks.go @@ -28,6 +28,7 @@ import ( servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1" servingclient "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1" "go.opencensus.io/trace" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" ) @@ -91,3 +92,62 @@ func WaitForSubscriptionState(client eventingclient.SubscriptionInterface, name return inState(r) }) } + +// WaitForBrokerState polls the status of the Broker called name from client +// every interval until inState returns `true` indicating it is done, returns an +// error or timeout. desc will be used to name the metric that is emitted to +// track how long it took for name to get into the state checked by inState. +func WaitForBrokerState(client eventingclient.BrokerInterface, name string, inState func(r *eventingv1alpha1.Broker) (bool, error), desc string) error { + metricName := fmt.Sprintf("WaitForBrokerState/%s/%s", name, desc) + _, span := trace.StartSpan(context.Background(), metricName) + defer span.End() + + return wait.PollImmediate(interval, timeout, func() (bool, error) { + r, err := client.Get(name, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + // Return false as we are not done yet. + // We swallow the error to keep on polling + return false, nil + } else if err != nil { + // Return true to stop and return the error. + return true, err + } + return inState(r) + }) +} + +// WaitForTriggerState polls the status of the Trigger called name from client +// every interval until inState returns `true` indicating it is done, returns an +// error or timeout. desc will be used to name the metric that is emitted to +// track how long it took for name to get into the state checked by inState. +func WaitForTriggerState(client eventingclient.TriggerInterface, name string, inState func(r *eventingv1alpha1.Trigger) (bool, error), desc string) error { + metricName := fmt.Sprintf("WaitForTriggerState/%s/%s", name, desc) + _, span := trace.StartSpan(context.Background(), metricName) + defer span.End() + + return wait.PollImmediate(interval, timeout, func() (bool, error) { + r, err := client.Get(name, metav1.GetOptions{}) + if err != nil { + return true, err + } + return inState(r) + }) +} + +// WaitForTriggersListState polls the status of the TriggerList +// from client every interval until inState returns `true` indicating it +// is done, returns an error or timeout. desc will be used to name the metric +// that is emitted to track how long it took to get into the state checked by inState. +func WaitForTriggersListState(clients eventingclient.TriggerInterface, inState func(t *eventingv1alpha1.TriggerList) (bool, error), desc string) error { + metricName := fmt.Sprintf("WaitForTriggerListState/%s", desc) + _, span := trace.StartSpan(context.Background(), metricName) + defer span.End() + + return wait.PollImmediate(interval, timeout, func() (bool, error) { + t, err := clients.List(metav1.ListOptions{}) + if err != nil { + return true, err + } + return inState(t) + }) +} diff --git a/test/e2e/broker_trigger_test.go b/test/e2e/broker_trigger_test.go new file mode 100644 index 00000000000..127d3f5b180 --- /dev/null +++ b/test/e2e/broker_trigger_test.go @@ -0,0 +1,266 @@ +// +build e2e + +/* +Copyright 2019 The Knative 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 e2e + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + + "github.com/knative/eventing/test" + "github.com/knative/pkg/test/logging" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/uuid" +) + +const ( + defaultBrokerName = "default" + waitForFilterPodRunning = 30 * time.Second + selectorKey = "end2end-test-broker-trigger" + + any = v1alpha1.TriggerAnyFilter + eventType1 = "type1" + eventType2 = "type2" + eventSource1 = "source1" + eventSource2 = "source2" +) + +// Helper struct to tie the type and sources of the events we expect to receive +// in subscribers with the selectors we use when creating their pods. +type eventReceiver struct { + typeAndSource test.TypeAndSource + selector map[string]string +} + +// This test annotates the testing namespace so that a default broker is created. +// It then binds many triggers with different filtering patterns to that default broker, +// and sends different events to the broker's address. Finally, it verifies that only +// the appropriate events are routed to the subscribers. +func TestDefaultBrokerWithManyTriggers(t *testing.T) { + clients, cleaner := Setup(t, t.Logf) + + // Verify namespace exists. + ns, cleanupNS := NamespaceExists(t, clients, t.Logf) + + defer cleanupNS() + defer TearDown(clients, cleaner, t.Logf) + + t.Logf("Labeling namespace %s", ns) + + // Label namespace so that it creates the default broker. + err := LabelNamespace(clients, t.Logf, map[string]string{"knative-eventing-injection": "enabled"}) + if err != nil { + t.Fatalf("Error annotating namespace: %v", err) + } + + t.Logf("Namespace %s annotated", ns) + + // Wait for default broker ready. + t.Logf("Waiting for default broker to be ready") + defaultBroker := test.Broker(defaultBrokerName, ns) + err = WaitForBrokerReady(clients, defaultBroker) + if err != nil { + t.Fatalf("Error waiting for default broker to become ready: %v", err) + } + + defaultBrokerUrl := fmt.Sprintf("http://%s", defaultBroker.Status.Address.Hostname) + + t.Logf("Default broker ready: %q", defaultBrokerUrl) + + // These are the event types and sources that triggers will listen to, as well as the selectors + // to set in the subscriber and services pods. + eventsToReceive := []eventReceiver{ + {test.TypeAndSource{Type: any, Source: any}, newSelector()}, + {test.TypeAndSource{Type: eventType1, Source: any}, newSelector()}, + {test.TypeAndSource{Type: any, Source: eventSource1}, newSelector()}, + {test.TypeAndSource{Type: eventType1, Source: eventSource1}, newSelector()}, + } + + t.Logf("Creating Subscriber pods") + + // Save the pods references in this map for later use. + subscriberPods := make(map[string]*corev1.Pod, len(eventsToReceive)) + for _, event := range eventsToReceive { + subscriberPodName := name("dumper", event.typeAndSource.Type, event.typeAndSource.Source) + subscriberPod := test.EventLoggerPod(subscriberPodName, ns, event.selector) + if err := CreatePod(clients, subscriberPod, t.Logf, cleaner); err != nil { + t.Fatalf("Error creating subscriber pod: %v", err) + } + subscriberPods[subscriberPodName] = subscriberPod + } + + t.Logf("Subscriber pods created") + + t.Logf("Waiting for subscriber pods to become running") + + // Wait for all of the pods in the namespace to become running. + if err := WaitForAllPodsRunning(clients, t.Logf, ns); err != nil { + t.Fatalf("Error waiting for event logger pod to become running: %v", err) + } + + t.Logf("Subscriber pods running") + + t.Logf("Creating Subscriber services") + + for _, event := range eventsToReceive { + subscriberSvcName := name("svc", event.typeAndSource.Type, event.typeAndSource.Source) + service := test.Service(subscriberSvcName, ns, event.selector) + if err := CreateService(clients, service, t.Logf, cleaner); err != nil { + t.Fatalf("Error creating subscriber service: %v", err) + } + } + + t.Logf("Subscriber services created") + + t.Logf("Creating Triggers") + + for _, event := range eventsToReceive { + triggerName := name("trigger", event.typeAndSource.Type, event.typeAndSource.Source) + // subscriberName should be the same as the subscriberSvc from before. + subscriberName := name("svc", event.typeAndSource.Type, event.typeAndSource.Source) + trigger := test.NewTriggerBuilder(triggerName, ns). + EventType(event.typeAndSource.Type). + EventSource(event.typeAndSource.Source). + // Don't need to set the broker as we use the default one + // but wanted to be more explicit. + Broker(defaultBrokerName). + SubscriberSvc(subscriberName). + Build() + err := CreateTrigger(clients, trigger, t.Logf, cleaner) + if err != nil { + t.Fatalf("Error creating trigger: %v", err) + } + } + + t.Logf("Triggers created") + + t.Logf("Waiting for triggers to become ready") + + // Wait for all of the triggers in the namespace to be ready. + if err := WaitForAllTriggersReady(clients, t.Logf, ns); err != nil { + t.Fatalf("Error waiting for triggers to become ready: %v", err) + } + + t.Logf("Triggers ready") + + // These are the event types and sources that will be send. + eventsToSend := []test.TypeAndSource{ + {eventType1, eventSource1}, + {eventType1, eventSource2}, + {eventType2, eventSource1}, + {eventType2, eventSource2}, + } + + // We notice some crashLoopBacks in the filter and ingress pod creation. + // We then delay the creation of the sender pods in order not to miss events. + // TODO improve this + t.Logf("Waiting for filter and ingress pods to become running") + time.Sleep(waitForFilterPodRunning) + + t.Logf("Creating event sender pods") + + // Map to save the expected events per dumper so that we can verify the delivery. + expectedEvents := make(map[string][]string) + // Map to save the unexpected events per dumper so that we can verify that they weren't delivered. + unexpectedEvents := make(map[string][]string) + for _, eventToSend := range eventsToSend { + // Create cloud event. + // Using event type and source as part of the body for easier debugging. + body := fmt.Sprintf("Body-%s-%s", eventToSend.Type, eventToSend.Source) + cloudEvent := test.CloudEvent{ + Source: eventToSend.Source, + Type: eventToSend.Type, + Data: fmt.Sprintf(`{"msg":%q}`, body), + } + // Create sender pod. + senderPodName := name("sender", eventToSend.Type, eventToSend.Source) + senderPod := test.EventSenderPod(senderPodName, ns, defaultBrokerUrl, cloudEvent) + if err := CreatePod(clients, senderPod, t.Logf, cleaner); err != nil { + t.Fatalf("Error creating event sender pod: %v", err) + } + + // Check on every dumper whether we should expect this event or not, and add its body + // to the expectedEvents/unexpectedEvents maps. + for _, eventToReceive := range eventsToReceive { + subscriberPodName := name("dumper", eventToReceive.typeAndSource.Type, eventToReceive.typeAndSource.Source) + if shouldExpectEvent(&eventToSend, &eventToReceive, t.Logf) { + expectedEvents[subscriberPodName] = append(expectedEvents[subscriberPodName], body) + } else { + unexpectedEvents[subscriberPodName] = append(unexpectedEvents[subscriberPodName], body) + } + } + } + + t.Logf("Event sender pods created") + + t.Logf("Waiting for event sender pods to be running") + + // Wait for all of them to be running. + if err := WaitForAllPodsRunning(clients, t.Logf, ns); err != nil { + t.Fatalf("Error waiting for event sender pod to become running: %v", err) + } + + t.Logf("Event sender pods running") + + t.Logf("Verifying events delivered to appropriate dumpers") + + for _, event := range eventsToReceive { + subscriberPodName := name("dumper", event.typeAndSource.Type, event.typeAndSource.Source) + subscriberPod := subscriberPods[subscriberPodName] + t.Logf("Dumper %q expecting %q", subscriberPodName, strings.Join(expectedEvents[subscriberPodName], ",")) + if err := WaitForLogContents(clients, t.Logf, subscriberPodName, subscriberPod.Spec.Containers[0].Name, ns, expectedEvents[subscriberPodName]); err != nil { + t.Fatalf("Event(s) not found in logs of subscriber pod %q: %v", subscriberPodName, err) + } + // At this point all the events should have been received in the pod. + // We check whether we find unexpected events. If so, then we fail. + found, err := FindAnyLogContents(clients, t.Logf, subscriberPodName, subscriberPod.Spec.Containers[0].Name, ns, unexpectedEvents[subscriberPodName]) + if err != nil { + t.Fatalf("Failed querying to find log contents in pod %q: %v", subscriberPodName, err) + } + if found { + t.Fatalf("Unexpected event(s) found in logs of subscriber pod %q", subscriberPodName) + } + } +} + +// Helper function to create names for different objects (e.g., triggers, services, etc.). +func name(obj, eventType, eventSource string) string { + // Pod names need to be lowercase. We might have an eventType as Any, that is why we lowercase them. + return strings.ToLower(fmt.Sprintf("%s-%s-%s", obj, eventType, eventSource)) +} + +// Returns a new selector with a random uuid. +func newSelector() map[string]string { + return map[string]string{selectorKey: string(uuid.NewUUID())} +} + +// Checks whether we should expect to receive 'eventToSend' in 'eventReceiver' based on its type and source pattern. +func shouldExpectEvent(eventToSend *test.TypeAndSource, receiver *eventReceiver, logf logging.FormatLogger) bool { + if receiver.typeAndSource.Type != any && receiver.typeAndSource.Type != eventToSend.Type { + logf("Event types mismatch, receive %s, send %s", receiver.typeAndSource.Type, eventToSend.Type) + return false + } + if receiver.typeAndSource.Source != any && receiver.typeAndSource.Source != eventToSend.Source { + logf("Event sources mismatch, receive %s, send %s", receiver.typeAndSource.Source, eventToSend.Source) + return false + } + return true +} diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index beba473b060..4f727cb25ec 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "k8s.io/apimachinery/pkg/api/errors" + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" "github.com/knative/eventing/test" pkgTest "github.com/knative/pkg/test" @@ -159,6 +161,71 @@ func WithChannelAndSubscriptionReady(clients *test.Clients, channel *v1alpha1.Ch return nil } +// CreateBroker will create a Broker. +func CreateBroker(clients *test.Clients, broker *v1alpha1.Broker, logf logging.FormatLogger, cleaner *test.Cleaner) error { + brokers := clients.Eventing.EventingV1alpha1().Brokers(broker.Namespace) + res, err := brokers.Create(broker) + if err != nil { + return err + } + cleaner.Add(v1alpha1.SchemeGroupVersion.Group, v1alpha1.SchemeGroupVersion.Version, "brokers", broker.Namespace, res.ObjectMeta.Name) + return nil +} + +// WithBrokerReady creates a Broker and waits until it is Ready. +func WithBrokerReady(clients *test.Clients, broker *v1alpha1.Broker, logf logging.FormatLogger, cleaner *test.Cleaner) error { + if err := CreateBroker(clients, broker, logf, cleaner); err != nil { + return err + } + return WaitForBrokerReady(clients, broker) +} + +// WaitForBrokerReady waits until the broker is Ready. +func WaitForBrokerReady(clients *test.Clients, broker *v1alpha1.Broker) error { + brokers := clients.Eventing.EventingV1alpha1().Brokers(broker.Namespace) + if err := test.WaitForBrokerState(brokers, broker.Name, test.IsBrokerReady, "BrokerIsReady"); err != nil { + return err + } + // Update the given object so they'll reflect the ready state. + updatedBroker, err := brokers.Get(broker.Name, metav1.GetOptions{}) + if err != nil { + return err + } + updatedBroker.DeepCopyInto(broker) + return nil +} + +// CreateTrigger will create a Trigger. +func CreateTrigger(clients *test.Clients, trigger *v1alpha1.Trigger, logf logging.FormatLogger, cleaner *test.Cleaner) error { + triggers := clients.Eventing.EventingV1alpha1().Triggers(trigger.Namespace) + res, err := triggers.Create(trigger) + if err != nil { + return err + } + cleaner.Add(v1alpha1.SchemeGroupVersion.Group, v1alpha1.SchemeGroupVersion.Version, "triggers", trigger.Namespace, res.ObjectMeta.Name) + return nil +} + +// WithTriggerReady creates a Trigger and waits until it is Ready. +func WithTriggerReady(clients *test.Clients, trigger *v1alpha1.Trigger, logf logging.FormatLogger, cleaner *test.Cleaner) error { + if err := CreateTrigger(clients, trigger, logf, cleaner); err != nil { + return err + } + + triggers := clients.Eventing.EventingV1alpha1().Triggers(trigger.Namespace) + if err := test.WaitForTriggerState(triggers, trigger.Name, test.IsTriggerReady, "TriggerIsReady"); err != nil { + return err + } + // Update the given object so they'll reflect the ready state. + updatedTrigger, err := triggers.Get(trigger.Name, metav1.GetOptions{}) + if err != nil { + return err + } + updatedTrigger.DeepCopyInto(trigger) + + return nil +} + // CreateServiceAccount will create a service account func CreateServiceAccount(clients *test.Clients, sa *corev1.ServiceAccount, _ logging.FormatLogger, cleaner *test.Cleaner) error { sas := clients.Kube.Kube.CoreV1().ServiceAccounts(pkgTest.Flags.Namespace) @@ -264,18 +331,49 @@ func PodLogs(clients *test.Clients, podName string, containerName string, namesp return nil, fmt.Errorf("Could not find logs for %s/%s", podName, containerName) } -// WaitForLogContent waits until logs for given Pod/Container include the given content. -// If the content is not present within timeout it returns error. -func WaitForLogContent(clients *test.Clients, logf logging.FormatLogger, podName string, containerName string, namespace string, content string) error { +// WaitForLogContents waits until logs for given Pod/Container include the given contents. +// If the contents are not present within timeout it returns error. +func WaitForLogContents(clients *test.Clients, logf logging.FormatLogger, podName string, containerName string, namespace string, contents []string) error { return wait.PollImmediate(interval, timeout, func() (bool, error) { logs, err := PodLogs(clients, podName, containerName, namespace, logf) if err != nil { return true, err } - return strings.Contains(string(logs), content), nil + for _, content := range contents { + if !strings.Contains(string(logs), content) { + logf("Could not find content %q for %s/%s. Found %q instead", content, podName, containerName, string(logs)) + return false, nil + } else { + logf("Found content %q for %s/%s in logs %q", content, podName, containerName, string(logs)) + // do not return as we will keep on looking for the other contents in the slice + } + } + return true, nil }) } +// FindAnyLogContents attempts to find logs for given Pod/Container that has 'any' of the given contents. +// It returns an error if it couldn't retrieve the logs. In case 'any' of the contents are there, it returns true. +func FindAnyLogContents(clients *test.Clients, logf logging.FormatLogger, podName string, containerName string, namespace string, contents []string) (bool, error) { + logs, err := PodLogs(clients, podName, containerName, namespace, logf) + if err != nil { + return false, err + } + for _, content := range contents { + if strings.Contains(string(logs), content) { + logf("Found content %q for %s/%s.", content, podName, containerName) + return true, nil + } + } + return false, nil +} + +// WaitForLogContent waits until logs for given Pod/Container include the given content. +// If the content is not present within timeout it returns error. +func WaitForLogContent(clients *test.Clients, logf logging.FormatLogger, podName string, containerName string, namespace string, content string) error { + return WaitForLogContents(clients, logf, podName, containerName, namespace, []string{content}) +} + // WaitForAllPodsRunning will wait until all pods in the given namespace are running func WaitForAllPodsRunning(clients *test.Clients, _ logging.FormatLogger, namespace string) error { if err := pkgTest.WaitForPodListState(clients.Kube, test.PodsRunning, "PodsAreRunning", namespace); err != nil { @@ -283,3 +381,68 @@ func WaitForAllPodsRunning(clients *test.Clients, _ logging.FormatLogger, namesp } return nil } + +// WaitForAllTriggersReady will wait until all triggers in the given namespace are ready. +func WaitForAllTriggersReady(clients *test.Clients, logf logging.FormatLogger, namespace string) error { + triggers := clients.Eventing.EventingV1alpha1().Triggers(namespace) + if err := test.WaitForTriggersListState(triggers, test.TriggersReady, "TriggerIsReady"); err != nil { + return err + } + return nil +} + +// LabelNamespace labels the test namespace with the labels map. +func LabelNamespace(clients *test.Clients, logf logging.FormatLogger, labels map[string]string) error { + ns := pkgTest.Flags.Namespace + nsSpec, err := clients.Kube.Kube.CoreV1().Namespaces().Get(ns, metav1.GetOptions{}) + if err != nil && errors.IsNotFound(err) { + return err + } + if nsSpec.Labels == nil { + nsSpec.Labels = map[string]string{} + } + for k, v := range labels { + nsSpec.Labels[k] = v + } + _, err = clients.Kube.Kube.CoreV1().Namespaces().Update(nsSpec) + return err +} + +func NamespaceExists(t *testing.T, clients *test.Clients, logf logging.FormatLogger) (string, func()) { + shutdown := func() {} + ns := pkgTest.Flags.Namespace + logf("Namespace: %s", ns) + + nsSpec, err := clients.Kube.Kube.CoreV1().Namespaces().Get(ns, metav1.GetOptions{}) + + if err != nil && errors.IsNotFound(err) { + nsSpec = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}} + logf("Creating Namespace: %s", ns) + nsSpec, err = clients.Kube.Kube.CoreV1().Namespaces().Create(nsSpec) + + if err != nil { + t.Fatalf("Failed to create Namespace: %s; %v", ns, err) + } else { + shutdown = func() { + clients.Kube.Kube.CoreV1().Namespaces().Delete(nsSpec.Name, nil) + // TODO: this is a bit hacky but in order for the tests to work + // correctly for a clean namespace to be created we need to also + // wait for it to be removed. + // To fix this we could generate namespace names. + // This only happens when the namespace provided does not exist. + // + // wait up to 120 seconds for the namespace to be removed. + logf("Deleting Namespace: %s", ns) + for i := 0; i < 120; i++ { + time.Sleep(1 * time.Second) + if _, err := clients.Kube.Kube.CoreV1().Namespaces().Get(ns, metav1.GetOptions{}); err != nil && errors.IsNotFound(err) { + logf("Namespace has been deleted") + // the namespace is gone. + break + } + } + } + } + } + return ns, shutdown +} diff --git a/test/e2e/single_event_test.go b/test/e2e/single_event_test.go index a43030a588b..c0aad7c453c 100644 --- a/test/e2e/single_event_test.go +++ b/test/e2e/single_event_test.go @@ -88,7 +88,7 @@ func SingleEvent(t *testing.T, encoding string) { clients, cleaner := Setup(t, t.Logf) // verify namespace - ns, cleanupNS := namespaceExists(t, clients) + ns, cleanupNS := NamespaceExists(t, clients, t.Logf) defer cleanupNS() // TearDown() needs to be deferred after cleanupNS(). Otherwise the namespace is deleted and all diff --git a/test/states.go b/test/states.go index 49f4256435e..a05e08728d0 100644 --- a/test/states.go +++ b/test/states.go @@ -54,6 +54,34 @@ func IsSubscriptionReady(s *eventingv1alpha1.Subscription) (bool, error) { return s.Status.IsReady(), nil } +// IsBrokerReady will check the status conditions of the Broker and return true +// if the Broker is ready. +func IsBrokerReady(b *eventingv1alpha1.Broker) (bool, error) { + return b.Status.IsReady(), nil +} + +// IsTriggerReady will check the status conditions of the Trigger and +// return true if the Trigger is ready. +func IsTriggerReady(t *eventingv1alpha1.Trigger) (bool, error) { + return t.Status.IsReady(), nil +} + +// TriggersReady will check the status conditions of the trigger list and return true +// if all triggers are Ready. +func TriggersReady(triggerList *eventingv1alpha1.TriggerList) (bool, error) { + var names []string + for _, t := range triggerList.Items { + names = append(names, t.Name) + } + log.Printf("Checking triggers: %v", names) + for _, trigger := range triggerList.Items { + if !trigger.Status.IsReady() { + return false, nil + } + } + return true, nil +} + // PodsRunning will check the status conditions of the pod list and return true // if all pods are Running. func PodsRunning(podList *corev1.PodList) (bool, error) {