From 3be8c55c9fa4ab84bdb022e01a6bd1f4b71abce7 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 17 Aug 2018 15:05:24 -0400 Subject: [PATCH 1/6] Bus Refactor Clarify and solidify the public interfaces for a Bus. Common behavior has moved into composable units that can individually tests or injected into the bus for testing. Bus implementors generally only need to provide custom implementations for up to six methods defined by EventHandlerFuncs. Core components: - [Bus|Channel|Subscription]Reference: decouples bus from the raw k8s resources - Cache: lookup and presist raw resource types for a reference - EventHandlerFuncs: the contract for bus implementations - Reconciler: reads resource changes from the API server, saves the resource in the Cache and emits events to the EventHandlerFuncs - MessageDispatcher: sends messages to a destination over the event delivery protocol - MessageReceiver: receive messages for a channel on the bus over the event delivery protocol Behavior changes: - avoid unnessesary status updates if the status has not changed - avoid supressing errors when updating status - avoid supressing updates for unchanged resources as the local copy may not be fully reconciled --- Gopkg.lock | 1 - config/buses/stub/stub-bus.yaml | 2 +- pkg/buses/bus.go | 166 +++ pkg/buses/cache.go | 86 ++ pkg/buses/cache_test.go | 135 +++ pkg/buses/gcppubsub/bus.go | 322 +++--- .../{stub_test.go => gcppubsub/bus_test.go} | 2 +- pkg/buses/gcppubsub/dispatcher/main.go | 58 +- pkg/buses/gcppubsub/provisioner/main.go | 57 +- pkg/buses/handler_funcs.go | 258 +++++ pkg/buses/kafka/bus.go | 276 +++++ .../{dispatcher/stub_test.go => bus_test.go} | 2 +- pkg/buses/kafka/dispatcher/dispatcher.go | 251 ----- pkg/buses/kafka/dispatcher/main.go | 64 ++ pkg/buses/kafka/provisioner/main.go | 64 ++ pkg/buses/kafka/provisioner/provisioner.go | 149 --- pkg/buses/kafka/provisioner/stub_test.go | 24 - pkg/buses/message_dispatcher.go | 3 + pkg/buses/message_receiver.go | 8 +- pkg/buses/monitor.go | 978 ------------------ pkg/buses/reconciler.go | 664 ++++++++++++ pkg/buses/references.go | 72 +- pkg/buses/references_test.go | 178 ++++ pkg/buses/stub/bus.go | 117 +++ pkg/buses/stub/{stub_test.go => bus_test.go} | 2 +- pkg/buses/stub/dispatcher/main.go | 53 + pkg/buses/stub/main.go | 135 --- pkg/controller/bus/controller.go | 39 +- pkg/controller/channel/controller.go | 7 +- pkg/controller/clusterbus/controller.go | 34 +- 30 files changed, 2390 insertions(+), 1817 deletions(-) create mode 100644 pkg/buses/bus.go create mode 100644 pkg/buses/cache.go create mode 100644 pkg/buses/cache_test.go rename pkg/buses/{stub_test.go => gcppubsub/bus_test.go} (97%) create mode 100644 pkg/buses/handler_funcs.go create mode 100644 pkg/buses/kafka/bus.go rename pkg/buses/kafka/{dispatcher/stub_test.go => bus_test.go} (98%) delete mode 100644 pkg/buses/kafka/dispatcher/dispatcher.go create mode 100644 pkg/buses/kafka/dispatcher/main.go create mode 100644 pkg/buses/kafka/provisioner/main.go delete mode 100644 pkg/buses/kafka/provisioner/provisioner.go delete mode 100644 pkg/buses/kafka/provisioner/stub_test.go delete mode 100644 pkg/buses/monitor.go create mode 100644 pkg/buses/reconciler.go create mode 100644 pkg/buses/references_test.go create mode 100644 pkg/buses/stub/bus.go rename pkg/buses/stub/{stub_test.go => bus_test.go} (98%) create mode 100644 pkg/buses/stub/dispatcher/main.go delete mode 100644 pkg/buses/stub/main.go diff --git a/Gopkg.lock b/Gopkg.lock index 5b38348c8ba..0399479dd51 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1045,7 +1045,6 @@ "k8s.io/api/apps/v1", "k8s.io/api/batch/v1", "k8s.io/api/core/v1", - "k8s.io/api/rbac/v1beta1", "k8s.io/apimachinery/pkg/api/equality", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/api/meta", diff --git a/config/buses/stub/stub-bus.yaml b/config/buses/stub/stub-bus.yaml index d39f4c37aa7..27cffb43c15 100644 --- a/config/buses/stub/stub-bus.yaml +++ b/config/buses/stub/stub-bus.yaml @@ -18,7 +18,7 @@ metadata: spec: dispatcher: name: dispatcher - image: github.com/knative/eventing/pkg/buses/stub + image: github.com/knative/eventing/pkg/buses/stub/dispatcher args: [ "-logtostderr", "-stderrthreshold", "INFO", diff --git a/pkg/buses/bus.go b/pkg/buses/bus.go new file mode 100644 index 00000000000..4bc68309623 --- /dev/null +++ b/pkg/buses/bus.go @@ -0,0 +1,166 @@ +/* + * 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 buses + +import ( + "fmt" + + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" +) + +type bus struct { + busRef BusReference + handlerFuncs EventHandlerFuncs + + reconciler *Reconciler + cache *Cache + dispatcher *MessageDispatcher + receiver *MessageReceiver +} + +// BusOpts holds configuration options for new buses. These options are not +// required for proper operation of the bus, but are useful to override the +// default behavior and for testing. +type BusOpts struct { + // MasterURL is the address of the Kubernetes API server. Overrides any + // value in kubeconfig. Only required if out-of-cluster. + MasterURL string + // KubeConfig is the path to a kubeconfig. Only required if out-of-cluster. + KubeConfig string + + // Cache to use for this bus. A cache will be created for the bus if not + // specified. + Cache *Cache + // Reconciler to use for this bus. A reconciler wil be created for the bus + // if not specified. + Reconciler *Reconciler + // MessageDispatcher to use for this bus. The message dispatcher is used to + // send messages from the bus to a subscriber. A message dispatcher will be + // created for the bus if needed and not specified. + MessageDispatcher *MessageDispatcher + // MessageReceiver to use for this bus. The message receiver is used to + // receive message sent to a channel. A message receiver will be created + // for the bus if needed and not specified. + MessageReceiver *MessageReceiver +} + +// BusProvisioner provisions channels and subscriptions for a bus on backing +// infrastructure. +type BusProvisioner interface { + Run(threadiness int, stopCh <-chan struct{}) +} + +// NewBusProvisioner creates a new provisioner for a specific bus. +// EventHandlerFuncs are used to be notified when a channel or subscription is +// created, updated or removed. +func NewBusProvisioner(busRef BusReference, handlerFuncs EventHandlerFuncs, opts *BusOpts) BusProvisioner { + bus := &bus{ + busRef: busRef, + handlerFuncs: handlerFuncs, + } + + if opts.Cache == nil { + opts.Cache = NewCache() + } + bus.cache = opts.Cache + + if opts.Reconciler == nil { + opts.Reconciler = NewReconciler(Provisioner, opts.MasterURL, opts.KubeConfig, bus.cache, handlerFuncs) + } + bus.reconciler = opts.Reconciler + + return bus +} + +// BusDispatcher dispatches messages from channels to subscribers via backing +// infrastructure. +type BusDispatcher interface { + Run(threadiness int, stopCh <-chan struct{}) + DispatchMessage(subscriptionRef SubscriptionReference, message *Message) error +} + +// NewBusDispatcher creates a new dispatcher for a specific bus. +// EventHandlerFuncs are used to be notified when a subscription is created, +// updated or removed, or a message is received. +func NewBusDispatcher(busRef BusReference, handlerFuncs EventHandlerFuncs, opts *BusOpts) BusDispatcher { + bus := &bus{ + busRef: busRef, + handlerFuncs: handlerFuncs, + } + + if opts.Cache == nil { + opts.Cache = NewCache() + } + bus.cache = opts.Cache + + if opts.Reconciler == nil { + opts.Reconciler = NewReconciler(Dispatcher, opts.MasterURL, opts.KubeConfig, bus.cache, handlerFuncs) + } + bus.reconciler = opts.Reconciler + + if opts.MessageDispatcher == nil { + opts.MessageDispatcher = NewMessageDispatcher() + } + bus.dispatcher = opts.MessageDispatcher + + if opts.MessageReceiver == nil { + opts.MessageReceiver = NewMessageReceiver(func(channelRef ChannelReference, message *Message) error { + return bus.receiveMessage(channelRef, message) + }) + } + bus.receiver = opts.MessageReceiver + + return bus +} + +// Run starts the bus's processing. +func (b bus) Run(threadiness int, stopCh <-chan struct{}) { + go b.reconciler.Run(b.busRef, threadiness, stopCh) + b.reconciler.WaitForCacheSync(stopCh) + if b.receiver != nil { + go b.receiver.Run(stopCh) + } + + <-stopCh +} + +func (b *bus) receiveMessage(channelRef ChannelReference, message *Message) error { + _, err := b.cache.Channel(channelRef) + if err != nil { + return ErrUnknownChannel + } + return b.handlerFuncs.onReceiveMessage(channelRef, message) +} + +// DispatchMessage sends a message to a subscriber. This function is only +// avilable for bus dispatchers. +func (b *bus) DispatchMessage(subscriptionRef SubscriptionReference, message *Message) error { + subscription, err := b.cache.Subscription(subscriptionRef) + if err != nil { + return fmt.Errorf("unable to dispatch to unknown subscription %q", subscriptionRef.String()) + } + return b.dispatchMessage(subscription, message) +} + +func (b *bus) dispatchMessage(subscription *channelsv1alpha1.Subscription, message *Message) error { + subscriber := subscription.Spec.Subscriber + defaults := DispatchDefaults{ + Namespace: subscription.Namespace, + ReplyTo: subscription.Spec.ReplyTo, + } + return b.dispatcher.DispatchMessage(message, subscriber, defaults) +} diff --git a/pkg/buses/cache.go b/pkg/buses/cache.go new file mode 100644 index 00000000000..c0bf23636b6 --- /dev/null +++ b/pkg/buses/cache.go @@ -0,0 +1,86 @@ +/* + * 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 buses + +import ( + "fmt" + + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" +) + +// NewCache create a cache that is able to save and retrive Channels and +// Subscriptions by their reference. +func NewCache() *Cache { + return &Cache{ + channels: make(map[ChannelReference]*channelsv1alpha1.Channel), + subscriptions: make(map[SubscriptionReference]*channelsv1alpha1.Subscription), + } +} + +// Cache able to save and retrive Channels and Subscriptions by their +// reference. It is used by the reconciler to track which resources have been +// provisioned and comparing updated resources to the provisioned version. +type Cache struct { + channels map[ChannelReference]*channelsv1alpha1.Channel + subscriptions map[SubscriptionReference]*channelsv1alpha1.Subscription +} + +// Channel returns a cached channel for provided reference or an error if the +// channel is not in the cache. +func (c *Cache) Channel(channelRef ChannelReference) (*channelsv1alpha1.Channel, error) { + channel, ok := c.channels[channelRef] + if !ok { + return nil, fmt.Errorf("unknown channel %q", channelRef.String()) + } + return channel, nil +} + +// Subscription returns a cached subscription for provided reference or an +// error if the subscription is not in the cache. +func (c *Cache) Subscription(subscriptionRef SubscriptionReference) (*channelsv1alpha1.Subscription, error) { + subscription, ok := c.subscriptions[subscriptionRef] + if !ok { + return nil, fmt.Errorf("unknown subscription %q", subscriptionRef.String()) + } + return subscription, nil +} + +// AddChannel adds, or updates, the provided channel to the cache for later +// retrieal by its reference. +func (c *Cache) AddChannel(channel *channelsv1alpha1.Channel) { + channelRef := NewChannelReference(channel) + c.channels[channelRef] = channel +} + +// RemoveChannel removes the provided channel from the cache. +func (c *Cache) RemoveChannel(channel *channelsv1alpha1.Channel) { + channelRef := NewChannelReference(channel) + delete(c.channels, channelRef) +} + +// AddSubscription adds, or updates, the provided subscription to the cache for +// later retrieal by its reference. +func (c *Cache) AddSubscription(subscription *channelsv1alpha1.Subscription) { + subscriptionRef := NewSubscriptionReference(subscription) + c.subscriptions[subscriptionRef] = subscription +} + +// RemoveSubscription removes the provided subscription from the cache. +func (c *Cache) RemoveSubscription(subscription *channelsv1alpha1.Subscription) { + subscriptionRef := NewSubscriptionReference(subscription) + delete(c.subscriptions, subscriptionRef) +} diff --git a/pkg/buses/cache_test.go b/pkg/buses/cache_test.go new file mode 100644 index 00000000000..f8403bbce18 --- /dev/null +++ b/pkg/buses/cache_test.go @@ -0,0 +1,135 @@ +/* +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 buses_test + +import ( + "testing" + + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" + "github.com/knative/eventing/pkg/buses" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + cacheDefaultNamespace = "default" + cacheTestChannel = "test-channel" + cacheTestSubscription = "test-subscription" +) + +func TestStoreErrsForUnknownChannel(t *testing.T) { + cache := buses.NewCache() + channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) + var expected *channelsv1alpha1.Channel + actual, err := cache.Channel(channelRef) + if err == nil { + t.Errorf("%s expected: %+v got: %+v", "Error", "", err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Unexpected channel", nil, actual) + } +} + +func TestStoreRetrievesKnownChannel(t *testing.T) { + cache := buses.NewCache() + channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) + expected := makeChannel(channelRef) + cache.AddChannel(expected) + actual, err := cache.Channel(channelRef) + if err != nil { + t.Errorf("%s expected: %+v got: %+v", "Unexpected error", nil, err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Channel", expected, actual) + } +} + +func TestStoreRemovesKnownChannel(t *testing.T) { + cache := buses.NewCache() + channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) + channel := makeChannel(channelRef) + cache.AddChannel(channel) + cache.RemoveChannel(channel) + var expected *channelsv1alpha1.Channel + actual, err := cache.Channel(channelRef) + if err == nil { + t.Errorf("%s expected: %+v got: %+v", "Unexpected error", nil, err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Channel", expected, actual) + } +} + +func TestStoreErrsForUnknownSubscription(t *testing.T) { + cache := buses.NewCache() + subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) + var expected *channelsv1alpha1.Subscription + actual, err := cache.Subscription(subscriptionRef) + if err == nil { + t.Errorf("%s expected: %+v got: %+v", "Error", "", err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Unexpected subscription", nil, actual) + } +} + +func TestStoreRetrievesKnownSubscription(t *testing.T) { + cache := buses.NewCache() + subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) + expected := makeSubscription(subscriptionRef) + cache.AddSubscription(expected) + actual, err := cache.Subscription(subscriptionRef) + if err != nil { + t.Errorf("%s expected: %+v got: %+v", "Unexpected error", nil, err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Subscription", expected, actual) + } +} + +func TestStoreRemovesKnownSubscription(t *testing.T) { + cache := buses.NewCache() + subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) + subscription := makeSubscription(subscriptionRef) + cache.AddSubscription(subscription) + cache.RemoveSubscription(subscription) + var expected *channelsv1alpha1.Subscription + actual, err := cache.Subscription(subscriptionRef) + if err == nil { + t.Errorf("%s expected: %+v got: %+v", "Unexpected error", nil, err) + } + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "Subscription", expected, actual) + } +} + +func makeChannel(channelRef buses.ChannelReference) *channelsv1alpha1.Channel { + return &channelsv1alpha1.Channel{ + ObjectMeta: metav1.ObjectMeta{ + Name: channelRef.Name, + Namespace: channelRef.Namespace, + }, + } +} + +func makeSubscription(subscriptionRef buses.SubscriptionReference) *channelsv1alpha1.Subscription { + return &channelsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: subscriptionRef.Name, + Namespace: subscriptionRef.Namespace, + }, + } +} diff --git a/pkg/buses/gcppubsub/bus.go b/pkg/buses/gcppubsub/bus.go index 57ae3e50015..6fe26ef1554 100644 --- a/pkg/buses/gcppubsub/bus.go +++ b/pkg/buses/gcppubsub/bus.go @@ -19,139 +19,100 @@ package gcppubsub import ( "context" "fmt" - "strings" "cloud.google.com/go/pubsub" "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" "github.com/knative/eventing/pkg/buses" ) -type PubSubBus struct { - name string - monitor *buses.Monitor - messageReceiver *buses.MessageReceiver - messageDispatcher *buses.MessageDispatcher - pubsubClient *pubsub.Client - receivers map[string]context.CancelFunc +type CloudPubSubBus struct { + busRef buses.BusReference + reconciler *buses.Reconciler + dispatcher buses.BusDispatcher + provisioner buses.BusProvisioner + pubsubClient *pubsub.Client + receivers map[string]context.CancelFunc } -func (b *PubSubBus) CreateTopic(channel *channelsv1alpha1.Channel, parameters buses.ResolvedParameters) error { +func NewCloudPubSubBusDispatcher(busRef buses.BusReference, projectID string, opts *buses.BusOpts) (*CloudPubSubBus, error) { ctx := context.Background() - - topicID := b.topicID(channel) - topic := b.pubsubClient.Topic(topicID) - - // check if topic exists before creating - if exists, err := topic.Exists(ctx); err != nil { - return err - } else if exists { - return nil - } - - glog.Infof("Create topic %q\n", topicID) - topic, err := b.pubsubClient.CreateTopic(ctx, topicID) + pubsubClient, err := pubsub.NewClient(ctx, projectID) if err != nil { - return err + return nil, err } - return nil -} - -func (b *PubSubBus) DeleteTopic(channel *channelsv1alpha1.Channel) error { - ctx := context.Background() - - topicID := b.topicID(channel) - topic := b.pubsubClient.Topic(topicID) - - // check if topic exists before deleting - if exists, err := topic.Exists(ctx); err != nil { - return err - } else if !exists { - return nil + bus := &CloudPubSubBus{ + busRef: busRef, + pubsubClient: pubsubClient, } - - glog.Infof("Delete topic %q\n", topicID) - return topic.Delete(ctx) + eventHandlers := buses.EventHandlerFuncs{ + SubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { + return bus.startReceivingEvents(subscriptionRef, parameters) + }, + UnsubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference) error { + bus.stopReceivingEvents(subscriptionRef) + return nil + }, + ReceiveMessageFunc: func(channelRef buses.ChannelReference, messgae *buses.Message) error { + return bus.sendEventToTopic(channelRef, messgae) + }, + } + bus.dispatcher = buses.NewBusDispatcher(busRef, eventHandlers, opts) + bus.reconciler = opts.Reconciler + bus.receivers = make(map[string]context.CancelFunc) + return bus, nil } -func (b *PubSubBus) CreateOrUpdateSubscription(sub *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { +func NewCloudPubSubBusProvisioner(busRef buses.BusReference, projectID string, opts *buses.BusOpts) (*CloudPubSubBus, error) { ctx := context.Background() - - subscriptionID := b.subscriptionID(sub) - subscription := b.pubsubClient.Subscription(subscriptionID) - - // check if subscription exists before creating - if exists, err := subscription.Exists(ctx); err != nil { - return err - } else if exists { - // TODO update subscription configuration - // _, err := subscription.Update(b.ctx, pubsub.SubscriptionConfigToUpdate{}) - // return err - return nil + pubsubClient, err := pubsub.NewClient(ctx, projectID) + if err != nil { + return nil, err } - // create subscription - channel := b.monitor.Channel(sub.Spec.Channel, sub.Namespace) - if channel == nil { - return fmt.Errorf("Cannot create a Subscription for unknown Channel %q", sub.Spec.Channel) + bus := &CloudPubSubBus{ + busRef: busRef, + pubsubClient: pubsubClient, } - topicID := b.topicID(channel) - topic := b.pubsubClient.Topic(topicID) - glog.Infof("Create subscription %q for topic %q\n", subscriptionID, topicID) - subscription, err := b.pubsubClient.CreateSubscription(ctx, subscriptionID, pubsub.SubscriptionConfig{ - Topic: topic, - }) - return err -} - -func (b *PubSubBus) DeleteSubscription(sub *channelsv1alpha1.Subscription) error { - ctx := context.Background() - - subscriptionID := b.subscriptionID(sub) - subscription := b.pubsubClient.Subscription(subscriptionID) - - // check if subscription exists before deleting - if exists, err := subscription.Exists(ctx); err != nil { - return err - } else if !exists { - return nil + eventHandlers := buses.EventHandlerFuncs{ + ProvisionFunc: func(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) error { + return bus.createTopic(channelRef, parameters) + }, + UnprovisionFunc: func(channelRef buses.ChannelReference) error { + return bus.deleteTopic(channelRef) + }, + SubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { + return bus.createOrUpdateSubscription(channelRef, subscriptionRef, parameters) + }, + UnsubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference) error { + return bus.deleteSubscription(subscriptionRef) + }, } - - glog.Infof("Deleting subscription %q\n", subscriptionID) - return subscription.Delete(ctx) + bus.provisioner = buses.NewBusProvisioner(busRef, eventHandlers, opts) + return bus, nil } -func (b *PubSubBus) SendEventToTopic(channel *channelsv1alpha1.Channel, message *buses.Message) error { - ctx := context.Background() - - topicID := b.topicID(channel) - topic := b.pubsubClient.Topic(topicID) - - result := topic.Publish(ctx, &pubsub.Message{ - Data: message.Payload, - Attributes: message.Headers, - }) - id, err := result.Get(ctx) - if err != nil { - return err +func (b *CloudPubSubBus) Run(threadness int, stopCh <-chan struct{}) { + if b.dispatcher != nil { + b.dispatcher.Run(threadness, stopCh) + } + if b.provisioner != nil { + b.provisioner.Run(threadness, stopCh) } - - // TODO allow topics to be reused between publish events, call .Stop after an idle period - topic.Stop() - - glog.Infof("Published a message to %s; msg ID: %v\n", topicID, id) - return nil } -func (b *PubSubBus) ReceiveEvents(sub *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { +// startReceivingEvents will receive events from a Cloud Pub/Sub Subscription for a +// Knative Subscription. This method will not block, but will continue to +// receive events until either this method or StopReceivingEvents is called for +// the same Subscription. +func (b *CloudPubSubBus) startReceivingEvents(subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { ctx := context.Background() cctx, cancel := context.WithCancel(ctx) // cancel current subscription receiver, if any - b.StopReceiveEvents(sub) + b.stopReceivingEvents(subscriptionRef) - subscriptionID := b.subscriptionID(sub) + subscriptionID := b.subscriptionID(subscriptionRef) subscription := b.pubsubClient.Subscription(subscriptionID) b.receivers[subscriptionID] = cancel @@ -166,21 +127,16 @@ func (b *PubSubBus) ReceiveEvents(sub *channelsv1alpha1.Subscription, parameters go func() { glog.Infof("Start receiving events for subscription %q\n", subscriptionID) err := subscription.Receive(cctx, func(ctx context.Context, pubsubMessage *pubsub.Message) { - subscriber := sub.Spec.Subscriber message := &buses.Message{ Headers: pubsubMessage.Attributes, Payload: pubsubMessage.Data, } - defaults := buses.DispatchDefaults{ - Namespace: sub.Namespace, - ReplyTo: sub.Spec.ReplyTo, - } - err := b.messageDispatcher.DispatchMessage(message, subscriber, defaults) + err := b.dispatcher.DispatchMessage(subscriptionRef, message) if err != nil { - glog.Warningf("Unable to dispatch event %q to %q", pubsubMessage.ID, subscriber) + glog.Warningf("Unable to dispatch event %q to %q", pubsubMessage.ID, subscriptionRef.String()) pubsubMessage.Nack() } else { - glog.Infof("Dispatched event %q to %q", pubsubMessage.ID, subscriber) + glog.Infof("Dispatched event %q to %q", pubsubMessage.ID, subscriptionRef.String()) pubsubMessage.Ack() } }) @@ -188,71 +144,141 @@ func (b *PubSubBus) ReceiveEvents(sub *channelsv1alpha1.Subscription, parameters glog.Errorf("Error receiving messesages for %q: %v\n", subscriptionID, err) } delete(b.receivers, subscriptionID) - b.monitor.RequeueSubscription(sub) + b.reconciler.RequeueSubscription(subscriptionRef) }() return nil } -func (b *PubSubBus) StopReceiveEvents(subscription *channelsv1alpha1.Subscription) error { - subscriptionID := b.subscriptionID(subscription) +// stopReceivingEvents stops receiving events for a previous call to +// StartReceivingEvents. Calls for a Subscription that is not not actively receiving +// are ignored. +func (b *CloudPubSubBus) stopReceivingEvents(subscriptionRef buses.SubscriptionReference) { + subscriptionID := b.subscriptionID(subscriptionRef) if cancel, ok := b.receivers[subscriptionID]; ok { glog.Infof("Stop receiving events for subscription %q\n", subscriptionID) cancel() delete(b.receivers, subscriptionID) } - return nil } -func (b *PubSubBus) topicID(channel *channelsv1alpha1.Channel) string { - return fmt.Sprintf("channel-%s-%s-%s", b.name, channel.Namespace, channel.Name) -} +// sendEventToTopic sends a message to the Cloud Pub/Sub Topic backing the +// Channel. +func (b *CloudPubSubBus) sendEventToTopic(channelRef buses.ChannelReference, message *buses.Message) error { + ctx := context.Background() + + topicID := b.topicID(channelRef) + topic := b.pubsubClient.Topic(topicID) -func (b *PubSubBus) subscriptionID(subscription *channelsv1alpha1.Subscription) string { - return fmt.Sprintf("subscription-%s-%s-%s", b.name, subscription.Namespace, subscription.Name) + result := topic.Publish(ctx, &pubsub.Message{ + Data: message.Payload, + Attributes: message.Headers, + }) + id, err := result.Get(ctx) + if err != nil { + return err + } + + // TODO allow topics to be reused between publish events, call .Stop after an idle period + topic.Stop() + + glog.Infof("Published a message to %s; msg ID: %v\n", topicID, id) + return nil } -func (b *PubSubBus) ReceiveMessage(channel *buses.ChannelReference, message *buses.Message) error { - c := b.monitor.Channel(channel.Name, channel.Namespace) - if c == nil { - return buses.ErrUnknownChannel +// createTopic creates a Topic in Cloud Pub/Sub for the Channel. +func (b *CloudPubSubBus) createTopic(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) error { + ctx := context.Background() + + topicID := b.topicID(channelRef) + topic := b.pubsubClient.Topic(topicID) + + // check if topic exists before creating + if exists, err := topic.Exists(ctx); err != nil { + return err + } else if exists { + return nil } - err := b.SendEventToTopic(c, message) + glog.Infof("Create topic %q\n", topicID) + topic, err := b.pubsubClient.CreateTopic(ctx, topicID) if err != nil { - return fmt.Errorf("unable to send event to topic %q: %v", channel.Name, err) + return err } return nil } -func (b *PubSubBus) splitChannelName(host string) (string, string) { - chunks := strings.Split(host, ".") - channel := chunks[0] - namespace := chunks[1] - return channel, namespace +// deleteTopic deletes the Topic in Cloud Pub/Sub for the Channel. +func (b *CloudPubSubBus) deleteTopic(channelRef buses.ChannelReference) error { + ctx := context.Background() + + topicID := b.topicID(channelRef) + topic := b.pubsubClient.Topic(topicID) + + // check if topic exists before deleting + if exists, err := topic.Exists(ctx); err != nil { + return err + } else if !exists { + return nil + } + + glog.Infof("Delete topic %q\n", topicID) + return topic.Delete(ctx) } -func NewPubSubBus( - name string, projectID string, - monitor *buses.Monitor, - messageReceiver *buses.MessageReceiver, - messageDispatcher *buses.MessageDispatcher, -) (*PubSubBus, error) { +// createOrUpdateSubscription creates a Subscription in Cloud Pub/Sub for the +// Knative Subscription, or idempotently updates a Subscription if it already +// exists. +func (b *CloudPubSubBus) createOrUpdateSubscription(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { ctx := context.Background() - pubsubClient, err := pubsub.NewClient(ctx, projectID) - if err != nil { - return nil, err + + subscriptionID := b.subscriptionID(subscriptionRef) + subscription := b.pubsubClient.Subscription(subscriptionID) + + // check if subscription exists before creating + if exists, err := subscription.Exists(ctx); err != nil { + return err + } else if exists { + // TODO update subscription configuration + // _, err := subscription.Update(b.ctx, pubsub.SubscriptionConfigToUpdate{}) + // return err + return nil } - bus := PubSubBus{ - name: name, - monitor: monitor, - messageReceiver: messageReceiver, - messageDispatcher: messageDispatcher, - pubsubClient: pubsubClient, - receivers: map[string]context.CancelFunc{}, + // create subscription + topicID := b.topicID(channelRef) + topic := b.pubsubClient.Topic(topicID) + glog.Infof("Create subscription %q for topic %q\n", subscriptionID, topicID) + subscription, err := b.pubsubClient.CreateSubscription(ctx, subscriptionID, pubsub.SubscriptionConfig{ + Topic: topic, + }) + return err +} + +// deleteSubscription removes a Subscription from Cloud Pub/Sub for a Knative +// Subscription. +func (b *CloudPubSubBus) deleteSubscription(subscriptionRef buses.SubscriptionReference) error { + ctx := context.Background() + + subscriptionID := b.subscriptionID(subscriptionRef) + subscription := b.pubsubClient.Subscription(subscriptionID) + + // check if subscription exists before deleting + if exists, err := subscription.Exists(ctx); err != nil { + return err + } else if !exists { + return nil } - return &bus, nil + glog.Infof("Deleting subscription %q\n", subscriptionID) + return subscription.Delete(ctx) +} + +func (b *CloudPubSubBus) topicID(channelRef buses.ChannelReference) string { + return fmt.Sprintf("channel-%s-%s-%s", b.busRef.Name, channelRef.Namespace, channelRef.Name) +} + +func (b *CloudPubSubBus) subscriptionID(subscriptionRef buses.SubscriptionReference) string { + return fmt.Sprintf("subscription-%s-%s-%s", b.busRef.Name, subscriptionRef.Namespace, subscriptionRef.Name) } diff --git a/pkg/buses/stub_test.go b/pkg/buses/gcppubsub/bus_test.go similarity index 97% rename from pkg/buses/stub_test.go rename to pkg/buses/gcppubsub/bus_test.go index a22070d5831..326a3924cc8 100644 --- a/pkg/buses/stub_test.go +++ b/pkg/buses/gcppubsub/bus_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package buses +package gcppubsub import "testing" diff --git a/pkg/buses/gcppubsub/dispatcher/main.go b/pkg/buses/gcppubsub/dispatcher/main.go index fc910e2f567..272caa40b42 100644 --- a/pkg/buses/gcppubsub/dispatcher/main.go +++ b/pkg/buses/gcppubsub/dispatcher/main.go @@ -18,69 +18,43 @@ package main import ( "flag" - "fmt" "os" "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" "github.com/knative/eventing/pkg/buses" "github.com/knative/eventing/pkg/buses/gcppubsub" "github.com/knative/pkg/signals" ) const ( - threadsPerMonitor = 1 -) - -var ( - masterURL string - kubeconfig string + threadsPerReconciler = 1 ) func main() { defer glog.Flush() - flag.Parse() - - // set up signals so we handle the first shutdown signal gracefully - stopCh := signals.SetupSignalHandler() + busRef := buses.NewBusReferenceFromNames( + os.Getenv("BUS_NAME"), + os.Getenv("BUS_NAMESPACE"), + ) - namespace := os.Getenv("BUS_NAMESPACE") - name := os.Getenv("BUS_NAME") projectID := os.Getenv("GOOGLE_CLOUD_PROJECT") if projectID == "" { - glog.Fatalf("GOOGLE_CLOUD_PROJECT environment variable must be set.\n") + glog.Fatalf("GOOGLE_CLOUD_PROJECT environment variable must be set") } - component := fmt.Sprintf("%s-%s", name, buses.Dispatcher) + opts := &buses.BusOpts{} - var bus *gcppubsub.PubSubBus - monitor := buses.NewMonitor(component, masterURL, kubeconfig, buses.MonitorEventHandlerFuncs{ - SubscribeFunc: func(subscription *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { - return bus.ReceiveEvents(subscription, parameters) - }, - UnsubscribeFunc: func(subscription *channelsv1alpha1.Subscription) error { - return bus.StopReceiveEvents(subscription) - }, - }) - messageReceiver := buses.NewMessageReceiver(func(channel *buses.ChannelReference, message *buses.Message) error { - return bus.ReceiveMessage(channel, message) - }) - messageDispatcher := buses.NewMessageDispatcher() - bus, err := gcppubsub.NewPubSubBus(name, projectID, monitor, messageReceiver, messageDispatcher) + flag.StringVar(&opts.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + flag.Parse() + + bus, err := gcppubsub.NewCloudPubSubBusDispatcher(busRef, projectID, opts) if err != nil { - glog.Fatalf("Failed to create pubsub bus: %v", err) + glog.Fatalf("Error starting pub/sub bus dispatcher: %v", err) } - go func() { - if err := monitor.Run(namespace, name, threadsPerMonitor, stopCh); err != nil { - glog.Fatalf("Error running monitor: %s", err.Error()) - } - }() - messageReceiver.Run(stopCh) -} - -func init() { - flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + bus.Run(threadsPerReconciler, stopCh) } diff --git a/pkg/buses/gcppubsub/provisioner/main.go b/pkg/buses/gcppubsub/provisioner/main.go index 2ad141bdbea..6e3f62f2d87 100644 --- a/pkg/buses/gcppubsub/provisioner/main.go +++ b/pkg/buses/gcppubsub/provisioner/main.go @@ -18,68 +18,43 @@ package main import ( "flag" - "fmt" "os" "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" "github.com/knative/eventing/pkg/buses" "github.com/knative/eventing/pkg/buses/gcppubsub" "github.com/knative/pkg/signals" ) const ( - threadsPerMonitor = 1 -) - -var ( - masterURL string - kubeconfig string + threadsPerReconciler = 1 ) func main() { defer glog.Flush() - flag.Parse() + busRef := buses.NewBusReferenceFromNames( + os.Getenv("BUS_NAME"), + os.Getenv("BUS_NAMESPACE"), + ) - // set up signals so we handle the first shutdown signal gracefully - stopCh := signals.SetupSignalHandler() - - namespace := os.Getenv("BUS_NAMESPACE") - name := os.Getenv("BUS_NAME") projectID := os.Getenv("GOOGLE_CLOUD_PROJECT") if projectID == "" { - glog.Fatalf("GOOGLE_CLOUD_PROJECT environment variable must be set.\n") + glog.Fatalf("GOOGLE_CLOUD_PROJECT environment variable must be set") } - component := fmt.Sprintf("%s-%s", name, buses.Provisioner) + opts := &buses.BusOpts{} - var bus *gcppubsub.PubSubBus - monitor := buses.NewMonitor(component, masterURL, kubeconfig, buses.MonitorEventHandlerFuncs{ - ProvisionFunc: func(channel *channelsv1alpha1.Channel, parameters buses.ResolvedParameters) error { - return bus.CreateTopic(channel, parameters) - }, - UnprovisionFunc: func(channel *channelsv1alpha1.Channel) error { - return bus.DeleteTopic(channel) - }, - SubscribeFunc: func(subscription *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { - return bus.CreateOrUpdateSubscription(subscription, parameters) - }, - UnsubscribeFunc: func(subscription *channelsv1alpha1.Subscription) error { - return bus.DeleteSubscription(subscription) - }, - }) - bus, err := gcppubsub.NewPubSubBus(name, projectID, monitor, nil, nil) - if err != nil { - glog.Fatalf("Failed to create pubsub bus: %v", err) - } + flag.StringVar(&opts.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + flag.Parse() - if err := monitor.Run(namespace, name, threadsPerMonitor, stopCh); err != nil { - glog.Fatalf("Error running monitor: %s", err.Error()) + bus, err := gcppubsub.NewCloudPubSubBusProvisioner(busRef, projectID, opts) + if err != nil { + glog.Fatalf("Error starting pub/sub bus provisioner: %v", err) } -} -func init() { - flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + bus.Run(threadsPerReconciler, stopCh) } diff --git a/pkg/buses/handler_funcs.go b/pkg/buses/handler_funcs.go new file mode 100644 index 00000000000..41e320b4c7f --- /dev/null +++ b/pkg/buses/handler_funcs.go @@ -0,0 +1,258 @@ +/* + * 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 buses + +import ( + "fmt" + + "github.com/golang/glog" + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" + "github.com/knative/eventing/pkg/controller/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" +) + +const ( + // SuccessSynced is used as part of the Event 'reason' when a resource is synced + successSynced = "Synced" + // ErrResourceSync is used as part of the Event 'reason' when a resource fails + // to sync. + errResourceSync = "ErrResourceSync" +) + +// ResolvedParameters is a map containing parameter names and the resolved +// values from attributes or parameter defaults. +type ResolvedParameters = map[string]string + +// EventHandlerFuncs is a set of handler functions that are called when a bus +// requires sync, channels are provisioned/unprovisioned, or a subscription is +// created or deleted, or if one of the relevant resources is changed. +type EventHandlerFuncs struct { + // BusFunc is invoked when the Bus requires sync. + BusFunc func(bus channelsv1alpha1.GenericBus) error + + // ProvisionFunc is invoked when a new Channel should be provisioned or when + // the attributes change. + ProvisionFunc func(channelRef ChannelReference, parameters ResolvedParameters) error + + // UnprovisionFunc in invoked when a Channel should be deleted. + UnprovisionFunc func(channelRef ChannelReference) error + + // SubscribeFunc is invoked when a new Subscription should be set up or when + // the attributes change. + SubscribeFunc func(channelRef ChannelReference, subscriptionRef SubscriptionReference, parameters ResolvedParameters) error + + // UnsubscribeFunc is invoked when a Subscription should be deleted. + UnsubscribeFunc func(channelRef ChannelReference, subscriptionRef SubscriptionReference) error + + // ReceiveMessageFunc is invoked when a Message is received on a Channel + ReceiveMessageFunc func(channelRef ChannelReference, message *Message) error +} + +func (h EventHandlerFuncs) onBus(bus channelsv1alpha1.GenericBus, reconciler *Reconciler) error { + if h.BusFunc != nil { + err := h.BusFunc(bus) + if err != nil { + reconciler.RecordBusEventf(corev1.EventTypeWarning, errResourceSync, "Error syncing Bus: %s", err) + } else { + reconciler.RecordBusEventf(corev1.EventTypeNormal, successSynced, "Bus synched successfully") + } + return err + } + return nil +} + +func (h EventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { + if h.ProvisionFunc != nil { + parameters, err := h.resolveChannelParameters(reconciler.bus.GetSpec(), channel.Spec) + if err != nil { + return err + } + channelRef := NewChannelReference(channel) + err = h.ProvisionFunc(channelRef, parameters) + channelCopy := channel.DeepCopy() + var cond *channelsv1alpha1.ChannelCondition + if err != nil { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error provisioning channel: %s", err) + cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionFalse, errResourceSync, err.Error()) + } else { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel provisioned successfully") + cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionTrue, successSynced, "Channel provisioned successfully") + } + util.SetChannelCondition(&channelCopy.Status, *cond) + util.ConsolidateChannelCondition(&channelCopy.Status) + if !equality.Semantic.DeepEqual(channel.Status, channelCopy.Status) { + // status has changed, update channel + _, errS := reconciler.eventingClient.ChannelsV1alpha1().Channels(channel.Namespace).Update(channelCopy) + if errS != nil { + glog.Warningf("Could not update channel status: %v", errS) + if err != nil { + // return the original err, since it is more meaningful + return err + } + return errS + } + } + return err + } + return nil +} + +func (h EventHandlerFuncs) onUnprovision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { + if h.UnprovisionFunc != nil { + channelRef := NewChannelReference(channel) + err := h.UnprovisionFunc(channelRef) + if err != nil { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error unprovisioning channel: %s", err) + } else { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") + } + // skip updating status conditions since the channel was deleted + return err + } + return nil +} + +func (h EventHandlerFuncs) onSubscribe(subscription *channelsv1alpha1.Subscription, reconciler *Reconciler) error { + if h.SubscribeFunc != nil { + parameters, err := h.resolveSubscriptionParameters(reconciler.bus.GetSpec(), subscription.Spec) + if err != nil { + return err + } + channelRef := NewChannelReferenceFromSubscription(subscription) + subscriptionRef := NewSubscriptionReference(subscription) + err = h.SubscribeFunc(channelRef, subscriptionRef, parameters) + subscriptionCopy := subscription.DeepCopy() + var cond *channelsv1alpha1.SubscriptionCondition + if err != nil { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error subscribing: %s", err) + cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionFalse, errResourceSync, err.Error()) + } else { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Subscribed successfully") + cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionTrue, successSynced, "Subscription dispatcher successfully created") + } + util.SetSubscriptionCondition(&subscriptionCopy.Status, *cond) + if !equality.Semantic.DeepEqual(subscription.Status, subscriptionCopy.Status) { + // status has changed, update subscription + _, errS := reconciler.eventingClient.ChannelsV1alpha1().Subscriptions(subscription.Namespace).Update(subscriptionCopy) + if errS != nil { + glog.Warningf("Could not update subscription status: %v", errS) + if err != nil { + // return the original err, since it is more meaningful + return err + } + return errS + } + } + return err + } + return nil +} + +func (h EventHandlerFuncs) onUnsubscribe(subscription *channelsv1alpha1.Subscription, reconciler *Reconciler) error { + if h.UnsubscribeFunc != nil { + channelRef := NewChannelReferenceFromSubscription(subscription) + subscriptionRef := NewSubscriptionReference(subscription) + err := h.UnsubscribeFunc(channelRef, subscriptionRef) + if err != nil { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error unsubscribing: %s", err) + } else { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") + } + // skip updating status conditions since the subscription was deleted + return err + } + return nil +} + +func (h EventHandlerFuncs) onReceiveMessage(channelRef ChannelReference, message *Message) error { + if h.ReceiveMessageFunc == nil { + // TODO use a static error + return fmt.Errorf("unable to dispatch message") + } + return h.ReceiveMessageFunc(channelRef, message) +} + +// resolveChannelParameters resolves the given Channel Parameters and the Bus' +// Channel Parameters, returning an ResolvedParameters or an Error. +func (h EventHandlerFuncs) resolveChannelParameters(bus *channelsv1alpha1.BusSpec, channel channelsv1alpha1.ChannelSpec) (ResolvedParameters, error) { + genericBusParameters := bus.Parameters + var parameters *[]channelsv1alpha1.Parameter + if genericBusParameters != nil { + parameters = genericBusParameters.Channel + } + return h.resolveParameters(parameters, channel.Arguments) +} + +// resolveSubscriptionParameters resolves the given Subscription Parameters and +// the Bus' Subscription Parameters, returning an Attributes or an Error. +func (h EventHandlerFuncs) resolveSubscriptionParameters(bus *channelsv1alpha1.BusSpec, subscription channelsv1alpha1.SubscriptionSpec) (ResolvedParameters, error) { + genericBusParameters := bus.Parameters + var parameters *[]channelsv1alpha1.Parameter + if genericBusParameters != nil { + parameters = genericBusParameters.Subscription + } + return h.resolveParameters(parameters, subscription.Arguments) +} + +// resolveParameters resolves a slice of Parameters and a slice of arguments and +// returns an Attributes or an error if there are missing Arguments. Each +// Parameter represents a variable that must be provided by an Argument or +// optionally defaulted if a default value for the Parameter is specified. +// resolveAttributes combines the given arrays of Parameters and Arguments, +// using default values where necessary and returning an error if there are +// missing Arguments. +func (h EventHandlerFuncs) resolveParameters(parameters *[]channelsv1alpha1.Parameter, arguments *[]channelsv1alpha1.Argument) (ResolvedParameters, error) { + resolved := make(ResolvedParameters) + known := make(map[string]interface{}) + required := make(map[string]interface{}) + + // apply parameters + if parameters != nil { + for _, param := range *parameters { + known[param.Name] = true + if param.Default != nil { + resolved[param.Name] = *param.Default + } else { + required[param.Name] = true + } + } + } + // apply arguments + if arguments != nil { + for _, arg := range *arguments { + if _, ok := known[arg.Name]; !ok { + // ignore arguments not defined by parameters + glog.Warningf("Skipping unknown argument: %s\n", arg.Name) + continue + } + delete(required, arg.Name) + resolved[arg.Name] = arg.Value + } + } + + // check for missing arguments + if len(required) != 0 { + missing := []string{} + for name := range required { + missing = append(missing, name) + } + return nil, fmt.Errorf("missing required arguments: %v", missing) + } + + return resolved, nil +} diff --git a/pkg/buses/kafka/bus.go b/pkg/buses/kafka/bus.go new file mode 100644 index 00000000000..f8df67db527 --- /dev/null +++ b/pkg/buses/kafka/bus.go @@ -0,0 +1,276 @@ +/* + * 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 kafka + +import ( + "fmt" + "strconv" + + "github.com/Shopify/sarama" + cluster "github.com/bsm/sarama-cluster" + "github.com/golang/glog" + "github.com/knative/eventing/pkg/buses" +) + +const ( + initialOffset = "initialOffset" + numPartitions = "NumPartitions" + newest = "Newest" + oldest = "Oldest" +) + +type KafkaBus struct { + busRef buses.BusReference + dispatcher buses.BusDispatcher + provisioner buses.BusProvisioner + kafkaBrokers []string + kafkaClusterAdmin sarama.ClusterAdmin + kafkaAsyncProducer sarama.AsyncProducer + kafkaConsumers map[buses.SubscriptionReference]*cluster.Consumer +} + +func NewKafkaBusDispatcher(busRef buses.BusReference, brokers []string, opts *buses.BusOpts) (*KafkaBus, error) { + bus := &KafkaBus{ + busRef: busRef, + kafkaBrokers: brokers, + } + eventHandlers := buses.EventHandlerFuncs{ + SubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { + return bus.subscribe(channelRef, subscriptionRef, parameters) + }, + UnsubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference) error { + return bus.unsubscribe(channelRef, subscriptionRef) + }, + ReceiveMessageFunc: func(channelRef buses.ChannelReference, message *buses.Message) error { + bus.kafkaAsyncProducer.Input() <- toKafkaMessage(channelRef, message) + return nil + }, + } + bus.dispatcher = buses.NewBusDispatcher(busRef, eventHandlers, opts) + bus.kafkaConsumers = make(map[buses.SubscriptionReference]*cluster.Consumer) + + conf := sarama.NewConfig() + conf.Version = sarama.V1_1_0_0 + conf.ClientID = busRef.Name + "-dispatcher" + client, err := sarama.NewClient(brokers, conf) + if err != nil { + return nil, fmt.Errorf("unable to create kafka client: %v", err) + } + producer, err := sarama.NewAsyncProducerFromClient(client) + if err != nil { + return nil, fmt.Errorf("unable to create kafka producer: %v", err) + } + bus.kafkaAsyncProducer = producer + + return bus, nil +} + +func NewKafkaBusProvisioner(busRef buses.BusReference, brokers []string, opts *buses.BusOpts) (*KafkaBus, error) { + bus := &KafkaBus{ + busRef: busRef, + kafkaBrokers: brokers, + } + eventHandlers := buses.EventHandlerFuncs{ + ProvisionFunc: func(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) error { + return bus.provision(channelRef, parameters) + }, + UnprovisionFunc: func(channelRef buses.ChannelReference) error { + return bus.unprovision(channelRef) + }, + } + bus.provisioner = buses.NewBusProvisioner(busRef, eventHandlers, opts) + + conf := sarama.NewConfig() + conf.Version = sarama.V1_1_0_0 + conf.ClientID = busRef.Name + "-provisioner" + + clusterAdmin, err := sarama.NewClusterAdmin(brokers, conf) + if err != nil { + return nil, fmt.Errorf("unable to building kafka admin client: %v", err) + } + bus.kafkaClusterAdmin = clusterAdmin + + return bus, nil +} + +func (b *KafkaBus) Run(threadness int, stopCh <-chan struct{}) { + if b.kafkaAsyncProducer != nil { + go func() { + for { + select { + case e := <-b.kafkaAsyncProducer.Errors(): + glog.Warningf("Got %v", e) + case s := <-b.kafkaAsyncProducer.Successes(): + glog.Infof("Sent %v", s) + case <-stopCh: + return + } + } + }() + } + if b.dispatcher != nil { + b.dispatcher.Run(threadness, stopCh) + } + if b.provisioner != nil { + b.provisioner.Run(threadness, stopCh) + } +} + +func (b *KafkaBus) subscribe(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { + if _, ok := b.kafkaConsumers[subscriptionRef]; ok { + // subscribe can be called multiple times for the same subscription, + //unsubscribe before we resubscribe + err := b.unsubscribe(channelRef, subscriptionRef) + if err != nil { + return err + } + } + + glog.Infof("Subscribing %q to %q (%v)", subscriptionRef.String(), channelRef.String(), parameters) + + topicName := topicName(channelRef) + + initialOffset, err := resolveInitialOffset(parameters) + if err != nil { + return err + } + + group := fmt.Sprintf("%s.%s.%s", b.busRef.Name, subscriptionRef.Namespace, subscriptionRef.Name) + consumerConfig := cluster.NewConfig() + consumerConfig.Version = sarama.V1_1_0_0 + consumerConfig.Consumer.Offsets.Initial = initialOffset + consumer, err := cluster.NewConsumer(b.kafkaBrokers, group, []string{topicName}, consumerConfig) + if err != nil { + return err + } + + b.kafkaConsumers[subscriptionRef] = consumer + + go func() { + for { + msg, more := <-consumer.Messages() + if more { + glog.Infof("Dispatching a message for subscription %q", subscriptionRef.String()) + message := fromKafkaMessage(msg) + err := b.dispatcher.DispatchMessage(subscriptionRef, message) + if err != nil { + glog.Warningf("Got error trying to dispatch message: %v", err) + } + // TODO: handle errors with pluggable strategy + consumer.MarkOffset(msg, "") // Mark message as processed + } else { + break + } + } + glog.Infof("Consumer for subscription %q stopped", subscriptionRef.String()) + }() + + return nil +} + +func (b *KafkaBus) unsubscribe(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference) error { + glog.Infof("Un-Subscribing %q from %q", subscriptionRef.String(), channelRef.String()) + if consumer, ok := b.kafkaConsumers[subscriptionRef]; ok { + delete(b.kafkaConsumers, subscriptionRef) + return consumer.Close() + } + return nil +} + +func (b *KafkaBus) provision(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) error { + topicName := topicName(channelRef) + glog.Infof("Provisioning topic %s on bus backed by Kafka", topicName) + + partitions := 1 + if p, ok := parameters[numPartitions]; ok { + var err error + partitions, err = strconv.Atoi(p) + if err != nil { + glog.Warningf("Could not parse partition count for %q: %s", channelRef.String(), p) + } + } + + err := b.kafkaClusterAdmin.CreateTopic(topicName, &sarama.TopicDetail{ + ReplicationFactor: 1, + NumPartitions: int32(partitions), + }, false) + if err == sarama.ErrTopicAlreadyExists { + return nil + } else if err != nil { + glog.Errorf("Error creating topic %s: %v", topicName, err) + } else { + glog.Infof("Successfully created topic %s", topicName) + } + return err +} + +func (b *KafkaBus) unprovision(channelRef buses.ChannelReference) error { + topicName := topicName(channelRef) + glog.Infof("Un-provisioning topic %s from bus backed by Kafka", topicName) + + err := b.kafkaClusterAdmin.DeleteTopic(topicName) + if err == sarama.ErrUnknownTopicOrPartition { + return nil + } else if err != nil { + glog.Errorf("Error deleting topic %s: %v", topicName, err) + } else { + glog.Infof("Successfully deleted topic %s", topicName) + } + + return err +} + +func toKafkaMessage(channelRef buses.ChannelReference, message *buses.Message) *sarama.ProducerMessage { + kafkaMessage := sarama.ProducerMessage{ + Topic: topicName(channelRef), + Value: sarama.ByteEncoder(message.Payload), + } + for h, v := range message.Headers { + kafkaMessage.Headers = append(kafkaMessage.Headers, sarama.RecordHeader{ + Key: []byte(h), + Value: []byte(v), + }) + } + return &kafkaMessage +} + +func fromKafkaMessage(kafkaMessage *sarama.ConsumerMessage) *buses.Message { + headers := make(map[string]string) + for _, header := range kafkaMessage.Headers { + headers[string(header.Key)] = string(header.Value) + } + message := buses.Message{ + Headers: headers, + Payload: kafkaMessage.Value, + } + return &message +} + +func topicName(channel buses.ChannelReference) string { + return fmt.Sprintf("%s.%s", channel.Namespace, channel.Name) +} + +func resolveInitialOffset(parameters buses.ResolvedParameters) (int64, error) { + switch parameters[initialOffset] { + case oldest: + return sarama.OffsetOldest, nil + case newest: + return sarama.OffsetNewest, nil + default: + return 0, fmt.Errorf("unsupported initialOffset value. Must be one of %s or %s", oldest, newest) + } +} diff --git a/pkg/buses/kafka/dispatcher/stub_test.go b/pkg/buses/kafka/bus_test.go similarity index 98% rename from pkg/buses/kafka/dispatcher/stub_test.go rename to pkg/buses/kafka/bus_test.go index ee7bbe8c445..59652629577 100644 --- a/pkg/buses/kafka/dispatcher/stub_test.go +++ b/pkg/buses/kafka/bus_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package kafka import "testing" diff --git a/pkg/buses/kafka/dispatcher/dispatcher.go b/pkg/buses/kafka/dispatcher/dispatcher.go deleted file mode 100644 index 62538e6061a..00000000000 --- a/pkg/buses/kafka/dispatcher/dispatcher.go +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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 main - -import ( - "flag" - "fmt" - "log" - "net/http" - "os" - "strings" - - "github.com/Shopify/sarama" - "github.com/bsm/sarama-cluster" - "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" - "github.com/knative/eventing/pkg/buses" - informers "github.com/knative/eventing/pkg/client/informers/externalversions" - "github.com/knative/pkg/signals" -) - -const ( - InitialOffset = "initialOffset" - Newest = "Newest" - Oldest = "Oldest" -) - -type dispatcher struct { - monitor *buses.Monitor - messageReceiver *buses.MessageReceiver - messageDispatcher *buses.MessageDispatcher - brokers []string - consumers map[subscriptionKey]*cluster.Consumer - busName string - client *http.Client - producer sarama.AsyncProducer - informerFactory informers.SharedInformerFactory - namespace string -} - -type subscriptionKey struct { - name string - namespace string -} - -func main() { - - kubeconfig := flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - masterURL := flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") - flag.Parse() - - name := os.Getenv("BUS_NAME") - namespace := os.Getenv("BUS_NAMESPACE") - - brokers := strings.Split(os.Getenv("KAFKA_BROKERS"), ",") - if len(brokers) == 0 { - log.Fatalf("Environment variable KAFKA_BROKERS not set") - } - - conf := sarama.NewConfig() - conf.Version = sarama.V1_1_0_0 - conf.ClientID = name + "-dispatcher" - kafka_client, err := sarama.NewClient(brokers, conf) - if err != nil { - glog.Fatalf("Error building kafka client: %v", err) - } - - dispatcher, err := NewKafkaDispatcher(name, namespace, *masterURL, *kubeconfig, brokers, kafka_client) - if err != nil { - glog.Fatalf("Error building kafka provisioner: %v", err) - } - - stopCh := signals.SetupSignalHandler() - dispatcher.Start(stopCh) - - <-stopCh - glog.Flush() -} - -func NewKafkaDispatcher(name string, namespace string, masterURL string, kubeConfig string, brokers []string, client sarama.Client) (*dispatcher, error) { - - asyncProducer, err := sarama.NewAsyncProducerFromClient(client) - if err != nil { - return nil, err - } - - go func() { - for { - select { - case e := <-asyncProducer.Errors(): - glog.Warningf("Got %v", e) - case s := <-asyncProducer.Successes(): - glog.Infof("Sent %v", s) - } - } - }() - - d := dispatcher{ - busName: name, - namespace: namespace, - client: &http.Client{}, - brokers: brokers, - consumers: make(map[subscriptionKey]*cluster.Consumer), - producer: asyncProducer, - } - component := fmt.Sprintf("%s-%s", name, buses.Dispatcher) - monitor := buses.NewMonitor(component, masterURL, kubeConfig, buses.MonitorEventHandlerFuncs{ - SubscribeFunc: d.subscribe, - UnsubscribeFunc: d.unsubscribe, - }) - d.monitor = monitor - d.messageDispatcher = buses.NewMessageDispatcher() - d.messageReceiver = buses.NewMessageReceiver(d.handleEvent) - - return &d, nil -} - -func (d *dispatcher) Start(stopCh <-chan struct{}) { - go d.monitor.Run(d.namespace, d.busName, 2, stopCh) - go d.messageReceiver.Run(stopCh) -} - -func (d *dispatcher) subscribe(subscription *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { - glog.Infof("Subscribing %s/%s: %s -> %s (%v)", subscription.Namespace, - subscription.Name, subscription.Spec.Channel, subscription.Spec.Subscriber, parameters) - channel := &buses.ChannelReference{ - Name: subscription.Spec.Channel, - Namespace: subscription.Namespace, - } - topicName := topicName(channel) - - initialOffset, err := initialOffset(parameters) - if err != nil { - return err - } - - group := fmt.Sprintf("%s.%s.%s", d.busName, subscription.Namespace, subscription.Name) - consumerConfig := cluster.NewConfig() - consumerConfig.Version = sarama.V1_1_0_0 - consumerConfig.Consumer.Offsets.Initial = initialOffset - consumer, err := cluster.NewConsumer(d.brokers, group, []string{topicName}, consumerConfig) - if err != nil { - return err - } - - d.consumers[subscriptionKeyFor(subscription)] = consumer - - go func() { - for { - msg, more := <-consumer.Messages() - if more { - glog.Infof("Dispatching a message for subscription %s/%s: %s -> %s", subscription.Namespace, - subscription.Name, subscription.Spec.Channel, subscription.Spec.Subscriber) - message := fromKafkaMessage(msg) - defaults := buses.DispatchDefaults{ - Namespace: subscription.Namespace, - ReplyTo: subscription.Spec.ReplyTo, - } - err := d.messageDispatcher.DispatchMessage(message, subscription.Spec.Subscriber, defaults) - if err != nil { - glog.Warningf("Got error trying to dispatch message: %v", err) - } - // TODO: handle errors with pluggable strategy - consumer.MarkOffset(msg, "") // Mark message as processed - } else { - break - } - } - glog.Infof("Consumer for subscription %s/%s stopped", subscription.Namespace, subscription.Name) - }() - - return nil -} - -func initialOffset(parameters buses.ResolvedParameters) (int64, error) { - sInitial := parameters[InitialOffset] - if sInitial == Oldest { - return sarama.OffsetOldest, nil - } else if sInitial == Newest { - return sarama.OffsetNewest, nil - } else { - return 0, fmt.Errorf("unsupported initialOffset value. Must be one of %s or %s", Oldest, Newest) - } -} - -func (d *dispatcher) unsubscribe(subscription *channelsv1alpha1.Subscription) error { - glog.Infof("Un-Subscribing %s/%s: %s -> %s", subscription.Namespace, - subscription.Name, subscription.Spec.Channel, subscription.Spec.Subscriber) - key := subscriptionKeyFor(subscription) - if consumer, ok := d.consumers[key]; ok { - delete(d.consumers, key) - return consumer.Close() - } - return nil -} - -func subscriptionKeyFor(subscription *channelsv1alpha1.Subscription) subscriptionKey { - return subscriptionKey{name: subscription.Name, namespace: subscription.Namespace} -} - -func topicName(channel *buses.ChannelReference) string { - return fmt.Sprintf("%s.%s", channel.Namespace, channel.Name) -} - -func (d *dispatcher) handleEvent(channel *buses.ChannelReference, message *buses.Message) error { - if c := d.monitor.Channel(channel.Name, channel.Namespace); c == nil { - return buses.ErrUnknownChannel - } - - d.producer.Input() <- toKafkaMessage(channel, message) - - return nil -} - -func toKafkaMessage(channel *buses.ChannelReference, message *buses.Message) *sarama.ProducerMessage { - kafkaMessage := sarama.ProducerMessage{ - Topic: topicName(channel), - Value: sarama.ByteEncoder(message.Payload), - } - for h, v := range message.Headers { - kafkaMessage.Headers = append(kafkaMessage.Headers, sarama.RecordHeader{[]byte(h), []byte(v)}) - } - return &kafkaMessage -} - -func fromKafkaMessage(kafkaMessage *sarama.ConsumerMessage) *buses.Message { - headers := make(map[string]string) - for _, header := range kafkaMessage.Headers { - headers[string(header.Key)] = string(header.Value) - } - message := buses.Message{ - Headers: headers, - Payload: kafkaMessage.Value, - } - return &message -} diff --git a/pkg/buses/kafka/dispatcher/main.go b/pkg/buses/kafka/dispatcher/main.go new file mode 100644 index 00000000000..812706bd60a --- /dev/null +++ b/pkg/buses/kafka/dispatcher/main.go @@ -0,0 +1,64 @@ +/* + * 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 main + +import ( + "flag" + "log" + "os" + "strings" + + "github.com/Shopify/sarama" + "github.com/golang/glog" + "github.com/knative/eventing/pkg/buses" + "github.com/knative/eventing/pkg/buses/kafka" + "github.com/knative/pkg/signals" +) + +const ( + threadsPerReconciler = 1 +) + +func main() { + defer glog.Flush() + sarama.Logger = log.New(os.Stderr, "[Sarama] ", log.LstdFlags) + + busRef := buses.NewBusReferenceFromNames( + os.Getenv("BUS_NAME"), + os.Getenv("BUS_NAMESPACE"), + ) + + brokers := strings.Split(os.Getenv("KAFKA_BROKERS"), ",") + if len(brokers) == 0 { + log.Fatalf("Environment variable KAFKA_BROKERS not set") + } + + opts := &buses.BusOpts{} + + flag.StringVar(&opts.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + flag.Parse() + + bus, err := kafka.NewKafkaBusProvisioner(busRef, brokers, opts) + if err != nil { + glog.Fatalf("Error starting kafka bus provisioner: %v", err) + } + + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + bus.Run(threadsPerReconciler, stopCh) +} diff --git a/pkg/buses/kafka/provisioner/main.go b/pkg/buses/kafka/provisioner/main.go new file mode 100644 index 00000000000..812706bd60a --- /dev/null +++ b/pkg/buses/kafka/provisioner/main.go @@ -0,0 +1,64 @@ +/* + * 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 main + +import ( + "flag" + "log" + "os" + "strings" + + "github.com/Shopify/sarama" + "github.com/golang/glog" + "github.com/knative/eventing/pkg/buses" + "github.com/knative/eventing/pkg/buses/kafka" + "github.com/knative/pkg/signals" +) + +const ( + threadsPerReconciler = 1 +) + +func main() { + defer glog.Flush() + sarama.Logger = log.New(os.Stderr, "[Sarama] ", log.LstdFlags) + + busRef := buses.NewBusReferenceFromNames( + os.Getenv("BUS_NAME"), + os.Getenv("BUS_NAMESPACE"), + ) + + brokers := strings.Split(os.Getenv("KAFKA_BROKERS"), ",") + if len(brokers) == 0 { + log.Fatalf("Environment variable KAFKA_BROKERS not set") + } + + opts := &buses.BusOpts{} + + flag.StringVar(&opts.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + flag.Parse() + + bus, err := kafka.NewKafkaBusProvisioner(busRef, brokers, opts) + if err != nil { + glog.Fatalf("Error starting kafka bus provisioner: %v", err) + } + + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + bus.Run(threadsPerReconciler, stopCh) +} diff --git a/pkg/buses/kafka/provisioner/provisioner.go b/pkg/buses/kafka/provisioner/provisioner.go deleted file mode 100644 index 455eb1938c5..00000000000 --- a/pkg/buses/kafka/provisioner/provisioner.go +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 main - -import ( - "flag" - "fmt" - "log" - "os" - "strconv" - "strings" - - "github.com/Shopify/sarama" - "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" - "github.com/knative/eventing/pkg/buses" - informers "github.com/knative/eventing/pkg/client/informers/externalversions" - "github.com/knative/pkg/signals" -) - -const ( - NumPartitions = "NumPartitions" -) - -type provisioner struct { - client sarama.Client - admin sarama.ClusterAdmin - monitor *buses.Monitor - informerFactory informers.SharedInformerFactory - namespace string - name string -} - -func main() { - - sarama.Logger = log.New(os.Stderr, "[Sarama] ", log.LstdFlags) - - kubeconfig := flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - masterURL := flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") - flag.Parse() - - name := os.Getenv("BUS_NAME") - namespace := os.Getenv("BUS_NAMESPACE") - - brokers := strings.Split(os.Getenv("KAFKA_BROKERS"), ",") - if len(brokers) == 0 { - log.Fatalf("Environment variable KAFKA_BROKERS not set") - } - - conf := sarama.NewConfig() - conf.Version = sarama.V1_1_0_0 - conf.ClientID = name + "-provisioner" - - clusterAdmin, err := sarama.NewClusterAdmin(brokers, conf) - if err != nil { - glog.Fatalf("Error building kafka admin client: %s", err.Error()) - } - - provisioner, err := NewKafkaProvisioner(name, namespace, *masterURL, *kubeconfig, clusterAdmin) - if err != nil { - glog.Fatalf("Error building kafka provisioner: %s", err.Error()) - } - - stopCh := signals.SetupSignalHandler() - provisioner.Start(stopCh) - - <-stopCh - glog.Flush() -} - -func NewKafkaProvisioner(name string, namespace string, masterURL string, kubeconfig string, admin sarama.ClusterAdmin) (*provisioner, error) { - - p := provisioner{ - namespace: namespace, - name: name, - admin: admin, - } - component := fmt.Sprintf("%s-%s", name, buses.Provisioner) - monitor := buses.NewMonitor(component, masterURL, kubeconfig, buses.MonitorEventHandlerFuncs{ - ProvisionFunc: p.provision, - UnprovisionFunc: p.unprovision, - }) - p.monitor = monitor - - return &p, nil -} - -func (p *provisioner) Start(stopCh <-chan struct{}) { - go p.monitor.Run(p.namespace, p.name, 2, stopCh) -} - -func (p *provisioner) provision(channel *channelsv1alpha1.Channel, parameters buses.ResolvedParameters) error { - topicName := topicNameFromChannel(channel) - glog.Infof("Provisioning topic %s on bus backed by Kafka", topicName) - - partitions := 1 - if p, ok := parameters[NumPartitions]; ok { - var err error - partitions, err = strconv.Atoi(p) - if err != nil { - glog.Warningf("Could not parse partition count for %s/%s: %s", channel.Namespace, channel.Name, p) - } - } - - err := p.admin.CreateTopic(topicName, &sarama.TopicDetail{ReplicationFactor: 1, NumPartitions: int32(partitions)}, false) - if err == sarama.ErrTopicAlreadyExists { - return nil - } else if err != nil { - glog.Errorf("Error creating topic %s: %v", topicName, err) - } else { - glog.Infof("Successfully created topic %s", topicName) - } - return err -} - -func (p *provisioner) unprovision(channel *channelsv1alpha1.Channel) error { - topicName := topicNameFromChannel(channel) - glog.Infof("Un-provisioning topic %s from bus backed by Kafka", topicName) - - err := p.admin.DeleteTopic(topicName) - if err == sarama.ErrUnknownTopicOrPartition { - return nil - } else if err != nil { - glog.Errorf("Error deleting topic %s: %v", topicName, err) - } else { - glog.Infof("Successfully deleted topic %s", topicName) - } - - return err -} - -func topicNameFromChannel(channel *channelsv1alpha1.Channel) string { - return fmt.Sprintf("%s.%s", channel.Namespace, channel.Name) -} diff --git a/pkg/buses/kafka/provisioner/stub_test.go b/pkg/buses/kafka/provisioner/stub_test.go deleted file mode 100644 index ee7bbe8c445..00000000000 --- a/pkg/buses/kafka/provisioner/stub_test.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -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 main - -import "testing" - -func TestStub(t *testing.T) { - // TODO: Implement tests in this directory, please. - // This file is used to make code coverage on this directly count. -} diff --git a/pkg/buses/message_dispatcher.go b/pkg/buses/message_dispatcher.go index 0bb66064beb..ae52ebeca9d 100644 --- a/pkg/buses/message_dispatcher.go +++ b/pkg/buses/message_dispatcher.go @@ -23,6 +23,8 @@ import ( "net/http" "net/url" "strings" + + "github.com/golang/glog" ) const correlationIDHeaderName = "Knative-Correlation-Id" @@ -77,6 +79,7 @@ func (d *MessageDispatcher) DispatchMessage(message *Message, destination string } func (d *MessageDispatcher) executeRequest(url *url.URL, message *Message) (*Message, error) { + glog.Infof("Dispatching message to %s\n", url.String()) req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(message.Payload)) if err != nil { return nil, fmt.Errorf("Unable to create request %v", err) diff --git a/pkg/buses/message_receiver.go b/pkg/buses/message_receiver.go index a2a14d890b5..75509df637c 100644 --- a/pkg/buses/message_receiver.go +++ b/pkg/buses/message_receiver.go @@ -27,14 +27,14 @@ import ( // MessageReceiver starts a server to receive new messages for the bus. The new // message is emitted via the receiver function. type MessageReceiver struct { - receiverFunc func(*ChannelReference, *Message) error + receiverFunc func(ChannelReference, *Message) error forwardHeaders map[string]bool forwardPrefixes []string } // NewMessageReceiver creates a message receiver passing new messages to the // receiverFunc. -func NewMessageReceiver(receiverFunc func(*ChannelReference, *Message) error) *MessageReceiver { +func NewMessageReceiver(receiverFunc func(ChannelReference, *Message) error) *MessageReceiver { receiver := &MessageReceiver{ receiverFunc: receiverFunc, forwardHeaders: headerSet(forwardHeaders), @@ -161,9 +161,9 @@ func (r *MessageReceiver) fromHTTPHeaders(headers http.Header) map[string]string // parseChannelReference converts the channel's hostname into a channel // reference. -func (r *MessageReceiver) parseChannelReference(host string) *ChannelReference { +func (r *MessageReceiver) parseChannelReference(host string) ChannelReference { chunks := strings.Split(host, ".") - return &ChannelReference{ + return ChannelReference{ Name: chunks[0], Namespace: chunks[1], } diff --git a/pkg/buses/monitor.go b/pkg/buses/monitor.go deleted file mode 100644 index 3feaef2ac1e..00000000000 --- a/pkg/buses/monitor.go +++ /dev/null @@ -1,978 +0,0 @@ -/* - * 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 buses - -import ( - "fmt" - "reflect" - "strings" - "sync" - "time" - - "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" - "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" - channelscheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" - listers "github.com/knative/eventing/pkg/client/listers/channels/v1alpha1" - - clientset "github.com/knative/eventing/pkg/client/clientset/versioned" - informers "github.com/knative/eventing/pkg/client/informers/externalversions" - "github.com/knative/eventing/pkg/controller/util" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" - typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/record" - "k8s.io/client-go/util/workqueue" -) - -const ( - Dispatcher = "dispatcher" - Provisioner = "provisioner" - - busKind = "Bus" - clusterBusKind = "ClusterBus" - channelKind = "Channel" - subscriptionKind = "Subscription" - - // SuccessSynced is used as part of the Event 'reason' when a resource is synced - successSynced = "Synced" - // ErrResourceSync is used as part of the Event 'reason' when a resource fails - // to sync. - errResourceSync = "ErrResourceSync" -) - -// Monitor is a utility mix-in intended to be used by Bus authors to easily -// write provisioners and dispatchers for buses. Bus provisioners are -// responsible for managing the storage asset(s) that back a channel. Bus -// dispatchers are responsible for dispatching events on the Channel to the -// Channel's Subscriptions. Monitor handles setting up informers that watch a -// Bus, its Channels, and their Subscriptions and allows Bus authors to register -// event handler functions to be called when Provision/Unprovision and -// Subscribe/Unsubscribe happen. -type Monitor struct { - bus channelsv1alpha1.GenericBus - handler MonitorEventHandlerFuncs - informerFactory informers.SharedInformerFactory - clientset clientset.Interface - busesLister listers.BusLister - busesSynced cache.InformerSynced - clusterBusesLister listers.ClusterBusLister - clusterBusesSynced cache.InformerSynced - channelsLister listers.ChannelLister - channelsSynced cache.InformerSynced - subscriptionsLister listers.SubscriptionLister - subscriptionsSynced cache.InformerSynced - cache map[channelKey]*channelSummary - provisionedChannels map[channelKey]*channelsv1alpha1.Channel - provisionedSubscriptions map[subscriptionKey]*channelsv1alpha1.Subscription - mutex *sync.Mutex - - // workqueue is a rate limited work queue. This is used to queue work to be - // processed instead of performing it as soon as a change happens. This - // means we can ensure we only process a fixed amount of resources at a - // time, and makes it easy to ensure we are never processing the same item - // simultaneously in two different workers. - workqueue workqueue.RateLimitingInterface - // recorder is an event recorder for recording Event resources to the - // Kubernetes API. - recorder record.EventRecorder -} - -type ResolvedParameters = map[string]string - -// MonitorEventHandlerFuncs is a set of handler functions that are called when a -// bus requires sync, channels are provisioned/unprovisioned, or a subscription -// is created or deleted, or if one of the relevant resources is changed. -type MonitorEventHandlerFuncs struct { - // BusFunc is invoked when the Bus requires sync. - BusFunc func(bus channelsv1alpha1.GenericBus) error - - // ProvisionFunc is invoked when a new Channel should be provisioned or when - // the attributes change. - ProvisionFunc func(channel *channelsv1alpha1.Channel, parameters ResolvedParameters) error - - // UnprovisionFunc in invoked when a Channel should be deleted. - UnprovisionFunc func(channel *channelsv1alpha1.Channel) error - - // SubscribeFunc is invoked when a new Subscription should be set up or when - // the attributes change. - SubscribeFunc func(subscription *channelsv1alpha1.Subscription, parameters ResolvedParameters) error - - // UnsubscribeFunc is invoked when a Subscription should be deleted. - UnsubscribeFunc func(subscription *channelsv1alpha1.Subscription) error -} - -func (h MonitorEventHandlerFuncs) onBus(bus channelsv1alpha1.GenericBus, monitor *Monitor) error { - if h.BusFunc != nil { - err := h.BusFunc(bus) - if err != nil { - monitor.recorder.Eventf(bus, corev1.EventTypeWarning, errResourceSync, "Error syncing Bus: %s", err) - } else { - monitor.recorder.Event(bus, corev1.EventTypeNormal, successSynced, "Bus synched successfully") - } - return err - } - return nil -} - -func (h MonitorEventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, monitor *Monitor) error { - if h.ProvisionFunc != nil { - parameters, err := monitor.resolveChannelParameters(channel.Spec) - if err != nil { - return err - } - err = h.ProvisionFunc(channel, parameters) - channelCopy := channel.DeepCopy() - var cond *channelsv1alpha1.ChannelCondition - if err != nil { - monitor.recorder.Eventf(channel, corev1.EventTypeWarning, errResourceSync, "Error provisioning channel: %s", err) - cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionFalse, errResourceSync, err.Error()) - } else { - monitor.recorder.Event(channel, corev1.EventTypeNormal, successSynced, "Channel provisioned successfully") - cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionTrue, successSynced, "Channel provisioned successfully") - } - util.SetChannelCondition(&channelCopy.Status, *cond) - util.ConsolidateChannelCondition(&channelCopy.Status) - _, errS := monitor.clientset.ChannelsV1alpha1().Channels(channel.Namespace).Update(channelCopy) - if errS != nil { - glog.Warningf("Could not update status: %v", errS) - } - return err - } - return nil -} - -func (h MonitorEventHandlerFuncs) onUnprovision(channel *channelsv1alpha1.Channel, monitor *Monitor) error { - if h.UnprovisionFunc != nil { - err := h.UnprovisionFunc(channel) - if err != nil { - monitor.recorder.Eventf(channel, corev1.EventTypeWarning, errResourceSync, "Error unprovisioning channel: %s", err) - } else { - monitor.recorder.Event(channel, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") - } - // skip updating status conditions since the channel was deleted - return err - } - return nil -} - -func (h MonitorEventHandlerFuncs) onSubscribe(subscription *channelsv1alpha1.Subscription, monitor *Monitor) error { - if h.SubscribeFunc != nil { - attributes, err := monitor.resolveSubscriptionParameters(subscription.Spec) - if err != nil { - return err - } - err = h.SubscribeFunc(subscription, attributes) - subscriptionCopy := subscription.DeepCopy() - var cond *channelsv1alpha1.SubscriptionCondition - if err != nil { - monitor.recorder.Eventf(subscription, corev1.EventTypeWarning, errResourceSync, "Error subscribing: %s", err) - cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionFalse, errResourceSync, err.Error()) - } else { - monitor.recorder.Event(subscription, corev1.EventTypeNormal, successSynced, "Subscribed successfully") - cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionTrue, successSynced, "Subscription dispatcher successfully created") - } - util.SetSubscriptionCondition(&subscriptionCopy.Status, *cond) - _, errS := monitor.clientset.ChannelsV1alpha1().Subscriptions(subscription.Namespace).Update(subscriptionCopy) - if errS != nil { - glog.Warningf("Could not update status: %v", errS) - } - return err - } - return nil -} - -func (h MonitorEventHandlerFuncs) onUnsubscribe(subscription *channelsv1alpha1.Subscription, monitor *Monitor) error { - if h.UnsubscribeFunc != nil { - err := h.UnsubscribeFunc(subscription) - if err != nil { - monitor.recorder.Eventf(subscription, corev1.EventTypeWarning, errResourceSync, "Error unsubscribing: %s", err) - } else { - monitor.recorder.Event(subscription, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") - } - // skip updating status conditions since the subscription was deleted - return err - } - return nil -} - -// channelSummary is a record, for a particular Channel, of that Channel's spec -// and its current subscriptions. -type channelSummary struct { - Channel *channelsv1alpha1.ChannelSpec - Subscriptions map[subscriptionKey]subscriptionSummary -} - -// subscriptionSummary is a record of a Subscription's spec that is used as part -// of a channelSummary. -type subscriptionSummary struct { - Subscription channelsv1alpha1.SubscriptionSpec -} - -// NewMonitor creates a monitor for a bus given: -// -// component: the name of the component this monitor should use in created k8s events -// masterURL: the URL of the API server the monitor should communicate with -// kubeconfig: the path of a kubeconfig file to create a client connection to the masterURL with -// handler: a MonitorEventHandlerFuncs with handler functions for the monitor to call -func NewMonitor( - component, masterURL, kubeconfig string, - handler MonitorEventHandlerFuncs, -) *Monitor { - cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) - if err != nil { - glog.Fatalf("Error building kubeconfig: %s", err.Error()) - } - - kubeClient, err := kubernetes.NewForConfig(cfg) - if err != nil { - glog.Fatalf("Error building kubernetes clientset: %s", err.Error()) - } - - client, err := clientset.NewForConfig(cfg) - if err != nil { - glog.Fatalf("Error building clientset: %s", err.Error()) - } - - informerFactory := informers.NewSharedInformerFactory(client, time.Second*30) - busInformer := informerFactory.Channels().V1alpha1().Buses() - clusterBusInformer := informerFactory.Channels().V1alpha1().ClusterBuses() - channelInformer := informerFactory.Channels().V1alpha1().Channels() - subscriptionInformer := informerFactory.Channels().V1alpha1().Subscriptions() - - // Create event broadcaster - // Add types to the default Kubernetes Scheme so Events can be logged for the component. - channelscheme.AddToScheme(scheme.Scheme) - glog.V(4).Info("Creating event broadcaster") - eventBroadcaster := record.NewBroadcaster() - eventBroadcaster.StartLogging(glog.Infof) - eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) - recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: component}) - - monitor := &Monitor{ - bus: nil, - handler: handler, - - clientset: client, - informerFactory: informerFactory, - busesLister: busInformer.Lister(), - busesSynced: busInformer.Informer().HasSynced, - clusterBusesLister: clusterBusInformer.Lister(), - clusterBusesSynced: clusterBusInformer.Informer().HasSynced, - channelsLister: channelInformer.Lister(), - channelsSynced: channelInformer.Informer().HasSynced, - subscriptionsLister: subscriptionInformer.Lister(), - subscriptionsSynced: subscriptionInformer.Informer().HasSynced, - cache: make(map[channelKey]*channelSummary), - provisionedChannels: make(map[channelKey]*channelsv1alpha1.Channel), - provisionedSubscriptions: make(map[subscriptionKey]*channelsv1alpha1.Subscription), - mutex: &sync.Mutex{}, - - workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Monitor"), - recorder: recorder, - } - - glog.Info("Setting up event handlers") - // Set up an event handler for when Bus resources change - busInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - bus := obj.(*channelsv1alpha1.Bus) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForBus(bus)) - }, - UpdateFunc: func(old, new interface{}) { - oldBus := old.(*channelsv1alpha1.Bus) - newBus := new.(*channelsv1alpha1.Bus) - - if oldBus.ResourceVersion == newBus.ResourceVersion { - // Periodic resync will send update events for all known Buses. - // Two different versions of the same Bus will always have different RVs. - return - } - - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForBus(newBus)) - }, - }) - // Set up an event handler for when ClusterBus resources change - clusterBusInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - clusterBus := obj.(*channelsv1alpha1.ClusterBus) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForClusterBus(clusterBus)) - }, - UpdateFunc: func(old, new interface{}) { - oldClusterBus := old.(*channelsv1alpha1.ClusterBus) - newClusterBus := new.(*channelsv1alpha1.ClusterBus) - - if oldClusterBus.ResourceVersion == newClusterBus.ResourceVersion { - // Periodic resync will send update events for all known ClusterBuses. - // Two different versions of the same ClusterBus will always have different RVs. - return - } - - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForClusterBus(newClusterBus)) - }, - }) - // Set up an event handler for when Channel resources change - channelInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - channel := obj.(*channelsv1alpha1.Channel) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(channel)) - }, - UpdateFunc: func(old, new interface{}) { - oldChannel := old.(*channelsv1alpha1.Channel) - newChannel := new.(*channelsv1alpha1.Channel) - - if oldChannel.ResourceVersion == newChannel.ResourceVersion { - // Periodic resync will send update events for all known Channels. - // Two different versions of the same Channel will always have different RVs. - return - } - - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(newChannel)) - }, - DeleteFunc: func(obj interface{}) { - channel := obj.(*channelsv1alpha1.Channel) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(channel)) - }, - }) - // Set up an event handler for when Subscription resources change - subscriptionInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - subscription := obj.(*channelsv1alpha1.Subscription) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(subscription)) - }, - UpdateFunc: func(old, new interface{}) { - oldSubscription := old.(*channelsv1alpha1.Subscription) - newSubscription := new.(*channelsv1alpha1.Subscription) - - if oldSubscription.ResourceVersion == newSubscription.ResourceVersion { - // Periodic resync will send update events for all known Subscriptions. - // Two different versions of the same Subscription will always have different RVs. - return - } - - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(newSubscription)) - }, - DeleteFunc: func(obj interface{}) { - subscription := obj.(*channelsv1alpha1.Subscription) - monitor.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(subscription)) - }, - }) - - return monitor -} - -// Channel returns the provisioned Channel with the given name and namespace, or -// nil if such a Channel hasn't been provisioned. -func (m *Monitor) Channel(name string, namespace string) *channelsv1alpha1.Channel { - channelKey := makeChannelKeyWithNames(namespace, name) - if channel, ok := m.provisionedChannels[channelKey]; ok { - return channel - } - return nil -} - -// Subscription returns the provisioned Subscription with the given name and -// namespace, or nil if such a Subscription hasn't been provisioned. -func (m *Monitor) Subscription(name string, namespace string) *channelsv1alpha1.Subscription { - subscriptionKey := makeSubscriptionKeyWithNames(namespace, name) - if subscription, ok := m.provisionedSubscriptions[subscriptionKey]; ok { - return subscription - } - return nil -} - -// Subscriptions returns a slice of SubscriptionSpecs for the Channel with the -// given name and namespace, or nil if the Channel hasn't been provisioned. -func (m *Monitor) Subscriptions(channelName string, namespace string) *[]channelsv1alpha1.SubscriptionSpec { - channelKey := makeChannelKeyWithNames(namespace, channelName) - summary := m.getChannelSummary(channelKey) - channel := m.Channel(channelName, namespace) - - if summary == nil || summary.Channel == nil || channel == nil { - // the channel is unknown - return nil - } - - if !m.bus.BacksChannel(channel) { - // the channel is not for this bus - return nil - } - - m.mutex.Lock() - subscriptions := []channelsv1alpha1.SubscriptionSpec{} - for _, subscription := range summary.Subscriptions { - subscriptions = append(subscriptions, subscription.Subscription) - } - m.mutex.Unlock() - - return &subscriptions -} - -// resolveChannelParameters resolves the given Channel Parameters and the Bus' -// Channel Parameters, returning an ResolvedParameters or an Error. -func (m *Monitor) resolveChannelParameters(channel channelsv1alpha1.ChannelSpec) (ResolvedParameters, error) { - genericBusParameters := m.bus.GetSpec().Parameters - var parameters *[]channelsv1alpha1.Parameter - if genericBusParameters != nil { - parameters = genericBusParameters.Channel - } - return m.resolveParameters(parameters, channel.Arguments) -} - -// resolveSubscriptionParameters resolves the given Subscription Parameters and -// the Bus' Subscription Parameters, returning an Attributes or an Error. -func (m *Monitor) resolveSubscriptionParameters(subscription channelsv1alpha1.SubscriptionSpec) (ResolvedParameters, error) { - genericBusParameters := m.bus.GetSpec().Parameters - var parameters *[]channelsv1alpha1.Parameter - if genericBusParameters != nil { - parameters = genericBusParameters.Subscription - } - return m.resolveParameters(parameters, subscription.Arguments) -} - -// resolveParameters resolves a slice of Parameters and a slice of arguments and -// returns an Attributes or an error if there are missing Arguments. Each -// Parameter represents a variable that must be provided by an Argument or -// optionally defaulted if a default value for the Parameter is specified. -// resolveAttributes combines the given arrays of Parameters and Arguments, -// using default values where necessary and returning an error if there are -// missing Arguments. -func (m *Monitor) resolveParameters(parameters *[]channelsv1alpha1.Parameter, arguments *[]channelsv1alpha1.Argument) (ResolvedParameters, error) { - resolved := make(ResolvedParameters) - known := make(map[string]interface{}) - required := make(map[string]interface{}) - - // apply parameters - if parameters != nil { - for _, param := range *parameters { - known[param.Name] = true - if param.Default != nil { - resolved[param.Name] = *param.Default - } else { - required[param.Name] = true - } - } - } - // apply arguments - if arguments != nil { - for _, arg := range *arguments { - if _, ok := known[arg.Name]; !ok { - // ignore arguments not defined by parameters - glog.Warningf("Skipping unknown argument: %s\n", arg.Name) - continue - } - delete(required, arg.Name) - resolved[arg.Name] = arg.Value - } - } - - // check for missing arguments - if len(required) != 0 { - missing := []string{} - for name := range required { - missing = append(missing, name) - } - return nil, fmt.Errorf("missing required arguments: %v", missing) - } - - return resolved, nil -} - -func (m *Monitor) RequeueSubscription(subscription *channelsv1alpha1.Subscription) { - glog.Infof("Requeue subscription %q\n", subscription.Name) - m.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(subscription)) -} - -// Run will set up the event handlers for types we are interested in, as well -// as syncing informer caches and starting workers. It will block until stopCh -// is closed, at which point it will shutdown the workqueue and wait for -// workers to finish processing their current work items. -func (m *Monitor) Run(busNamespace, busName string, threadiness int, stopCh <-chan struct{}) error { - defer runtime.HandleCrash() - defer m.workqueue.ShutDown() - - // Start the informer factories to begin populating the informer caches - glog.Info("Starting monitor") - go m.informerFactory.Start(stopCh) - - // Wait for the caches to be synced before starting workers - glog.Info("Waiting for informer caches to sync") - if err := m.WaitForCacheSync(stopCh); err != nil { - return err - } - - if len(busNamespace) == 0 { - // monitor is for a ClusterBus - clusterBus, err := m.clusterBusesLister.Get(busName) - if err != nil { - glog.Fatalf("Unknown clusterbus %q", busName) - } - m.bus = clusterBus.DeepCopy() - } else { - // monitor is for a namespaced Bus - bus, err := m.busesLister.Buses(busNamespace).Get(busName) - if err != nil { - glog.Fatalf("Unknown bus '%s/%s'", busNamespace, busName) - } - m.bus = bus.DeepCopy() - } - - glog.Info("Starting workers") - // Launch workers to process resources - for i := 0; i < threadiness; i++ { - go wait.Until(m.runWorker, time.Second, stopCh) - } - - glog.Info("Started workers") - <-stopCh - glog.Info("Shutting down workers") - - return nil -} - -// WaitForCacheSync blocks returning until the monitor's informers have -// synchronized. It returns an error if the caches cannot sync. -func (m *Monitor) WaitForCacheSync(stopCh <-chan struct{}) error { - if ok := cache.WaitForCacheSync(stopCh, m.busesSynced, m.clusterBusesSynced, m.channelsSynced, m.subscriptionsSynced); !ok { - return fmt.Errorf("failed to wait for caches to sync") - } - return nil -} - -// runWorker is a long-running function that will continually call the -// processNextWorkItem function in order to read and process a message on the -// workqueue. -func (m *Monitor) runWorker() { - for m.processNextWorkItem() { - } -} - -// processNextWorkItem will read a single work item off the workqueue and -// attempt to process it, by calling the syncHandler. -func (m *Monitor) processNextWorkItem() bool { - obj, shutdown := m.workqueue.Get() - - if shutdown { - return false - } - - // We wrap this block in a func so we can defer m.workqueue.Done. - err := func(obj interface{}) error { - // We call Done here so the workqueue knows we have finished - // processing this item. We also must remember to call Forget if we - // do not want this work item being re-queued. For example, we do - // not call Forget if a transient error occurs, instead the item is - // put back on the workqueue and attempted again after a back-off - // period. - defer m.workqueue.Done(obj) - var key string - var ok bool - // We expect strings to come off the workqueue. These are of the - // form namespace/name. We do this as the delayed nature of the - // workqueue means the items in the informer cache may actually be - // more up to date that when the item was initially put onto the - // workqueue. - if key, ok = obj.(string); !ok { - // As the item in the workqueue is actually invalid, we call - // Forget here else we'd go into a loop of attempting to - // process a work item that is invalid. - m.workqueue.Forget(obj) - runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) - return nil - } - // Run the syncHandler, passing it the name string of the resource to be synced. - if err := m.syncHandler(key); err != nil { - m.workqueue.AddRateLimited(obj) - return fmt.Errorf("error syncing monitor '%s': %s", key, err.Error()) - } - // Finally, if no error occurs we Forget this item so it does not - // get queued again until another change happens. - m.workqueue.Forget(obj) - glog.Infof("Successfully synced monitor '%s'", key) - return nil - }(obj) - - if err != nil { - runtime.HandleError(err) - return true - } - - return true -} - -// syncHandler compares the actual state with the desired, and attempts to -// converge the two. It then updates the Status block of the resource with the -// current status. -func (m *Monitor) syncHandler(key string) error { - // Convert the namespace/name string into a distinct namespace and name - kind, namespace, name, err := splitWorkqueueKey(key) - if err != nil { - runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) - return nil - } - - if m.bus == nil && !(kind == busKind || kind == clusterBusKind) { - // don't attempt to sync until we have seen the bus for this monitor - return fmt.Errorf("Unknown bus for monitor") - } - - switch kind { - case busKind: - err = m.syncBus(namespace, name) - case clusterBusKind: - err = m.syncClusterBus(name) - case channelKind: - err = m.syncChannel(namespace, name) - case subscriptionKind: - err = m.syncSubscription(namespace, name) - default: - runtime.HandleError(fmt.Errorf("Unknown resource kind %s", kind)) - return nil - } - - if err != nil { - return err - } - - return nil -} - -func (m *Monitor) syncBus(namespace string, name string) error { - // Get the Bus resource with this namespace/name - bus, err := m.busesLister.Buses(namespace).Get(name) - if err != nil { - // The Bus resource may no longer exist - if errors.IsNotFound(err) { - // nothing to do - return nil - } - - return err - } - - // Sync the Bus - err = m.createOrUpdateBus(bus) - if err != nil { - return err - } - - return nil -} - -func (m *Monitor) syncClusterBus(name string) error { - // Get the ClusterBus resource with this name - clusterBus, err := m.clusterBusesLister.Get(name) - if err != nil { - // The ClusterBus resource may no longer exist - if errors.IsNotFound(err) { - // nothing to do - return nil - } - - return err - } - - // Sync the ClusterBus - err = m.createOrUpdateClusterBus(clusterBus) - if err != nil { - return err - } - - return nil -} - -func (m *Monitor) syncChannel(namespace string, name string) error { - // Get the Channel resource with this namespace/name - channel, err := m.channelsLister.Channels(namespace).Get(name) - if err != nil { - // The Channel resource may no longer exist - if errors.IsNotFound(err) { - err = m.removeChannel(namespace, name) - if err != nil { - return err - } - return nil - } - - return err - } - - // Sync the Channel - err = m.createOrUpdateChannel(channel) - if err != nil { - return err - } - - return nil -} - -func (m *Monitor) syncSubscription(namespace string, name string) error { - // Get the Subscription resource with this namespace/name - subscription, err := m.subscriptionsLister.Subscriptions(namespace).Get(name) - if err != nil { - // The Subscription resource may no longer exist - if errors.IsNotFound(err) { - err = m.removeSubscription(namespace, name) - if err != nil { - return err - } - return nil - } - - return err - } - - // Sync the Subscription - err = m.createOrUpdateSubscription(subscription) - if err != nil { - return err - } - - return nil -} - -func (m *Monitor) getChannelSummary(key channelKey) *channelSummary { - return m.cache[key] -} - -func (m *Monitor) getOrCreateChannelSummary(key channelKey) *channelSummary { - m.mutex.Lock() - summary, ok := m.cache[key] - if !ok { - summary = &channelSummary{ - Channel: nil, - Subscriptions: make(map[subscriptionKey]subscriptionSummary), - } - m.cache[key] = summary - } - m.mutex.Unlock() - - return summary -} - -func (m *Monitor) createOrUpdateBus(bus *channelsv1alpha1.Bus) error { - if m.bus.GetObjectKind().GroupVersionKind().Kind != bus.Kind || - bus.Namespace != m.bus.GetObjectMeta().GetNamespace() || - bus.Name != m.bus.GetObjectMeta().GetName() { - // this is not our bus - return nil - } - - if !reflect.DeepEqual(m.bus.GetSpec(), bus.Spec) { - m.bus = bus.DeepCopy() - err := m.handler.onBus(bus, m) - if err != nil { - return err - } - } - - return nil -} - -func (m *Monitor) createOrUpdateClusterBus(clusterBus *channelsv1alpha1.ClusterBus) error { - if m.bus.GetObjectKind().GroupVersionKind().Kind != clusterBus.Kind || - clusterBus.Name != m.bus.GetObjectMeta().GetName() { - // this is not our clusterbus - return nil - } - - if !reflect.DeepEqual(m.bus.GetSpec(), clusterBus.Spec) { - m.bus = clusterBus.DeepCopy() - err := m.handler.onBus(clusterBus, m) - if err != nil { - return err - } - } - - return nil -} - -func (m *Monitor) createOrUpdateChannel(channel *channelsv1alpha1.Channel) error { - channelKey := makeChannelKeyFromChannel(channel) - summary := m.getOrCreateChannelSummary(channelKey) - - m.mutex.Lock() - old := summary.Channel - new := &channel.Spec - summary.Channel = new - m.mutex.Unlock() - - if m.bus.BacksChannel(channel) && !reflect.DeepEqual(old, new) { - err := m.handler.onProvision(channel, m) - if err != nil { - return err - } - m.provisionedChannels[channelKey] = channel - } - - return nil -} - -func (m *Monitor) removeChannel(namespace string, name string) error { - channelKey := makeChannelKeyWithNames(namespace, name) - channel, ok := m.provisionedChannels[channelKey] - if !ok { - return nil - } - - summary := m.getOrCreateChannelSummary(channelKey) - - m.mutex.Lock() - summary.Channel = nil - m.mutex.Unlock() - - err := m.handler.onUnprovision(channel, m) - if err != nil { - return err - } - delete(m.provisionedChannels, channelKey) - - return nil -} - -func (m *Monitor) isSubscriptionProvisioned(subscription *channelsv1alpha1.Subscription) bool { - subscriptionKey := makeSubscriptionKeyFromSubscription(subscription) - _, ok := m.provisionedSubscriptions[subscriptionKey] - return ok -} - -func (m *Monitor) createOrUpdateSubscription(subscription *channelsv1alpha1.Subscription) error { - subscriptionKey := makeSubscriptionKeyFromSubscription(subscription) - channelKey := makeChannelKeyFromSubscription(subscription) - summary := m.getOrCreateChannelSummary(channelKey) - - m.mutex.Lock() - old := summary.Subscriptions[subscriptionKey] - new := subscriptionSummary{ - Subscription: subscription.Spec, - } - summary.Subscriptions[subscriptionKey] = new - m.mutex.Unlock() - - channel := m.Channel(subscription.Spec.Channel, subscription.Namespace) - if channel == nil { - return fmt.Errorf("unknown channel %q for subscription", subscription.Spec.Channel) - } - if !m.bus.BacksChannel(channel) { - return nil - } - - if !m.isSubscriptionProvisioned(subscription) || !reflect.DeepEqual(old.Subscription, new.Subscription) { - err := m.handler.onSubscribe(subscription, m) - if err != nil { - return err - } - m.provisionedSubscriptions[subscriptionKey] = subscription - } - - return nil -} - -func (m *Monitor) removeSubscription(namespace string, name string) error { - subscriptionKey := makeSubscriptionKeyWithNames(namespace, name) - subscription, ok := m.provisionedSubscriptions[subscriptionKey] - if !ok { - return nil - } - - channelKey := makeChannelKeyFromSubscription(subscription) - summary := m.getOrCreateChannelSummary(channelKey) - - m.mutex.Lock() - delete(summary.Subscriptions, subscriptionKey) - m.mutex.Unlock() - - err := m.handler.onUnsubscribe(subscription, m) - if err != nil { - return err - } - delete(m.provisionedSubscriptions, subscriptionKey) - - return nil -} - -type channelKey struct { - Namespace string - Name string -} - -func makeChannelKeyFromChannel(channel *channelsv1alpha1.Channel) channelKey { - return makeChannelKeyWithNames(channel.Namespace, channel.Name) -} - -func makeChannelKeyFromSubscription(subscription *channelsv1alpha1.Subscription) channelKey { - return makeChannelKeyWithNames(subscription.Namespace, subscription.Spec.Channel) -} - -func makeChannelKeyWithNames(namespace string, name string) channelKey { - return channelKey{ - Namespace: namespace, - Name: name, - } -} - -type subscriptionKey struct { - Namespace string - Name string -} - -func makeSubscriptionKeyFromSubscription(subscription *channelsv1alpha1.Subscription) subscriptionKey { - return makeSubscriptionKeyWithNames(subscription.Namespace, subscription.Name) -} - -func makeSubscriptionKeyWithNames(namespace string, name string) subscriptionKey { - return subscriptionKey{ - Namespace: namespace, - Name: name, - } -} - -func makeWorkqueueKeyForBus(bus *channelsv1alpha1.Bus) string { - return makeWorkqueueKey(busKind, bus.Namespace, bus.Name) -} - -func makeWorkqueueKeyForClusterBus(clusterBus *channelsv1alpha1.ClusterBus) string { - return makeWorkqueueKey(clusterBusKind, "", clusterBus.Name) -} - -func makeWorkqueueKeyForChannel(channel *channelsv1alpha1.Channel) string { - return makeWorkqueueKey(channelKind, channel.Namespace, channel.Name) -} - -func makeWorkqueueKeyForSubscription(subscription *channelsv1alpha1.Subscription) string { - return makeWorkqueueKey(subscriptionKind, subscription.Namespace, subscription.Name) -} - -func makeWorkqueueKey(kind string, namespace string, name string) string { - return fmt.Sprintf("%s/%s/%s", kind, namespace, name) -} - -func splitWorkqueueKey(key string) (string, string, string, error) { - chunks := strings.Split(key, "/") - if len(chunks) != 3 { - return "", "", "", fmt.Errorf("Unknown workqueue key %v", key) - } - kind := chunks[0] - namespace := chunks[1] - name := chunks[2] - return kind, namespace, name, nil -} diff --git a/pkg/buses/reconciler.go b/pkg/buses/reconciler.go new file mode 100644 index 00000000000..b15868ab0c9 --- /dev/null +++ b/pkg/buses/reconciler.go @@ -0,0 +1,664 @@ +/* + * 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 buses + +import ( + "fmt" + "strings" + "time" + + "github.com/golang/glog" + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" + eventingclientset "github.com/knative/eventing/pkg/client/clientset/versioned" + eventingscheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + eventinginformers "github.com/knative/eventing/pkg/client/informers/externalversions" + channelslisters "github.com/knative/eventing/pkg/client/listers/channels/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + kubernetesclientset "k8s.io/client-go/kubernetes" + typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + informercache "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" +) + +const ( + // Dispatcher manages the data plane for a bus + Dispatcher = "dispatcher" + // Provisioner manages the control plane for a bus + Provisioner = "provisioner" + + busKind = "Bus" + clusterBusKind = "ClusterBus" + channelKind = "Channel" + subscriptionKind = "Subscription" +) + +// Reconciler is a utility mix-in intended to be used by Bus authors to easily +// write provisioners and dispatchers for buses. Bus provisioners are +// responsible for managing the storage asset(s) that back a channel. Bus +// dispatchers are responsible for dispatching events on the Channel to the +// Channel's Subscriptions. Reconciler handles setting up informers that watch +// a Bus, its Channels, and their Subscriptions and allows Bus authors to +// register event handler functions to be called when Provision/Unprovision and +// Subscribe/Unsubscribe happen. +type Reconciler struct { + bus channelsv1alpha1.GenericBus + handler EventHandlerFuncs + cache *Cache + + eventingInformerFactory eventinginformers.SharedInformerFactory + eventingClient eventingclientset.Interface + busesLister channelslisters.BusLister + busesSynced informercache.InformerSynced + clusterBusesLister channelslisters.ClusterBusLister + clusterBusesSynced informercache.InformerSynced + channelsLister channelslisters.ChannelLister + channelsSynced informercache.InformerSynced + subscriptionsLister channelslisters.SubscriptionLister + subscriptionsSynced informercache.InformerSynced + + // workqueue is a rate limited work queue. This is used to queue work to be + // processed instead of performing it as soon as a change happens. This + // means we can ensure we only process a fixed amount of resources at a + // time, and makes it easy to ensure we are never processing the same item + // simultaneously in two different workers. + workqueue workqueue.RateLimitingInterface + // recorder is an event recorder for recording Event resources to the + // Kubernetes API. + recorder record.EventRecorder +} + +// NewReconciler creates a reconciler for a bus given: +// +// component: the name of the component this reconciler should use in created k8s events +// masterURL: the URL of the API server the reconciler should communicate with +// kubeconfig: the path of a kubeconfig file to create a client connection to the masterURL with +// handler: a EventHandlerFuncs with handler functions for the reconciler to call +func NewReconciler( + component, masterURL, kubeconfig string, + cache *Cache, handler EventHandlerFuncs, +) *Reconciler { + cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) + if err != nil { + glog.Fatalf("Error building kubeconfig: %s", err.Error()) + } + + kubeClient, err := kubernetesclientset.NewForConfig(cfg) + if err != nil { + glog.Fatalf("Error building kubernetes clientset: %s", err.Error()) + } + + eventingClient, err := eventingclientset.NewForConfig(cfg) + if err != nil { + glog.Fatalf("Error building eventing clientset: %s", err.Error()) + } + + eventingInformerFactory := eventinginformers.NewSharedInformerFactory(eventingClient, time.Second*30) + busInformer := eventingInformerFactory.Channels().V1alpha1().Buses() + clusterBusInformer := eventingInformerFactory.Channels().V1alpha1().ClusterBuses() + channelInformer := eventingInformerFactory.Channels().V1alpha1().Channels() + subscriptionInformer := eventingInformerFactory.Channels().V1alpha1().Subscriptions() + + // Create event broadcaster + // Add types to the default Kubernetes Scheme so Events can be logged for the component. + eventingscheme.AddToScheme(eventingscheme.Scheme) + glog.V(4).Info("Creating event broadcaster") + eventBroadcaster := record.NewBroadcaster() + eventBroadcaster.StartLogging(glog.Infof) + eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) + recorder := eventBroadcaster.NewRecorder(eventingscheme.Scheme, corev1.EventSource{Component: component}) + + reconciler := &Reconciler{ + bus: nil, + handler: handler, + cache: cache, + + eventingClient: eventingClient, + eventingInformerFactory: eventingInformerFactory, + busesLister: busInformer.Lister(), + busesSynced: busInformer.Informer().HasSynced, + clusterBusesLister: clusterBusInformer.Lister(), + clusterBusesSynced: clusterBusInformer.Informer().HasSynced, + channelsLister: channelInformer.Lister(), + channelsSynced: channelInformer.Informer().HasSynced, + subscriptionsLister: subscriptionInformer.Lister(), + subscriptionsSynced: subscriptionInformer.Informer().HasSynced, + + workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Reconciler"), + recorder: recorder, + } + + glog.Info("Setting up event handlers") + // Set up an event handler for when Bus resources change + busInformer.Informer().AddEventHandler(informercache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + bus := obj.(*channelsv1alpha1.Bus) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForBus(bus)) + }, + UpdateFunc: func(old, new interface{}) { + oldBus := old.(*channelsv1alpha1.Bus) + newBus := new.(*channelsv1alpha1.Bus) + + if oldBus.ResourceVersion == newBus.ResourceVersion { + // Periodic resync will send update events for all known Buses. + // Two different versions of the same Bus will always have different RVs. + return + } + + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForBus(newBus)) + }, + }) + // Set up an event handler for when ClusterBus resources change + clusterBusInformer.Informer().AddEventHandler(informercache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + clusterBus := obj.(*channelsv1alpha1.ClusterBus) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForClusterBus(clusterBus)) + }, + UpdateFunc: func(old, new interface{}) { + oldClusterBus := old.(*channelsv1alpha1.ClusterBus) + newClusterBus := new.(*channelsv1alpha1.ClusterBus) + + if oldClusterBus.ResourceVersion == newClusterBus.ResourceVersion { + // Periodic resync will send update events for all known ClusterBuses. + // Two different versions of the same ClusterBus will always have different RVs. + return + } + + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForClusterBus(newClusterBus)) + }, + }) + // Set up an event handler for when Channel resources change + channelInformer.Informer().AddEventHandler(informercache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + channel := obj.(*channelsv1alpha1.Channel) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(channel)) + }, + UpdateFunc: func(old, new interface{}) { + oldChannel := old.(*channelsv1alpha1.Channel) + newChannel := new.(*channelsv1alpha1.Channel) + + if oldChannel.ResourceVersion == newChannel.ResourceVersion { + // Periodic resync will send update events for all known Channels. + // Two different versions of the same Channel will always have different RVs. + return + } + + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(newChannel)) + }, + DeleteFunc: func(obj interface{}) { + channel := obj.(*channelsv1alpha1.Channel) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForChannel(channel)) + }, + }) + // Set up an event handler for when Subscription resources change + subscriptionInformer.Informer().AddEventHandler(informercache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + subscription := obj.(*channelsv1alpha1.Subscription) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(subscription)) + }, + UpdateFunc: func(old, new interface{}) { + oldSubscription := old.(*channelsv1alpha1.Subscription) + newSubscription := new.(*channelsv1alpha1.Subscription) + + if oldSubscription.ResourceVersion == newSubscription.ResourceVersion { + // Periodic resync will send update events for all known Subscriptions. + // Two different versions of the same Subscription will always have different RVs. + return + } + + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(newSubscription)) + }, + DeleteFunc: func(obj interface{}) { + subscription := obj.(*channelsv1alpha1.Subscription) + reconciler.workqueue.AddRateLimited(makeWorkqueueKeyForSubscription(subscription)) + }, + }) + + return reconciler +} + +// RequeueSubscription will add the Subscription to the workqueue for future +// processing. Reprocessing a Subscription is often used within a dispatcher +// when a long lived receiver is interrupted by an asynchronous error. +func (r *Reconciler) RequeueSubscription(subscriptionRef SubscriptionReference) { + glog.Infof("Requeue subscription %q\n", subscriptionRef.String()) + r.workqueue.AddRateLimited(makeWorkqueueKey(subscriptionKind, subscriptionRef.Namespace, subscriptionRef.Name)) +} + +// RecordBusEventf creates a new event for the reconciled bus and records it +// with the api server. +func (r *Reconciler) RecordBusEventf(eventtype, reason, messageFmt string, args ...interface{}) { + r.recorder.Eventf(r.bus, eventtype, reason, messageFmt, args...) +} + +// RecordChannelEventf creates a new event for the channel and records it with +// the api server. Attempts to records an event for an unknown channel are +// ignored. +func (r *Reconciler) RecordChannelEventf(channelRef ChannelReference, eventtype, reason, messageFmt string, args ...interface{}) { + channel, err := r.cache.Channel(channelRef) + if err != nil { + // TODO handle error + return + } + r.recorder.Eventf(channel, eventtype, reason, messageFmt, args...) +} + +// RecordSubscriptionEventf creates a new event for the subscription and +// records it with the api server. Attempts to records an event for an unknown +// subscription are ignored. +func (r *Reconciler) RecordSubscriptionEventf(subscriptionRef SubscriptionReference, eventtype, reason, messageFmt string, args ...interface{}) { + subscription, err := r.cache.Subscription(subscriptionRef) + if err != nil { + // TODO handle error + return + } + r.recorder.Eventf(subscription, eventtype, reason, messageFmt, args...) +} + +// Run will set up the event handlers for types we are interested in, as well +// as syncing informer caches and starting workers. It will block until stopCh +// is closed, at which point it will shutdown the workqueue and wait for +// workers to finish processing their current work items. +func (r *Reconciler) Run(busRef BusReference, threadiness int, stopCh <-chan struct{}) error { + defer runtime.HandleCrash() + defer r.workqueue.ShutDown() + + // Start the informer factories to begin populating the informer caches + glog.Info("Starting reconciler") + go r.eventingInformerFactory.Start(stopCh) + + // Wait for the caches to be synced before starting workers + glog.Info("Waiting for informer caches to sync") + if err := r.WaitForCacheSync(stopCh); err != nil { + return err + } + + if len(busRef.Namespace) == 0 { + // reconciler is for a ClusterBus + clusterBus, err := r.clusterBusesLister.Get(busRef.Name) + if err != nil { + glog.Fatalf("Unknown clusterbus %q: %v", busRef.Name, err) + } + r.bus = clusterBus.DeepCopy() + } else { + // reconciler is for a namespaced Bus + bus, err := r.busesLister.Buses(busRef.Namespace).Get(busRef.Name) + if err != nil { + glog.Fatalf("Unknown bus %q: %v", busRef, err) + } + r.bus = bus.DeepCopy() + } + + glog.Info("Starting workers") + // Launch workers to process resources + for i := 0; i < threadiness; i++ { + go wait.Until(r.runWorker, time.Second, stopCh) + } + + glog.Info("Started workers") + <-stopCh + glog.Info("Shutting down workers") + + return nil +} + +// WaitForCacheSync blocks returning until the reconciler's informers have +// synchronized. It returns an error if the caches cannot sync. +func (r *Reconciler) WaitForCacheSync(stopCh <-chan struct{}) error { + if ok := informercache.WaitForCacheSync(stopCh, r.busesSynced, r.clusterBusesSynced, r.channelsSynced, r.subscriptionsSynced); !ok { + return fmt.Errorf("failed to wait for caches to sync") + } + return nil +} + +// runWorker is a long-running function that will continually call the +// processNextWorkItem function in order to read and process a message on the +// workqueue. +func (r *Reconciler) runWorker() { + for r.processNextWorkItem() { + } +} + +// processNextWorkItem will read a single work item off the workqueue and +// attempt to process it, by calling the syncHandler. +func (r *Reconciler) processNextWorkItem() bool { + obj, shutdown := r.workqueue.Get() + + if shutdown { + return false + } + + // We wrap this block in a func so we can defer r.workqueue.Done. + err := func(obj interface{}) error { + // We call Done here so the workqueue knows we have finished + // processing this item. We also must remember to call Forget if we + // do not want this work item being re-queued. For example, we do + // not call Forget if a transient error occurs, instead the item is + // put back on the workqueue and attempted again after a back-off + // period. + defer r.workqueue.Done(obj) + var key string + var ok bool + // We expect strings to come off the workqueue. These are of the + // form namespace/name. We do this as the delayed nature of the + // workqueue means the items in the informer cache may actually be + // more up to date that when the item was initially put onto the + // workqueue. + if key, ok = obj.(string); !ok { + // As the item in the workqueue is actually invalid, we call + // Forget here else we'd go into a loop of attempting to + // process a work item that is invalid. + r.workqueue.Forget(obj) + runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) + return nil + } + // Run the syncHandler, passing it the name string of the resource to be synced. + if err := r.syncHandler(key); err != nil { + r.workqueue.AddRateLimited(obj) + return fmt.Errorf("error syncing reconciler '%s': %s", key, err.Error()) + } + // Finally, if no error occurs we Forget this item so it does not + // get queued again until another change happens. + r.workqueue.Forget(obj) + glog.Infof("Successfully synced reconciler '%s'", key) + return nil + }(obj) + + if err != nil { + runtime.HandleError(err) + return true + } + + return true +} + +// syncHandler compares the actual state with the desired, and attempts to +// converge the two. It then updates the Status block of the resource with the +// current status. +func (r *Reconciler) syncHandler(key string) error { + // Convert the namespace/name string into a distinct namespace and name + kind, namespace, name, err := splitWorkqueueKey(key) + if err != nil { + runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) + return nil + } + + if r.bus == nil && !(kind == busKind || kind == clusterBusKind) { + // don't attempt to sync until we have seen the bus for this reconciler + return fmt.Errorf("Unknown bus for reconciler") + } + + switch kind { + case busKind: + err = r.syncBus(namespace, name) + case clusterBusKind: + err = r.syncClusterBus(name) + case channelKind: + err = r.syncChannel(namespace, name) + case subscriptionKind: + err = r.syncSubscription(namespace, name) + default: + runtime.HandleError(fmt.Errorf("Unknown resource kind %s", kind)) + return nil + } + + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) syncBus(namespace string, name string) error { + // Get the Bus resource with this namespace/name + bus, err := r.busesLister.Buses(namespace).Get(name) + if err != nil { + // The Bus resource may no longer exist + if errors.IsNotFound(err) { + // nothing to do + return nil + } + + return err + } + + // Sync the Bus + err = r.createOrUpdateBus(bus) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) syncClusterBus(name string) error { + // Get the ClusterBus resource with this name + clusterBus, err := r.clusterBusesLister.Get(name) + if err != nil { + // The ClusterBus resource may no longer exist + if errors.IsNotFound(err) { + // nothing to do + return nil + } + + return err + } + + // Sync the ClusterBus + err = r.createOrUpdateClusterBus(clusterBus) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) syncChannel(namespace string, name string) error { + // Get the Channel resource with this namespace/name + channel, err := r.channelsLister.Channels(namespace).Get(name) + if err != nil { + // The Channel resource may no longer exist + if errors.IsNotFound(err) { + channelRef := NewChannelReferenceFromNames(name, namespace) + err = r.removeChannel(channelRef) + if err != nil { + return err + } + return nil + } + + return err + } + + // Sync the Channel + err = r.createOrUpdateChannel(channel) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) syncSubscription(namespace string, name string) error { + // Get the Subscription resource with this namespace/name + subscription, err := r.subscriptionsLister.Subscriptions(namespace).Get(name) + if err != nil { + // The Subscription resource may no longer exist + if errors.IsNotFound(err) { + subscriptionRef := NewSubscriptionReferenceFromNames(name, namespace) + err = r.removeSubscription(subscriptionRef) + if err != nil { + return err + } + return nil + } + + return err + } + + // Sync the Subscription + err = r.createOrUpdateSubscription(subscription) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) createOrUpdateBus(bus *channelsv1alpha1.Bus) error { + if r.bus.GetObjectKind().GroupVersionKind().Kind != bus.Kind || + bus.Namespace != r.bus.GetObjectMeta().GetNamespace() || + bus.Name != r.bus.GetObjectMeta().GetName() { + // this is not our bus + return nil + } + + previousBus := r.bus + r.bus = bus.DeepCopy() + if !equality.Semantic.DeepEqual(r.bus.GetSpec(), previousBus.GetSpec()) { + err := r.handler.onBus(r.bus, r) + if err != nil { + return err + } + } + + return nil +} + +func (r *Reconciler) createOrUpdateClusterBus(clusterBus *channelsv1alpha1.ClusterBus) error { + if r.bus.GetObjectKind().GroupVersionKind().Kind != clusterBus.Kind || + clusterBus.Name != r.bus.GetObjectMeta().GetName() { + // this is not our clusterbus + return nil + } + + previousBus := r.bus + r.bus = clusterBus.DeepCopy() + if !equality.Semantic.DeepEqual(r.bus.GetSpec(), previousBus.GetSpec()) { + err := r.handler.onBus(r.bus, r) + if err != nil { + return err + } + } + + return nil +} + +func (r *Reconciler) createOrUpdateChannel(channel *channelsv1alpha1.Channel) error { + if !r.bus.BacksChannel(channel) { + return nil + } + + r.cache.AddChannel(channel) + err := r.handler.onProvision(channel, r) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) removeChannel(channelRef ChannelReference) error { + channel, err := r.cache.Channel(channelRef) + if err != nil { + // the channel isn't provisioned + return nil + } + + err = r.handler.onUnprovision(channel, r) + if err != nil { + return err + } + r.cache.RemoveChannel(channel) + + return nil +} + +func (r *Reconciler) createOrUpdateSubscription(subscription *channelsv1alpha1.Subscription) error { + channelRef := NewChannelReferenceFromSubscription(subscription) + _, err := r.cache.Channel(channelRef) + if err != nil { + // channel is not provisioned, before erring we need to check if the channel is provionable + channel, errS := r.channelsLister.Channels(channelRef.Namespace).Get(channelRef.Name) + if errS != nil { + return err + } + if !r.bus.BacksChannel(channel) { + return nil + } + return err + } + + r.cache.AddSubscription(subscription) + err = r.handler.onSubscribe(subscription, r) + if err != nil { + return err + } + + return nil +} + +func (r *Reconciler) removeSubscription(subscriptionRef SubscriptionReference) error { + subscription, err := r.cache.Subscription(subscriptionRef) + if err != nil { + return nil + } + + err = r.handler.onUnsubscribe(subscription, r) + if err != nil { + return err + } + r.cache.RemoveSubscription(subscription) + + return nil +} + +func makeWorkqueueKeyForBus(bus *channelsv1alpha1.Bus) string { + return makeWorkqueueKey(busKind, bus.Namespace, bus.Name) +} + +func makeWorkqueueKeyForClusterBus(clusterBus *channelsv1alpha1.ClusterBus) string { + return makeWorkqueueKey(clusterBusKind, "", clusterBus.Name) +} + +func makeWorkqueueKeyForChannel(channel *channelsv1alpha1.Channel) string { + return makeWorkqueueKey(channelKind, channel.Namespace, channel.Name) +} + +func makeWorkqueueKeyForSubscription(subscription *channelsv1alpha1.Subscription) string { + return makeWorkqueueKey(subscriptionKind, subscription.Namespace, subscription.Name) +} + +func makeWorkqueueKey(kind string, namespace string, name string) string { + return fmt.Sprintf("%s/%s/%s", kind, namespace, name) +} + +func splitWorkqueueKey(key string) (string, string, string, error) { + chunks := strings.Split(key, "/") + if len(chunks) != 3 { + return "", "", "", fmt.Errorf("Unknown workqueue key %v", key) + } + kind := chunks[0] + namespace := chunks[1] + name := chunks[2] + return kind, namespace, name, nil +} diff --git a/pkg/buses/references.go b/pkg/buses/references.go index df8cecb63cb..93e00be6bad 100644 --- a/pkg/buses/references.go +++ b/pkg/buses/references.go @@ -16,22 +16,86 @@ package buses -import "fmt" +import ( + "fmt" + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" +) + +// BusReference references a Bus or ClusterBus within the cluster by name and +// namespace. For ClusterBus the namespace will be an empty string. type BusReference struct { - Name string Namespace string + Name string +} + +// NewBusReferenceFromNames creates a BusReference for a name and namespace. +func NewBusReferenceFromNames(name, namespace string) BusReference { + return BusReference{ + Namespace: namespace, + Name: name, + } } func (r *BusReference) String() string { - return fmt.Sprintf("%s/%s", r.Namespace, r.Name) + if r.Namespace != "" { + return fmt.Sprintf("%s/%s", r.Namespace, r.Name) + } + return r.Name } +// ChannelReference references a Channel within the cluster by name and +// namespace. type ChannelReference struct { - Name string Namespace string + Name string +} + +// NewChannelReference creates a ChannelReference from a Channel +func NewChannelReference(channel *channelsv1alpha1.Channel) ChannelReference { + return NewChannelReferenceFromNames(channel.Name, channel.Namespace) +} + +// NewChannelReferenceFromSubscription creates a ChannelReference from a +// Subscription for a Channel. +func NewChannelReferenceFromSubscription(subscription *channelsv1alpha1.Subscription) ChannelReference { + return NewChannelReferenceFromNames(subscription.Spec.Channel, subscription.Namespace) +} + +// NewChannelReferenceFromNames creates a ChannelReference for a name and +// namespace. +func NewChannelReferenceFromNames(name, namespace string) ChannelReference { + return ChannelReference{ + Namespace: namespace, + Name: name, + } } func (r *ChannelReference) String() string { return fmt.Sprintf("%s/%s", r.Namespace, r.Name) } + +// SubscriptionReference references a Subscription within the cluster by name +// and namespace. +type SubscriptionReference struct { + Namespace string + Name string +} + +// NewSubscriptionReference creates a SubscriptionReference from a Subscription +func NewSubscriptionReference(subscription *channelsv1alpha1.Subscription) SubscriptionReference { + return NewSubscriptionReferenceFromNames(subscription.Name, subscription.Namespace) +} + +// NewSubscriptionReferenceFromNames creates a SubscriptionReference for a name and +// namespace. +func NewSubscriptionReferenceFromNames(name, namespace string) SubscriptionReference { + return SubscriptionReference{ + Namespace: namespace, + Name: name, + } +} + +func (r *SubscriptionReference) String() string { + return fmt.Sprintf("%s/%s", r.Namespace, r.Name) +} diff --git a/pkg/buses/references_test.go b/pkg/buses/references_test.go new file mode 100644 index 00000000000..c42f97170c6 --- /dev/null +++ b/pkg/buses/references_test.go @@ -0,0 +1,178 @@ +/* +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 buses_test + +import ( + "fmt" + "testing" + + channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" + "github.com/knative/eventing/pkg/buses" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + referencesTestNamespace = "test-namespace" + referencesTestBusName = "test-bus" + referencesTestClusterBusName = "test-clusterbus" + referencesTestChannelName = "test-channel" + referencesTestSubscriptionName = "test-subscription" +) + +func TestNewBusReferenceFromNames(t *testing.T) { + expected := buses.BusReference{ + Name: referencesTestBusName, + Namespace: referencesTestNamespace, + } + actual := buses.NewBusReferenceFromNames(referencesTestBusName, referencesTestNamespace) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + +func TestNewBusReferenceFromNames_ClusterBus(t *testing.T) { + expected := buses.BusReference{ + Name: referencesTestClusterBusName, + } + actual := buses.NewBusReferenceFromNames(referencesTestClusterBusName, "") + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + +func TestBusReference_String(t *testing.T) { + busRef := buses.BusReference{ + Name: referencesTestBusName, + Namespace: referencesTestNamespace, + } + expected := fmt.Sprintf("%s/%s", referencesTestNamespace, referencesTestBusName) + actual := busRef.String() + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + +func TestBusReference_String_ClusterBus(t *testing.T) { + busRef := buses.BusReference{ + Name: referencesTestClusterBusName, + } + expected := referencesTestClusterBusName + actual := busRef.String() + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + +func TestNewChannelReference(t *testing.T) { + channel := &channelsv1alpha1.Channel{ + ObjectMeta: metav1.ObjectMeta{ + Name: referencesTestChannelName, + Namespace: referencesTestNamespace, + }, + } + expected := buses.ChannelReference{ + Name: referencesTestChannelName, + Namespace: referencesTestNamespace, + } + actual := buses.NewChannelReference(channel) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "ChannelReference", expected, actual) + } +} + +func TestNewChannelReferenceFromSubscription(t *testing.T) { + subscription := &channelsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: referencesTestSubscriptionName, + Namespace: referencesTestNamespace, + }, + Spec: channelsv1alpha1.SubscriptionSpec{ + Channel: referencesTestChannelName, + }, + } + expected := buses.ChannelReference{ + Name: referencesTestChannelName, + Namespace: referencesTestNamespace, + } + actual := buses.NewChannelReferenceFromSubscription(subscription) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "ChannelReference", expected, actual) + } +} + +func TestNewChannelReferenceFromNames(t *testing.T) { + expected := buses.ChannelReference{ + Name: referencesTestChannelName, + Namespace: referencesTestNamespace, + } + actual := buses.NewChannelReferenceFromNames(referencesTestChannelName, referencesTestNamespace) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "ChannelReference", expected, actual) + } +} + +func TestChannelReference_String(t *testing.T) { + channelRef := buses.ChannelReference{ + Name: referencesTestChannelName, + Namespace: referencesTestNamespace, + } + expected := fmt.Sprintf("%s/%s", referencesTestNamespace, referencesTestChannelName) + actual := channelRef.String() + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "ChannelReference", expected, actual) + } +} + +func TestNewSubscriptionReference(t *testing.T) { + subscription := &channelsv1alpha1.Subscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: referencesTestSubscriptionName, + Namespace: referencesTestNamespace, + }, + } + expected := buses.SubscriptionReference{ + Name: referencesTestSubscriptionName, + Namespace: referencesTestNamespace, + } + actual := buses.NewSubscriptionReference(subscription) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "SubscriptionReference", expected, actual) + } +} + +func TestNewSubscriptionReferenceFromNames(t *testing.T) { + expected := buses.SubscriptionReference{ + Name: referencesTestSubscriptionName, + Namespace: referencesTestNamespace, + } + actual := buses.NewSubscriptionReferenceFromNames(referencesTestSubscriptionName, referencesTestNamespace) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "SubscriptionReference", expected, actual) + } +} + +func TestSubscriptionReference_String(t *testing.T) { + subscriptionRef := buses.SubscriptionReference{ + Name: referencesTestSubscriptionName, + Namespace: referencesTestNamespace, + } + expected := fmt.Sprintf("%s/%s", referencesTestNamespace, referencesTestSubscriptionName) + actual := subscriptionRef.String() + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "SubscriptionReference", expected, actual) + } +} diff --git a/pkg/buses/stub/bus.go b/pkg/buses/stub/bus.go new file mode 100644 index 00000000000..2a9fb884dc4 --- /dev/null +++ b/pkg/buses/stub/bus.go @@ -0,0 +1,117 @@ +/* + * 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 stub + +import ( + "github.com/golang/glog" + + "github.com/knative/eventing/pkg/buses" +) + +func NewStubBusDispatcher(busRef buses.BusReference, opts *buses.BusOpts) *StubBus { + bus := &StubBus{ + channels: make(map[buses.ChannelReference]*stubChannel), + } + handlerFuncs := buses.EventHandlerFuncs{ + ProvisionFunc: func(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) error { + glog.Infof("Provision channel %q\n", channelRef.String()) + bus.addChannel(channelRef, parameters) + return nil + }, + UnprovisionFunc: func(channelRef buses.ChannelReference) error { + glog.Infof("Unprovision channel %q\n", channelRef.String()) + bus.removeChannel(channelRef) + return nil + }, + SubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters) error { + glog.Infof("Subscribe %q to %q channel\n", subscriptionRef.String(), channelRef.String()) + bus.channel(channelRef).addSubscription(subscriptionRef, parameters, bus.dispatcher) + return nil + }, + UnsubscribeFunc: func(channelRef buses.ChannelReference, subscriptionRef buses.SubscriptionReference) error { + glog.Infof("Unsubscribe %q from %q channel\n", subscriptionRef.String(), channelRef.String()) + bus.channel(channelRef).removeSubscription(subscriptionRef) + return nil + }, + ReceiveMessageFunc: func(channelRef buses.ChannelReference, message *buses.Message) error { + glog.Infof("Recieved message for %q channel\n", channelRef.String()) + bus.channel(channelRef).receiveMessage(message) + return nil + }, + } + bus.dispatcher = buses.NewBusDispatcher(busRef, handlerFuncs, opts) + + return bus +} + +type StubBus struct { + dispatcher buses.BusDispatcher + channels map[buses.ChannelReference]*stubChannel +} + +func (b *StubBus) Run(threadness int, stopCh <-chan struct{}) { + b.dispatcher.Run(threadness, stopCh) +} + +func (b *StubBus) addChannel(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) { + b.channels[channelRef] = &stubChannel{ + parameters: parameters, + subscriptions: make(map[buses.SubscriptionReference]*stubSubscription), + } +} + +func (b *StubBus) removeChannel(channelRef buses.ChannelReference) { + delete(b.channels, channelRef) +} + +func (b *StubBus) channel(channelRef buses.ChannelReference) *stubChannel { + return b.channels[channelRef] +} + +type stubChannel struct { + channelRef buses.ChannelReference + parameters buses.ResolvedParameters + subscriptions map[buses.SubscriptionReference]*stubSubscription +} + +func (c *stubChannel) receiveMessage(message *buses.Message) { + for _, stubSubscription := range c.subscriptions { + go stubSubscription.dispatchMessage(message) + } +} + +func (c *stubChannel) addSubscription(subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters, bus buses.BusDispatcher) { + c.subscriptions[subscriptionRef] = &stubSubscription{ + bus: bus, + parameters: parameters, + subscriptionRef: subscriptionRef, + } +} + +func (c *stubChannel) removeSubscription(subscriptionRef buses.SubscriptionReference) { + delete(c.subscriptions, subscriptionRef) +} + +type stubSubscription struct { + bus buses.BusDispatcher + parameters buses.ResolvedParameters + subscriptionRef buses.SubscriptionReference +} + +func (s *stubSubscription) dispatchMessage(message *buses.Message) error { + return s.bus.DispatchMessage(s.subscriptionRef, message) +} diff --git a/pkg/buses/stub/stub_test.go b/pkg/buses/stub/bus_test.go similarity index 98% rename from pkg/buses/stub/stub_test.go rename to pkg/buses/stub/bus_test.go index ee7bbe8c445..2a75accdd4c 100644 --- a/pkg/buses/stub/stub_test.go +++ b/pkg/buses/stub/bus_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package stub import "testing" diff --git a/pkg/buses/stub/dispatcher/main.go b/pkg/buses/stub/dispatcher/main.go new file mode 100644 index 00000000000..bd96bb3033c --- /dev/null +++ b/pkg/buses/stub/dispatcher/main.go @@ -0,0 +1,53 @@ +/* + * 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 main + +import ( + "flag" + "os" + + "github.com/golang/glog" + + "github.com/knative/eventing/pkg/buses" + "github.com/knative/eventing/pkg/buses/stub" + "github.com/knative/pkg/signals" +) + +const ( + threadsPerReconciler = 1 +) + +func main() { + defer glog.Flush() + + busRef := buses.NewBusReferenceFromNames( + os.Getenv("BUS_NAME"), + os.Getenv("BUS_NAMESPACE"), + ) + + opts := &buses.BusOpts{} + + flag.StringVar(&opts.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") + flag.Parse() + + bus := stub.NewStubBusDispatcher(busRef, opts) + + // set up signals so we handle the first shutdown signal gracefully + stopCh := signals.SetupSignalHandler() + bus.Run(threadsPerReconciler, stopCh) +} diff --git a/pkg/buses/stub/main.go b/pkg/buses/stub/main.go deleted file mode 100644 index 8f82d4bace9..00000000000 --- a/pkg/buses/stub/main.go +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 main - -import ( - "flag" - "fmt" - "os" - - "github.com/golang/glog" - channelsv1alpha1 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" - "github.com/knative/eventing/pkg/buses" - "github.com/knative/pkg/signals" -) - -const ( - threadsPerMonitor = 1 -) - -var ( - masterURL string - kubeconfig string -) - -// StubBus is able to broadcast messages to multiple subscribers, but does not -// have any delivery guarantees. -// -// The stub bus is commonly used in development and testing, but is often not -// suitable for production environments. -type StubBus struct { - ref *buses.BusReference - monitor *buses.Monitor - receiver *buses.MessageReceiver - dispatcher *buses.MessageDispatcher -} - -// NewStubBus creates a stub bus. -func NewStubBus(ref *buses.BusReference, monitor *buses.Monitor) *StubBus { - bus := &StubBus{ - ref: ref, - monitor: monitor, - } - bus.dispatcher = buses.NewMessageDispatcher() - bus.receiver = buses.NewMessageReceiver(bus.receiveMessage) - return bus -} - -// Run starts the bus's monitor and receiver. This function will block until -// the stop channel receives a message. -func (b *StubBus) Run(stopCh <-chan struct{}) { - go func() { - if err := b.monitor.Run(b.ref.Namespace, b.ref.Name, 1, stopCh); err != nil { - glog.Fatalf("Error running monitor: %s", err.Error()) - } - }() - b.monitor.WaitForCacheSync(stopCh) - b.receiver.Run(stopCh) -} - -// receiveMessage receives new messages for the bus from the message receiver, -// looks up active subscriptions for the channel and dispatches the message to -// each subscriber. -func (b *StubBus) receiveMessage(channel *buses.ChannelReference, message *buses.Message) error { - subscriptions := b.monitor.Subscriptions(channel.Name, channel.Namespace) - if subscriptions == nil { - return buses.ErrUnknownChannel - } - for _, subscription := range *subscriptions { - go b.dispatchMessage(subscription, channel, message) - } - return nil -} - -// dispatchMessage dispatches messages for the bus to a channel's subscriber. -func (b *StubBus) dispatchMessage(subscription channelsv1alpha1.SubscriptionSpec, channel *buses.ChannelReference, message *buses.Message) { - subscriber := subscription.Subscriber - defaults := buses.DispatchDefaults{Namespace: channel.Namespace, ReplyTo: subscription.ReplyTo} - glog.Infof("Sending to %q for %q with defaults %+v", subscriber, channel, defaults) - b.dispatcher.DispatchMessage(message, subscriber, defaults) -} - -func main() { - defer glog.Flush() - - flag.Parse() - - // set up signals so we handle the first shutdown signal gracefully - stopCh := signals.SetupSignalHandler() - - busReference := &buses.BusReference{ - Namespace: os.Getenv("BUS_NAMESPACE"), - Name: os.Getenv("BUS_NAME"), - } - component := fmt.Sprintf("%s-%s", busReference.Name, buses.Dispatcher) - - monitor := buses.NewMonitor(component, masterURL, kubeconfig, buses.MonitorEventHandlerFuncs{ - ProvisionFunc: func(channel *channelsv1alpha1.Channel, parameters buses.ResolvedParameters) error { - glog.Infof("Provision channel %q\n", channel.Name) - return nil - }, - UnprovisionFunc: func(channel *channelsv1alpha1.Channel) error { - glog.Infof("Unprovision channel %q\n", channel.Name) - return nil - }, - SubscribeFunc: func(subscription *channelsv1alpha1.Subscription, parameters buses.ResolvedParameters) error { - glog.Infof("Subscribe %q to %q channel\n", subscription.Spec.Subscriber, subscription.Spec.Channel) - return nil - }, - UnsubscribeFunc: func(subscription *channelsv1alpha1.Subscription) error { - glog.Infof("Unsubscribe %q from %q channel\n", subscription.Spec.Subscriber, subscription.Spec.Channel) - return nil - }, - }) - bus := NewStubBus(busReference, monitor) - bus.Run(stopCh) -} - -func init() { - flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") -} diff --git a/pkg/controller/bus/controller.go b/pkg/controller/bus/controller.go index d5c83cd6fa0..221c36fafba 100644 --- a/pkg/controller/bus/controller.go +++ b/pkg/controller/bus/controller.go @@ -59,8 +59,6 @@ import ( const ( controllerAgentName = "bus-controller" serviceAccountName = "bus-operator" - provisionerRole = "provisioner" - dispatcherRole = "dispatcher" ) const ( @@ -126,7 +124,8 @@ func NewController( restConfig *rest.Config, kubeInformerFactory kubeinformers.SharedInformerFactory, busInformerFactory informers.SharedInformerFactory, - istioInformerFactory sharedinformers.SharedInformerFactory) controller.Interface { + istioInformerFactory sharedinformers.SharedInformerFactory, +) controller.Interface { // obtain references to shared index informers for the Bus, Deployment and Service // types. @@ -588,11 +587,7 @@ func (c *Controller) handleObject(obj interface{}) { // the appropriate OwnerReferences on the resource so handleObject can discover // the Bus resource that 'owns' it. func newDispatcherService(bus *channelsv1alpha1.Bus) *corev1.Service { - labels := map[string]string{ - "bus": bus.Name, - "namespace": bus.Namespace, - "role": dispatcherRole, - } + labels := dispatcherLabels(bus.Name, bus.Namespace) return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: controller.BusDispatcherServiceName(bus.Name, bus.Namespace), @@ -623,11 +618,7 @@ func newDispatcherService(bus *channelsv1alpha1.Bus) *corev1.Service { // the appropriate OwnerReferences on the resource so handleObject can discover // the Bus resource that 'owns' it. func newDispatcherDeployment(bus *channelsv1alpha1.Bus) *appsv1.Deployment { - labels := map[string]string{ - "bus": bus.Name, - "namespace": bus.Namespace, - "role": dispatcherRole, - } + labels := dispatcherLabels(bus.Name, bus.Namespace) one := int32(1) container := bus.Spec.Dispatcher.DeepCopy() container.Env = append(container.Env, @@ -688,11 +679,7 @@ func newDispatcherDeployment(bus *channelsv1alpha1.Bus) *appsv1.Deployment { // the appropriate OwnerReferences on the resource so handleObject can discover // the Bus resource that 'owns' it. func newProvisionerDeployment(bus *channelsv1alpha1.Bus) *appsv1.Deployment { - labels := map[string]string{ - "bus": bus.Name, - "namespace": bus.Namespace, - "role": provisionerRole, - } + labels := provisionerLabels(bus.Name, bus.Namespace) one := int32(1) container := bus.Spec.Provisioner.DeepCopy() container.Env = append(container.Env, @@ -744,3 +731,19 @@ func newProvisionerDeployment(bus *channelsv1alpha1.Bus) *appsv1.Deployment { }, } } + +func dispatcherLabels(busName, namespace string) map[string]string { + return map[string]string{ + "bus": busName, + "namespace": namespace, + "role": "dispatcher", + } +} + +func provisionerLabels(busName, namespace string) map[string]string { + return map[string]string{ + "bus": busName, + "namespace": namespace, + "role": "provisioner", + } +} diff --git a/pkg/controller/channel/controller.go b/pkg/controller/channel/controller.go index cf29e4fb428..175c0bc40c0 100644 --- a/pkg/controller/channel/controller.go +++ b/pkg/controller/channel/controller.go @@ -121,7 +121,8 @@ func NewController( restConfig *rest.Config, kubeInformerFactory kubeinformers.SharedInformerFactory, channelInformerFactory informers.SharedInformerFactory, - sharedInformerFactory sharedinformers.SharedInformerFactory) controller.Interface { + sharedInformerFactory sharedinformers.SharedInformerFactory, +) controller.Interface { // obtain references to shared index informers for the Service and Channel types. virtualserviceInformer := sharedInformerFactory.Networking().V1alpha3().VirtualServices() @@ -518,11 +519,11 @@ func newVirtualService(channel *channelsv1alpha1.Channel) *istiov1alpha3.Virtual "channel": channel.Name, } var destinationHost string - if len(channel.Spec.Bus) != 0 { + if channel.Spec.Bus != "" { labels["bus"] = channel.Spec.Bus destinationHost = controller.ServiceHostName(controller.BusDispatcherServiceName(channel.Spec.Bus, channel.Namespace), system.Namespace) } - if len(channel.Spec.ClusterBus) != 0 { + if channel.Spec.ClusterBus != "" { labels["clusterBus"] = channel.Spec.ClusterBus destinationHost = controller.ServiceHostName(controller.ClusterBusDispatcherServiceName(channel.Spec.ClusterBus), system.Namespace) } diff --git a/pkg/controller/clusterbus/controller.go b/pkg/controller/clusterbus/controller.go index 60bbea7722e..ff627145b29 100644 --- a/pkg/controller/clusterbus/controller.go +++ b/pkg/controller/clusterbus/controller.go @@ -56,8 +56,6 @@ import ( const ( controllerAgentName = "clusterbus-controller" serviceAccountName = "bus-operator" - provisionerRole = "provisioner" - dispatcherRole = "dispatcher" ) const ( @@ -108,7 +106,8 @@ func NewController( restConfig *rest.Config, kubeInformerFactory kubeinformers.SharedInformerFactory, clusterBusInformerFactory informers.SharedInformerFactory, - sharedInformerFactory sharedinformers.SharedInformerFactory) controller.Interface { + sharedInformerFactory sharedinformers.SharedInformerFactory, +) controller.Interface { // obtain references to shared index informers for the ClusterBus, Deployment and Service // types. @@ -514,10 +513,7 @@ func (c *Controller) handleObject(obj interface{}) { // the appropriate OwnerReferences on the resource so handleObject can discover // the ClusterBus resource that 'owns' it. func newDispatcherService(clusterBus *channelsv1alpha1.ClusterBus) *corev1.Service { - labels := map[string]string{ - "clusterBus": clusterBus.Name, - "role": dispatcherRole, - } + labels := dispatcherLabels(clusterBus.Name) return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: controller.ClusterBusDispatcherServiceName(clusterBus.ObjectMeta.Name), @@ -548,10 +544,7 @@ func newDispatcherService(clusterBus *channelsv1alpha1.ClusterBus) *corev1.Servi // the appropriate OwnerReferences on the resource so handleObject can discover // the ClusterBus resource that 'owns' it. func newDispatcherDeployment(clusterBus *channelsv1alpha1.ClusterBus) *appsv1.Deployment { - labels := map[string]string{ - "clusterBus": clusterBus.Name, - "role": dispatcherRole, - } + labels := dispatcherLabels(clusterBus.Name) one := int32(1) container := clusterBus.Spec.Dispatcher.DeepCopy() container.Env = append(container.Env, @@ -608,10 +601,7 @@ func newDispatcherDeployment(clusterBus *channelsv1alpha1.ClusterBus) *appsv1.De // the appropriate OwnerReferences on the resource so handleObject can discover // the ClusterBus resource that 'owns' it. func newProvisionerDeployment(clusterBus *channelsv1alpha1.ClusterBus) *appsv1.Deployment { - labels := map[string]string{ - "clusterBus": clusterBus.Name, - "role": provisionerRole, - } + labels := provisionerLabels(clusterBus.Name) one := int32(1) container := clusterBus.Spec.Provisioner.DeepCopy() container.Env = append(container.Env, @@ -659,3 +649,17 @@ func newProvisionerDeployment(clusterBus *channelsv1alpha1.ClusterBus) *appsv1.D }, } } + +func dispatcherLabels(busName string) map[string]string { + return map[string]string{ + "clusterBus": busName, + "role": "dispatcher", + } +} + +func provisionerLabels(busName string) map[string]string { + return map[string]string{ + "clusterBus": busName, + "role": "provisioner", + } +} From d08fcaf0fd0afdcaa1a8a584d9e9621fbf88f941 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 22 Aug 2018 18:48:38 -0400 Subject: [PATCH 2/6] Update path to stub bus dispatcher for e2e tests --- test/e2e/k8sevents/stub.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/k8sevents/stub.yaml b/test/e2e/k8sevents/stub.yaml index 3e071f862b7..6acab3858f3 100644 --- a/test/e2e/k8sevents/stub.yaml +++ b/test/e2e/k8sevents/stub.yaml @@ -19,7 +19,7 @@ metadata: spec: dispatcher: name: dispatcher - image: github.com/knative/eventing/pkg/buses/stub + image: github.com/knative/eventing/pkg/buses/stub/dispatcher args: [ "-logtostderr", "-stderrthreshold", "INFO", From cb8c668e303cb6433ef14dcb18b8b8199895bac0 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 22 Aug 2018 19:46:57 -0400 Subject: [PATCH 3/6] Properly update existing channels in stub bus --- pkg/buses/stub/bus.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/buses/stub/bus.go b/pkg/buses/stub/bus.go index 2a9fb884dc4..107a210179b 100644 --- a/pkg/buses/stub/bus.go +++ b/pkg/buses/stub/bus.go @@ -68,9 +68,15 @@ func (b *StubBus) Run(threadness int, stopCh <-chan struct{}) { } func (b *StubBus) addChannel(channelRef buses.ChannelReference, parameters buses.ResolvedParameters) { - b.channels[channelRef] = &stubChannel{ - parameters: parameters, - subscriptions: make(map[buses.SubscriptionReference]*stubSubscription), + if channel, ok := b.channels[channelRef]; ok { + // update channel + channel.parameters = parameters + } else { + // create channel + b.channels[channelRef] = &stubChannel{ + parameters: parameters, + subscriptions: make(map[buses.SubscriptionReference]*stubSubscription), + } } } @@ -95,6 +101,7 @@ func (c *stubChannel) receiveMessage(message *buses.Message) { } func (c *stubChannel) addSubscription(subscriptionRef buses.SubscriptionReference, parameters buses.ResolvedParameters, bus buses.BusDispatcher) { + // create or update subscription c.subscriptions[subscriptionRef] = &stubSubscription{ bus: bus, parameters: parameters, From fce72f2d4608b34641a1a9f6fced7ea8d2785477 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 22 Aug 2018 20:58:09 -0400 Subject: [PATCH 4/6] Review feedback --- .../v1alpha1/subscription_validation.go | 8 ++- .../v1alpha1/subscription_validation_test.go | 21 +++++--- pkg/buses/bus.go | 49 ++++++++++--------- pkg/buses/cache.go | 12 +++++ pkg/buses/cache_test.go | 26 +++++++--- pkg/buses/gcppubsub/bus.go | 2 +- pkg/buses/handler_funcs.go | 33 ++++++------- pkg/buses/references.go | 9 ++++ pkg/buses/references_test.go | 32 ++++++++++++ 9 files changed, 133 insertions(+), 59 deletions(-) diff --git a/pkg/apis/channels/v1alpha1/subscription_validation.go b/pkg/apis/channels/v1alpha1/subscription_validation.go index 8b56afb2add..c2f7ae8ab91 100644 --- a/pkg/apis/channels/v1alpha1/subscription_validation.go +++ b/pkg/apis/channels/v1alpha1/subscription_validation.go @@ -27,12 +27,16 @@ func (s *Subscription) Validate() *apis.FieldError { } func (ss *SubscriptionSpec) Validate() *apis.FieldError { - if len(ss.Channel) == 0 { + if ss.Channel == "" { fe := apis.ErrMissingField("channel") fe.Details = "the Subscription must reference a Channel" return fe } - // TODO: should subscriptions have a subscriber? + if ss.Subscriber == "" { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a Subscriber" + return fe + } return nil } diff --git a/pkg/apis/channels/v1alpha1/subscription_validation_test.go b/pkg/apis/channels/v1alpha1/subscription_validation_test.go index da6c8c64add..f585ab191b9 100644 --- a/pkg/apis/channels/v1alpha1/subscription_validation_test.go +++ b/pkg/apis/channels/v1alpha1/subscription_validation_test.go @@ -16,9 +16,10 @@ limitations under the License. package v1alpha1 import ( + "testing" + "github.com/google/go-cmp/cmp" "github.com/knative/pkg/apis" - "testing" ) func TestSubscriptionSpecValidation(t *testing.T) { @@ -28,25 +29,29 @@ func TestSubscriptionSpecValidation(t *testing.T) { want *apis.FieldError }{{ name: "valid", - c: &SubscriptionSpec{ - Channel: "foo", - }, - want: nil, - }, { - name: "valid with subscriber", c: &SubscriptionSpec{ Channel: "bar", Subscriber: "foo", }, want: nil, }, { - name: "valid with subscriber and arguments", + name: "valid with arguments", c: &SubscriptionSpec{ Channel: "bar", Subscriber: "foo", Arguments: &[]Argument{{Name: "foo", Value: "bar"}}, }, want: nil, + }, { + name: "missing subscriber", + c: &SubscriptionSpec{ + Channel: "foo", + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a Subscriber" + return fe + }(), }, { name: "empty", c: &SubscriptionSpec{}, diff --git a/pkg/buses/bus.go b/pkg/buses/bus.go index 4bc68309623..cb39fb9fd4a 100644 --- a/pkg/buses/bus.go +++ b/pkg/buses/bus.go @@ -68,22 +68,22 @@ type BusProvisioner interface { // EventHandlerFuncs are used to be notified when a channel or subscription is // created, updated or removed. func NewBusProvisioner(busRef BusReference, handlerFuncs EventHandlerFuncs, opts *BusOpts) BusProvisioner { - bus := &bus{ - busRef: busRef, - handlerFuncs: handlerFuncs, + if opts == nil { + opts = &BusOpts{} } - if opts.Cache == nil { opts.Cache = NewCache() } - bus.cache = opts.Cache - if opts.Reconciler == nil { - opts.Reconciler = NewReconciler(Provisioner, opts.MasterURL, opts.KubeConfig, bus.cache, handlerFuncs) + opts.Reconciler = NewReconciler(Provisioner, opts.MasterURL, opts.KubeConfig, opts.Cache, handlerFuncs) } - bus.reconciler = opts.Reconciler - return bus + return &bus{ + busRef: busRef, + handlerFuncs: handlerFuncs, + cache: opts.Cache, + reconciler: opts.Reconciler, + } } // BusDispatcher dispatches messages from channels to subscribers via backing @@ -97,34 +97,37 @@ type BusDispatcher interface { // EventHandlerFuncs are used to be notified when a subscription is created, // updated or removed, or a message is received. func NewBusDispatcher(busRef BusReference, handlerFuncs EventHandlerFuncs, opts *BusOpts) BusDispatcher { - bus := &bus{ - busRef: busRef, - handlerFuncs: handlerFuncs, - } + var b *bus + if opts == nil { + opts = &BusOpts{} + } if opts.Cache == nil { opts.Cache = NewCache() } - bus.cache = opts.Cache - if opts.Reconciler == nil { - opts.Reconciler = NewReconciler(Dispatcher, opts.MasterURL, opts.KubeConfig, bus.cache, handlerFuncs) + opts.Reconciler = NewReconciler(Dispatcher, opts.MasterURL, opts.KubeConfig, opts.Cache, handlerFuncs) } - bus.reconciler = opts.Reconciler - if opts.MessageDispatcher == nil { opts.MessageDispatcher = NewMessageDispatcher() } - bus.dispatcher = opts.MessageDispatcher - if opts.MessageReceiver == nil { opts.MessageReceiver = NewMessageReceiver(func(channelRef ChannelReference, message *Message) error { - return bus.receiveMessage(channelRef, message) + return b.receiveMessage(channelRef, message) }) } - bus.receiver = opts.MessageReceiver - return bus + b = &bus{ + busRef: busRef, + handlerFuncs: handlerFuncs, + + cache: opts.Cache, + reconciler: opts.Reconciler, + dispatcher: opts.MessageDispatcher, + receiver: opts.MessageReceiver, + } + + return b } // Run starts the bus's processing. diff --git a/pkg/buses/cache.go b/pkg/buses/cache.go index c0bf23636b6..b263d140745 100644 --- a/pkg/buses/cache.go +++ b/pkg/buses/cache.go @@ -62,12 +62,18 @@ func (c *Cache) Subscription(subscriptionRef SubscriptionReference) (*channelsv1 // AddChannel adds, or updates, the provided channel to the cache for later // retrieal by its reference. func (c *Cache) AddChannel(channel *channelsv1alpha1.Channel) { + if channel == nil { + return + } channelRef := NewChannelReference(channel) c.channels[channelRef] = channel } // RemoveChannel removes the provided channel from the cache. func (c *Cache) RemoveChannel(channel *channelsv1alpha1.Channel) { + if channel == nil { + return + } channelRef := NewChannelReference(channel) delete(c.channels, channelRef) } @@ -75,12 +81,18 @@ func (c *Cache) RemoveChannel(channel *channelsv1alpha1.Channel) { // AddSubscription adds, or updates, the provided subscription to the cache for // later retrieal by its reference. func (c *Cache) AddSubscription(subscription *channelsv1alpha1.Subscription) { + if subscription == nil { + return + } subscriptionRef := NewSubscriptionReference(subscription) c.subscriptions[subscriptionRef] = subscription } // RemoveSubscription removes the provided subscription from the cache. func (c *Cache) RemoveSubscription(subscription *channelsv1alpha1.Subscription) { + if subscription == nil { + return + } subscriptionRef := NewSubscriptionReference(subscription) delete(c.subscriptions, subscriptionRef) } diff --git a/pkg/buses/cache_test.go b/pkg/buses/cache_test.go index f8403bbce18..e6c38c8663e 100644 --- a/pkg/buses/cache_test.go +++ b/pkg/buses/cache_test.go @@ -30,7 +30,7 @@ const ( cacheTestSubscription = "test-subscription" ) -func TestStoreErrsForUnknownChannel(t *testing.T) { +func TestCacheErrsForUnknownChannel(t *testing.T) { cache := buses.NewCache() channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) var expected *channelsv1alpha1.Channel @@ -43,7 +43,7 @@ func TestStoreErrsForUnknownChannel(t *testing.T) { } } -func TestStoreRetrievesKnownChannel(t *testing.T) { +func TestCacheRetrievesKnownChannel(t *testing.T) { cache := buses.NewCache() channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) expected := makeChannel(channelRef) @@ -57,7 +57,7 @@ func TestStoreRetrievesKnownChannel(t *testing.T) { } } -func TestStoreRemovesKnownChannel(t *testing.T) { +func TestCacheRemovesKnownChannel(t *testing.T) { cache := buses.NewCache() channelRef := buses.NewChannelReferenceFromNames(cacheTestChannel, cacheDefaultNamespace) channel := makeChannel(channelRef) @@ -73,7 +73,14 @@ func TestStoreRemovesKnownChannel(t *testing.T) { } } -func TestStoreErrsForUnknownSubscription(t *testing.T) { +func TestCacheNilChannel(t *testing.T) { + cache := buses.NewCache() + var channel *channelsv1alpha1.Channel + cache.AddChannel(channel) + cache.RemoveChannel(channel) +} + +func TestCacheErrsForUnknownSubscription(t *testing.T) { cache := buses.NewCache() subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) var expected *channelsv1alpha1.Subscription @@ -86,7 +93,7 @@ func TestStoreErrsForUnknownSubscription(t *testing.T) { } } -func TestStoreRetrievesKnownSubscription(t *testing.T) { +func TestCacheRetrievesKnownSubscription(t *testing.T) { cache := buses.NewCache() subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) expected := makeSubscription(subscriptionRef) @@ -100,7 +107,7 @@ func TestStoreRetrievesKnownSubscription(t *testing.T) { } } -func TestStoreRemovesKnownSubscription(t *testing.T) { +func TestCacheRemovesKnownSubscription(t *testing.T) { cache := buses.NewCache() subscriptionRef := buses.NewSubscriptionReferenceFromNames(cacheTestSubscription, cacheDefaultNamespace) subscription := makeSubscription(subscriptionRef) @@ -116,6 +123,13 @@ func TestStoreRemovesKnownSubscription(t *testing.T) { } } +func TestCacheNilSubscription(t *testing.T) { + cache := buses.NewCache() + var subscription *channelsv1alpha1.Subscription + cache.AddSubscription(subscription) + cache.RemoveSubscription(subscription) +} + func makeChannel(channelRef buses.ChannelReference) *channelsv1alpha1.Channel { return &channelsv1alpha1.Channel{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/buses/gcppubsub/bus.go b/pkg/buses/gcppubsub/bus.go index 6fe26ef1554..98b9007bf02 100644 --- a/pkg/buses/gcppubsub/bus.go +++ b/pkg/buses/gcppubsub/bus.go @@ -240,7 +240,7 @@ func (b *CloudPubSubBus) createOrUpdateSubscription(channelRef buses.ChannelRefe if exists, err := subscription.Exists(ctx); err != nil { return err } else if exists { - // TODO update subscription configuration + // TODO once the bus has configurable params, update subscription configuration // _, err := subscription.Update(b.ctx, pubsub.SubscriptionConfigToUpdate{}) // return err return nil diff --git a/pkg/buses/handler_funcs.go b/pkg/buses/handler_funcs.go index 41e320b4c7f..0779a8a527f 100644 --- a/pkg/buses/handler_funcs.go +++ b/pkg/buses/handler_funcs.go @@ -43,7 +43,7 @@ type ResolvedParameters = map[string]string // created or deleted, or if one of the relevant resources is changed. type EventHandlerFuncs struct { // BusFunc is invoked when the Bus requires sync. - BusFunc func(bus channelsv1alpha1.GenericBus) error + BusFunc func(busRef BusReference) error // ProvisionFunc is invoked when a new Channel should be provisioned or when // the attributes change. @@ -65,7 +65,8 @@ type EventHandlerFuncs struct { func (h EventHandlerFuncs) onBus(bus channelsv1alpha1.GenericBus, reconciler *Reconciler) error { if h.BusFunc != nil { - err := h.BusFunc(bus) + busRef := NewBusReference(bus) + err := h.BusFunc(busRef) if err != nil { reconciler.RecordBusEventf(corev1.EventTypeWarning, errResourceSync, "Error syncing Bus: %s", err) } else { @@ -82,10 +83,10 @@ func (h EventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, reconc if err != nil { return err } - channelRef := NewChannelReference(channel) - err = h.ProvisionFunc(channelRef, parameters) channelCopy := channel.DeepCopy() var cond *channelsv1alpha1.ChannelCondition + channelRef := NewChannelReference(channel) + err = h.ProvisionFunc(channelRef, parameters) if err != nil { reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error provisioning channel: %s", err) cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionFalse, errResourceSync, err.Error()) @@ -101,8 +102,7 @@ func (h EventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, reconc if errS != nil { glog.Warningf("Could not update channel status: %v", errS) if err != nil { - // return the original err, since it is more meaningful - return err + return fmt.Errorf("error provisioning channel (%v); error updating channel status (%v)", err, errS) } return errS } @@ -115,14 +115,12 @@ func (h EventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, reconc func (h EventHandlerFuncs) onUnprovision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { if h.UnprovisionFunc != nil { channelRef := NewChannelReference(channel) - err := h.UnprovisionFunc(channelRef) - if err != nil { + if err := h.UnprovisionFunc(channelRef); err != nil { reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error unprovisioning channel: %s", err) - } else { - reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") + return err } + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") // skip updating status conditions since the channel was deleted - return err } return nil } @@ -135,9 +133,9 @@ func (h EventHandlerFuncs) onSubscribe(subscription *channelsv1alpha1.Subscripti } channelRef := NewChannelReferenceFromSubscription(subscription) subscriptionRef := NewSubscriptionReference(subscription) - err = h.SubscribeFunc(channelRef, subscriptionRef, parameters) subscriptionCopy := subscription.DeepCopy() var cond *channelsv1alpha1.SubscriptionCondition + err = h.SubscribeFunc(channelRef, subscriptionRef, parameters) if err != nil { reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error subscribing: %s", err) cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionFalse, errResourceSync, err.Error()) @@ -152,8 +150,7 @@ func (h EventHandlerFuncs) onSubscribe(subscription *channelsv1alpha1.Subscripti if errS != nil { glog.Warningf("Could not update subscription status: %v", errS) if err != nil { - // return the original err, since it is more meaningful - return err + return fmt.Errorf("error subscribing (%v); error updating subscription status (%v)", err, errS) } return errS } @@ -167,14 +164,12 @@ func (h EventHandlerFuncs) onUnsubscribe(subscription *channelsv1alpha1.Subscrip if h.UnsubscribeFunc != nil { channelRef := NewChannelReferenceFromSubscription(subscription) subscriptionRef := NewSubscriptionReference(subscription) - err := h.UnsubscribeFunc(channelRef, subscriptionRef) - if err != nil { + if err := h.UnsubscribeFunc(channelRef, subscriptionRef); err != nil { reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error unsubscribing: %s", err) - } else { - reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") + return err } + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") // skip updating status conditions since the subscription was deleted - return err } return nil } diff --git a/pkg/buses/references.go b/pkg/buses/references.go index 93e00be6bad..0f4029ed8a3 100644 --- a/pkg/buses/references.go +++ b/pkg/buses/references.go @@ -29,6 +29,15 @@ type BusReference struct { Name string } +// NewBusReference creates a BusReference for a bus. +func NewBusReference(bus channelsv1alpha1.GenericBus) BusReference { + meta := bus.GetObjectMeta() + return BusReference{ + Namespace: meta.GetNamespace(), + Name: meta.GetName(), + } +} + // NewBusReferenceFromNames creates a BusReference for a name and namespace. func NewBusReferenceFromNames(name, namespace string) BusReference { return BusReference{ diff --git a/pkg/buses/references_test.go b/pkg/buses/references_test.go index c42f97170c6..91d3f147fa0 100644 --- a/pkg/buses/references_test.go +++ b/pkg/buses/references_test.go @@ -33,6 +33,38 @@ const ( referencesTestSubscriptionName = "test-subscription" ) +func TestNewBusReference(t *testing.T) { + bus := &channelsv1alpha1.Bus{ + ObjectMeta: metav1.ObjectMeta{ + Name: referencesTestBusName, + Namespace: referencesTestNamespace, + }, + } + expected := buses.BusReference{ + Name: referencesTestBusName, + Namespace: referencesTestNamespace, + } + actual := buses.NewBusReference(bus) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + +func TestNewBusReference_ClusterBus(t *testing.T) { + clusterBus := &channelsv1alpha1.ClusterBus{ + ObjectMeta: metav1.ObjectMeta{ + Name: referencesTestClusterBusName, + }, + } + expected := buses.BusReference{ + Name: referencesTestClusterBusName, + } + actual := buses.NewBusReference(clusterBus) + if expected != actual { + t.Errorf("%s expected: %+v got: %+v", "BusReference", expected, actual) + } +} + func TestNewBusReferenceFromNames(t *testing.T) { expected := buses.BusReference{ Name: referencesTestBusName, From 26605a1b9f20a4544a95ede7f673ab6967ece5a8 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 23 Aug 2018 10:48:57 -0400 Subject: [PATCH 5/6] Fix kafka bus --- pkg/buses/kafka/dispatcher/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/buses/kafka/dispatcher/main.go b/pkg/buses/kafka/dispatcher/main.go index 812706bd60a..6c69bfe9d88 100644 --- a/pkg/buses/kafka/dispatcher/main.go +++ b/pkg/buses/kafka/dispatcher/main.go @@ -53,9 +53,9 @@ func main() { flag.StringVar(&opts.MasterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") flag.Parse() - bus, err := kafka.NewKafkaBusProvisioner(busRef, brokers, opts) + bus, err := kafka.NewKafkaBusDispatcher(busRef, brokers, opts) if err != nil { - glog.Fatalf("Error starting kafka bus provisioner: %v", err) + glog.Fatalf("Error starting kafka bus dispatcher: %v", err) } // set up signals so we handle the first shutdown signal gracefully From b0ece002db5220bbc41d1f456ba1de9a358b9c08 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 27 Aug 2018 20:06:52 -0400 Subject: [PATCH 6/6] Review feedback - return early from handlerfuncs if func is nil --- pkg/buses/handler_funcs.go | 172 +++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/pkg/buses/handler_funcs.go b/pkg/buses/handler_funcs.go index 0779a8a527f..ef3a8840e3e 100644 --- a/pkg/buses/handler_funcs.go +++ b/pkg/buses/handler_funcs.go @@ -64,113 +64,115 @@ type EventHandlerFuncs struct { } func (h EventHandlerFuncs) onBus(bus channelsv1alpha1.GenericBus, reconciler *Reconciler) error { - if h.BusFunc != nil { - busRef := NewBusReference(bus) - err := h.BusFunc(busRef) - if err != nil { - reconciler.RecordBusEventf(corev1.EventTypeWarning, errResourceSync, "Error syncing Bus: %s", err) - } else { - reconciler.RecordBusEventf(corev1.EventTypeNormal, successSynced, "Bus synched successfully") - } - return err + if h.BusFunc == nil { + return nil } - return nil + busRef := NewBusReference(bus) + err := h.BusFunc(busRef) + if err != nil { + reconciler.RecordBusEventf(corev1.EventTypeWarning, errResourceSync, "Error syncing Bus: %s", err) + } else { + reconciler.RecordBusEventf(corev1.EventTypeNormal, successSynced, "Bus synched successfully") + } + return err } func (h EventHandlerFuncs) onProvision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { - if h.ProvisionFunc != nil { - parameters, err := h.resolveChannelParameters(reconciler.bus.GetSpec(), channel.Spec) - if err != nil { - return err - } - channelCopy := channel.DeepCopy() - var cond *channelsv1alpha1.ChannelCondition - channelRef := NewChannelReference(channel) - err = h.ProvisionFunc(channelRef, parameters) - if err != nil { - reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error provisioning channel: %s", err) - cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionFalse, errResourceSync, err.Error()) - } else { - reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel provisioned successfully") - cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionTrue, successSynced, "Channel provisioned successfully") - } - util.SetChannelCondition(&channelCopy.Status, *cond) - util.ConsolidateChannelCondition(&channelCopy.Status) - if !equality.Semantic.DeepEqual(channel.Status, channelCopy.Status) { - // status has changed, update channel - _, errS := reconciler.eventingClient.ChannelsV1alpha1().Channels(channel.Namespace).Update(channelCopy) - if errS != nil { - glog.Warningf("Could not update channel status: %v", errS) - if err != nil { - return fmt.Errorf("error provisioning channel (%v); error updating channel status (%v)", err, errS) - } - return errS + if h.ProvisionFunc == nil { + return nil + } + parameters, err := h.resolveChannelParameters(reconciler.bus.GetSpec(), channel.Spec) + if err != nil { + return err + } + channelCopy := channel.DeepCopy() + var cond *channelsv1alpha1.ChannelCondition + channelRef := NewChannelReference(channel) + err = h.ProvisionFunc(channelRef, parameters) + if err != nil { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error provisioning channel: %s", err) + cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionFalse, errResourceSync, err.Error()) + } else { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel provisioned successfully") + cond = util.NewChannelCondition(channelsv1alpha1.ChannelProvisioned, corev1.ConditionTrue, successSynced, "Channel provisioned successfully") + } + util.SetChannelCondition(&channelCopy.Status, *cond) + util.ConsolidateChannelCondition(&channelCopy.Status) + if !equality.Semantic.DeepEqual(channel.Status, channelCopy.Status) { + // status has changed, update channel + _, errS := reconciler.eventingClient.ChannelsV1alpha1().Channels(channel.Namespace).Update(channelCopy) + if errS != nil { + glog.Warningf("Could not update channel status: %v", errS) + if err != nil { + return fmt.Errorf("error provisioning channel (%v); error updating channel status (%v)", err, errS) } + return errS } - return err } - return nil + return err } func (h EventHandlerFuncs) onUnprovision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { - if h.UnprovisionFunc != nil { - channelRef := NewChannelReference(channel) - if err := h.UnprovisionFunc(channelRef); err != nil { - reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error unprovisioning channel: %s", err) - return err - } - reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") - // skip updating status conditions since the channel was deleted + if h.UnprovisionFunc == nil { + return nil } + channelRef := NewChannelReference(channel) + if err := h.UnprovisionFunc(channelRef); err != nil { + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeWarning, errResourceSync, "Error unprovisioning channel: %s", err) + return err + } + reconciler.RecordChannelEventf(channelRef, corev1.EventTypeNormal, successSynced, "Channel unprovisioned successfully") + // skip updating status conditions since the channel was deleted return nil } func (h EventHandlerFuncs) onSubscribe(subscription *channelsv1alpha1.Subscription, reconciler *Reconciler) error { - if h.SubscribeFunc != nil { - parameters, err := h.resolveSubscriptionParameters(reconciler.bus.GetSpec(), subscription.Spec) - if err != nil { - return err - } - channelRef := NewChannelReferenceFromSubscription(subscription) - subscriptionRef := NewSubscriptionReference(subscription) - subscriptionCopy := subscription.DeepCopy() - var cond *channelsv1alpha1.SubscriptionCondition - err = h.SubscribeFunc(channelRef, subscriptionRef, parameters) - if err != nil { - reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error subscribing: %s", err) - cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionFalse, errResourceSync, err.Error()) - } else { - reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Subscribed successfully") - cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionTrue, successSynced, "Subscription dispatcher successfully created") - } - util.SetSubscriptionCondition(&subscriptionCopy.Status, *cond) - if !equality.Semantic.DeepEqual(subscription.Status, subscriptionCopy.Status) { - // status has changed, update subscription - _, errS := reconciler.eventingClient.ChannelsV1alpha1().Subscriptions(subscription.Namespace).Update(subscriptionCopy) - if errS != nil { - glog.Warningf("Could not update subscription status: %v", errS) - if err != nil { - return fmt.Errorf("error subscribing (%v); error updating subscription status (%v)", err, errS) - } - return errS + if h.SubscribeFunc == nil { + return nil + } + parameters, err := h.resolveSubscriptionParameters(reconciler.bus.GetSpec(), subscription.Spec) + if err != nil { + return err + } + channelRef := NewChannelReferenceFromSubscription(subscription) + subscriptionRef := NewSubscriptionReference(subscription) + subscriptionCopy := subscription.DeepCopy() + var cond *channelsv1alpha1.SubscriptionCondition + err = h.SubscribeFunc(channelRef, subscriptionRef, parameters) + if err != nil { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error subscribing: %s", err) + cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionFalse, errResourceSync, err.Error()) + } else { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Subscribed successfully") + cond = util.NewSubscriptionCondition(channelsv1alpha1.SubscriptionDispatching, corev1.ConditionTrue, successSynced, "Subscription dispatcher successfully created") + } + util.SetSubscriptionCondition(&subscriptionCopy.Status, *cond) + if !equality.Semantic.DeepEqual(subscription.Status, subscriptionCopy.Status) { + // status has changed, update subscription + _, errS := reconciler.eventingClient.ChannelsV1alpha1().Subscriptions(subscription.Namespace).Update(subscriptionCopy) + if errS != nil { + glog.Warningf("Could not update subscription status: %v", errS) + if err != nil { + return fmt.Errorf("error subscribing (%v); error updating subscription status (%v)", err, errS) } + return errS } - return err } - return nil + return err } func (h EventHandlerFuncs) onUnsubscribe(subscription *channelsv1alpha1.Subscription, reconciler *Reconciler) error { - if h.UnsubscribeFunc != nil { - channelRef := NewChannelReferenceFromSubscription(subscription) - subscriptionRef := NewSubscriptionReference(subscription) - if err := h.UnsubscribeFunc(channelRef, subscriptionRef); err != nil { - reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error unsubscribing: %s", err) - return err - } - reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") - // skip updating status conditions since the subscription was deleted + if h.UnsubscribeFunc == nil { + return nil + } + channelRef := NewChannelReferenceFromSubscription(subscription) + subscriptionRef := NewSubscriptionReference(subscription) + if err := h.UnsubscribeFunc(channelRef, subscriptionRef); err != nil { + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeWarning, errResourceSync, "Error unsubscribing: %s", err) + return err } + reconciler.RecordSubscriptionEventf(subscriptionRef, corev1.EventTypeNormal, successSynced, "Unsubscribed successfully") + // skip updating status conditions since the subscription was deleted return nil }