diff --git a/Gopkg.lock b/Gopkg.lock index 1fa1d5e3ae0..00ce7bbd407 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1053,7 +1053,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/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 new file mode 100644 index 00000000000..cb39fb9fd4a --- /dev/null +++ b/pkg/buses/bus.go @@ -0,0 +1,169 @@ +/* + * 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 { + if opts == nil { + opts = &BusOpts{} + } + if opts.Cache == nil { + opts.Cache = NewCache() + } + if opts.Reconciler == nil { + opts.Reconciler = NewReconciler(Provisioner, opts.MasterURL, opts.KubeConfig, opts.Cache, handlerFuncs) + } + + return &bus{ + busRef: busRef, + handlerFuncs: handlerFuncs, + cache: opts.Cache, + reconciler: opts.Reconciler, + } +} + +// 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 { + var b *bus + + if opts == nil { + opts = &BusOpts{} + } + if opts.Cache == nil { + opts.Cache = NewCache() + } + if opts.Reconciler == nil { + opts.Reconciler = NewReconciler(Dispatcher, opts.MasterURL, opts.KubeConfig, opts.Cache, handlerFuncs) + } + if opts.MessageDispatcher == nil { + opts.MessageDispatcher = NewMessageDispatcher() + } + if opts.MessageReceiver == nil { + opts.MessageReceiver = NewMessageReceiver(func(channelRef ChannelReference, message *Message) error { + return b.receiveMessage(channelRef, message) + }) + } + + 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. +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..b263d140745 --- /dev/null +++ b/pkg/buses/cache.go @@ -0,0 +1,98 @@ +/* + * 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) { + 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) +} + +// 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 new file mode 100644 index 00000000000..e6c38c8663e --- /dev/null +++ b/pkg/buses/cache_test.go @@ -0,0 +1,149 @@ +/* +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 TestCacheErrsForUnknownChannel(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 TestCacheRetrievesKnownChannel(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 TestCacheRemovesKnownChannel(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 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 + 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 TestCacheRetrievesKnownSubscription(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 TestCacheRemovesKnownSubscription(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 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{ + 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..98b9007bf02 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 once the bus has configurable params, 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 c0d42bc39a8..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: %v", err) - } - }() - 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 44ae72ab9f5..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: %v", err) + 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..ef3a8840e3e --- /dev/null +++ b/pkg/buses/handler_funcs.go @@ -0,0 +1,255 @@ +/* + * 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(busRef BusReference) 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 { + 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 { + 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 +} + +func (h EventHandlerFuncs) onUnprovision(channel *channelsv1alpha1.Channel, reconciler *Reconciler) error { + 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 { + 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 +} + +func (h EventHandlerFuncs) onUnsubscribe(subscription *channelsv1alpha1.Subscription, reconciler *Reconciler) error { + 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 +} + +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..6c69bfe9d88 --- /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.NewKafkaBusDispatcher(busRef, brokers, opts) + if err != nil { + glog.Fatalf("Error starting kafka bus dispatcher: %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 b3516f4b2a5..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: %v", err) - } - - provisioner, err := NewKafkaProvisioner(name, namespace, *masterURL, *kubeconfig, clusterAdmin) - if err != nil { - glog.Fatalf("Error building kafka provisioner: %v", err) - } - - 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 3776690700e..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: %v", err) - } - - kubeClient, err := kubernetes.NewForConfig(cfg) - if err != nil { - glog.Fatalf("Error building kubernetes clientset: %v", err) - } - - client, err := clientset.NewForConfig(cfg) - if err != nil { - glog.Fatalf("Error building clientset: %v", err) - } - - 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': %v", key, err) - } - // 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..9552dbf1c92 --- /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: %v", err) + } + + kubeClient, err := kubernetesclientset.NewForConfig(cfg) + if err != nil { + glog.Fatalf("Error building kubernetes clientset: %v", err) + } + + eventingClient, err := eventingclientset.NewForConfig(cfg) + if err != nil { + glog.Fatalf("Error building eventing clientset: %v", err) + } + + 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': %v", key, err) + } + // 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..0f4029ed8a3 100644 --- a/pkg/buses/references.go +++ b/pkg/buses/references.go @@ -16,22 +16,95 @@ 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 +} + +// 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{ + 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..91d3f147fa0 --- /dev/null +++ b/pkg/buses/references_test.go @@ -0,0 +1,210 @@ +/* +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 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, + 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..107a210179b --- /dev/null +++ b/pkg/buses/stub/bus.go @@ -0,0 +1,124 @@ +/* + * 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) { + 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), + } + } +} + +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) { + // create or update subscription + 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 7b13484cd72..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: %v", err) - } - }() - 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 f7d84a6411c..88cae16d3b4 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 7732eeff2ce..158059927ef 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 51f05f033b9..8d0fe2cba34 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", + } +} 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",