diff --git a/Gopkg.lock b/Gopkg.lock index 089b0c30ceb..7d18bdc6aa4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1492,6 +1492,7 @@ "github.com/cloudevents/sdk-go/pkg/cloudevents", "github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http", "github.com/cloudevents/sdk-go/pkg/cloudevents/types", + "github.com/ghodss/yaml", "github.com/google/go-cmp/cmp", "github.com/google/go-cmp/cmp/cmpopts", "github.com/google/uuid", diff --git a/cmd/controller/main.go b/cmd/controller/main.go index dba2fde4b77..40c6898a912 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -24,6 +24,7 @@ import ( "github.com/knative/eventing/pkg/reconciler/broker" "github.com/knative/eventing/pkg/reconciler/channel" + "github.com/knative/eventing/pkg/reconciler/eventingchannel" "github.com/knative/eventing/pkg/reconciler/eventtype" "github.com/knative/eventing/pkg/reconciler/namespace" "github.com/knative/eventing/pkg/reconciler/sequence" @@ -35,6 +36,7 @@ func main() { sharedmain.Main("controller", subscription.NewController, namespace.NewController, + eventingchannel.NewController, channel.NewController, trigger.NewController, broker.NewController, diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 89e1608f30c..9d58c069c00 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -20,9 +20,11 @@ import ( "log" "github.com/knative/eventing/pkg/channeldefaulter" + "github.com/knative/eventing/pkg/defaultchannel" "go.uber.org/zap" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" "github.com/knative/eventing/pkg/logconfig" @@ -78,6 +80,12 @@ func main() { eventingv1alpha1.ChannelDefaulterSingleton = channelDefaulter configMapWatcher.Watch(channeldefaulter.ConfigMapName, channelDefaulter.UpdateConfigMap) + // Watch the default-ch-webhook ConfigMap and dynamically update the default + // Channel CRD. + chDefaulter := defaultchannel.New(logger.Desugar()) + eventingduckv1alpha1.ChannelDefaulterSingleton = chDefaulter + configMapWatcher.Watch(defaultchannel.ConfigMapName, chDefaulter.UpdateConfigMap) + if err = configMapWatcher.Start(stopCh); err != nil { logger.Fatalf("failed to start webhook configmap watcher: %v", err) } @@ -104,6 +112,7 @@ func main() { // For group messaging.knative.dev. messagingv1alpha1.SchemeGroupVersion.WithKind("InMemoryChannel"): &messagingv1alpha1.InMemoryChannel{}, messagingv1alpha1.SchemeGroupVersion.WithKind("Sequence"): &messagingv1alpha1.Sequence{}, + messagingv1alpha1.SchemeGroupVersion.WithKind("Channel"): &messagingv1alpha1.Channel{}, }, Logger: logger, } diff --git a/config/200-controller-clusterrole.yaml b/config/200-controller-clusterrole.yaml index 72a5a69ddfd..d08a231b870 100644 --- a/config/200-controller-clusterrole.yaml +++ b/config/200-controller-clusterrole.yaml @@ -84,6 +84,8 @@ rules: resources: - "sequences" - "sequences/status" + - "channels" + - "channels/status" verbs: *everything # Messaging resources and finalizers we care about. diff --git a/config/300-channel-eventing.yaml b/config/300-channel-eventing.yaml new file mode 100644 index 00000000000..00a97f304ba --- /dev/null +++ b/config/300-channel-eventing.yaml @@ -0,0 +1,116 @@ +# Copyright 2019 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO This is the provisioner-based Channel object, which is being deprecated in 0.8 and removed in 0.9. +# See https://github.com/knative/eventing/issues/1561 + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: channels.eventing.knative.dev + labels: + eventing.knative.dev/release: devel + knative.dev/crd-install: "true" +spec: + group: eventing.knative.dev + versions: + - name: v1alpha1 + served: true + storage: true + names: + kind: Channel + plural: channels + singular: channel + categories: + - all + - knative + - eventing + shortNames: + - chan + scope: Namespaced + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].status" + - name: Reason + type: string + JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason" + - name: Age + type: date + JSONPath: .metadata.creationTimestamp + validation: + openAPIV3Schema: + properties: + spec: + properties: + provisioner: + type: object + required: + - apiVersion + - kind + - name + properties: + apiVersion: + type: string + minLength: 1 + kind: + type: string + minLength: 1 + name: + type: string + minLength: 1 + arguments: + type: object + subscribable: + type: object + properties: + subscribers: + type: array + items: + required: + - uid + properties: + ref: + type: object + required: + - namespace + - name + - uid + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + minLength: 1 + namespace: + type: string + minLength: 1 + uid: + type: string + minLength: 1 + generation: + type: integer + uid: + type: string + minLength: 1 + subscriberURI: + type: string + minLength: 1 + replyURI: + type: string + minLength: 1 diff --git a/config/300-channel.yaml b/config/300-channel.yaml index 9bbcc3ae485..1af4dbf8520 100644 --- a/config/300-channel.yaml +++ b/config/300-channel.yaml @@ -1,4 +1,4 @@ -# Copyright 2018 The Knative Authors +# Copyright 2019 The Knative Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,29 +11,32 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: channels.eventing.knative.dev + name: channels.messaging.knative.dev labels: eventing.knative.dev/release: devel knative.dev/crd-install: "true" + messaging.knative.dev/subscribable: "true" spec: - group: eventing.knative.dev + group: messaging.knative.dev versions: - - name: v1alpha1 - served: true - storage: true + - name: v1alpha1 + served: true + storage: true names: kind: Channel plural: channels singular: channel categories: - - all - - knative - - eventing + - all + - knative + - messaging + - channel shortNames: - - chan + - ch scope: Namespaced subresources: status: {} @@ -44,6 +47,9 @@ spec: - name: Reason type: string JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason" + - name: URL + type: string + JSONPath: .status.address.url - name: Age type: date JSONPath: .metadata.creationTimestamp @@ -52,12 +58,8 @@ spec: properties: spec: properties: - provisioner: + channelTemplate: type: object - required: - - apiVersion - - kind - - name properties: apiVersion: type: string @@ -65,11 +67,11 @@ spec: kind: type: string minLength: 1 - name: - type: string - minLength: 1 - arguments: - type: object + spec: + type: object + required: + - apiVersion + - kind subscribable: type: object properties: @@ -99,8 +101,6 @@ spec: uid: type: string minLength: 1 - generation: - type: integer uid: type: string minLength: 1 diff --git a/config/400-default-ch-config.yaml b/config/400-default-ch-config.yaml new file mode 100644 index 00000000000..34bb2d2035e --- /dev/null +++ b/config/400-default-ch-config.yaml @@ -0,0 +1,29 @@ +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: default-ch-webhook + namespace: knative-eventing +data: + # Configuration for defaulting channels that do not specify CRD implementations. + default-ch-config: | + clusterDefault: + apiVersion: messaging.knative.dev/v1alpha1 + kind: InMemoryChannel + namespaceDefaults: + some-namespace: + apiVersion: messaging.knative.dev/v1alpha1 + kind: InMemoryChannel diff --git a/contrib/gcppubsub/pkg/controller/channel/reconcile.go b/contrib/gcppubsub/pkg/controller/channel/reconcile.go index b40770a47ee..d3f14ba1704 100644 --- a/contrib/gcppubsub/pkg/controller/channel/reconcile.go +++ b/contrib/gcppubsub/pkg/controller/channel/reconcile.go @@ -62,7 +62,7 @@ const ( subscriptionSyncFailed = "SubscriptionSyncFailed" subscriptionDeleteFailed = "SubscriptionDeleteFailed" - deprecatedMessage = "The `gcp-pubsub` ClusterChannelProvisioner is deprecated and will be removed in 0.8. Recommended replacement is using `Channel` CRD from https://github.com/GoogleCloudPlatform/cloud-run-events." + deprecatedMessage = "The `gcp-pubsub` ClusterChannelProvisioner is deprecated and will be removed in 0.9. Recommended replacement is using `Channel` CRD from https://github.com/GoogleCloudPlatform/cloud-run-events." ) // reconciler reconciles GCP-PubSub Channels by creating the K8s Service (ExternalName) diff --git a/contrib/kafka/pkg/controller/channel/reconcile.go b/contrib/kafka/pkg/controller/channel/reconcile.go index 31f585ff515..5d461557c94 100644 --- a/contrib/kafka/pkg/controller/channel/reconcile.go +++ b/contrib/kafka/pkg/controller/channel/reconcile.go @@ -45,7 +45,7 @@ const ( dispatcherReconcileFailed = "DispatcherReconcileFailed" dispatcherUpdateStatusFailed = "DispatcherUpdateStatusFailed" - deprecatedMessage = "The `kafka` ClusterChannelProvisioner is deprecated and will be removed in 0.8. Recommended replacement is using `KafkaChannel` CRD." + deprecatedMessage = "The `kafka` ClusterChannelProvisioner is deprecated and will be removed in 0.9. Recommended replacement is using `KafkaChannel` CRD." ) type channelArgs struct { diff --git a/contrib/natss/pkg/controller/channel/reconcile.go b/contrib/natss/pkg/controller/channel/reconcile.go index ee7e02d1b63..4c48b09e2fe 100644 --- a/contrib/natss/pkg/controller/channel/reconcile.go +++ b/contrib/natss/pkg/controller/channel/reconcile.go @@ -34,7 +34,7 @@ import ( ) const ( - deprecatedMessage = "The `natss` ClusterChannelProvisioner is deprecated and will be removed in 0.8. Recommended replacement is using `NatssChannel` CRD." + deprecatedMessage = "The `natss` ClusterChannelProvisioner is deprecated and will be removed in 0.9. Recommended replacement is using `NatssChannel` CRD." ) type reconciler struct { diff --git a/pkg/apis/duck/v1alpha1/channel_defaulter.go b/pkg/apis/duck/v1alpha1/channel_defaulter.go new file mode 100644 index 00000000000..d11c262a405 --- /dev/null +++ b/pkg/apis/duck/v1alpha1/channel_defaulter.go @@ -0,0 +1,37 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +// TODO This should be placed in channel_defaults.go within messaging once Subscription is moved to messaging. +// Context: there is a cyclic dependency between eventing and messaging if we place this in messaging. Broker needs to +// depend on this, which is fine. But the problem arises due to messaging depending on eventing, mainly on +// Subscription-related objects for the Sequence type. We should first move Subscription down to messaging and then we +// can move this down. See https://github.com/knative/eventing/issues/1562. +// + +// ChannelDefaulter sets the default Channel CRD and Arguments on Channels that do not +// specify any implementation. +type ChannelDefaulter interface { + // GetDefault determines the default Channel CRD for the given namespace. + GetDefault(namespace string) *ChannelTemplateSpec +} + +var ( + // ChannelDefaulterSingleton is the global singleton used to default Channels that do not + // specify a Channel CRD. + ChannelDefaulterSingleton ChannelDefaulter +) diff --git a/pkg/apis/duck/v1alpha1/channel_template_types.go b/pkg/apis/duck/v1alpha1/channel_template_types.go new file mode 100644 index 00000000000..819127d7048 --- /dev/null +++ b/pkg/apis/duck/v1alpha1/channel_template_types.go @@ -0,0 +1,45 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ChannelTemplateSpec struct { + metav1.TypeMeta `json:",inline"` + + // Spec defines the Spec to use for each channel created. Passed + // in verbatim to the Channel CRD as Spec section. + // +optional + Spec *runtime.RawExtension `json:"spec,omitempty"` +} + +// ChannelTemplateSpecInternal is an internal only version that includes ObjectMeta so that +// we can easily create new Channels off of it. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ChannelTemplateSpecInternal struct { + metav1.TypeMeta `json:",inline"` + + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the Spec to use for each channel created. Passed + // in verbatim to the Channel CRD as Spec section. + // +optional + Spec *runtime.RawExtension `json:"spec,omitempty"` +} diff --git a/pkg/apis/duck/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/duck/v1alpha1/zz_generated.deepcopy.go index 4867db71b5c..1cf97201ca4 100644 --- a/pkg/apis/duck/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/duck/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,67 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelTemplateSpec) DeepCopyInto(out *ChannelTemplateSpec) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpec. +func (in *ChannelTemplateSpec) DeepCopy() *ChannelTemplateSpec { + if in == nil { + return nil + } + out := new(ChannelTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ChannelTemplateSpec) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelTemplateSpecInternal) DeepCopyInto(out *ChannelTemplateSpecInternal) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpecInternal. +func (in *ChannelTemplateSpecInternal) DeepCopy() *ChannelTemplateSpecInternal { + if in == nil { + return nil + } + out := new(ChannelTemplateSpecInternal) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ChannelTemplateSpecInternal) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Channelable) DeepCopyInto(out *Channelable) { *out = *in diff --git a/pkg/apis/eventing/v1alpha1/broker_defaults.go b/pkg/apis/eventing/v1alpha1/broker_defaults.go index 3c6c0726e03..3f190baeaf2 100644 --- a/pkg/apis/eventing/v1alpha1/broker_defaults.go +++ b/pkg/apis/eventing/v1alpha1/broker_defaults.go @@ -18,9 +18,21 @@ package v1alpha1 import ( "context" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" ) func (b *Broker) SetDefaults(ctx context.Context) { + // If we are not using the deprecated channelTemplate and haven't configured the new one, + // then set the default channel to the new channelTemplate. + // This check is done to avoid having both the deprecated and the new channelTemplate set, which will cause + // validation problems. + if b != nil && b.Spec.DeprecatedChannelTemplate == nil && b.Spec.ChannelTemplate == nil { + // The singleton may not have been set, if so ignore it and validation will reject the Broker. + if cd := eventingduckv1alpha1.ChannelDefaulterSingleton; cd != nil { + channelTemplate := cd.GetDefault(b.Namespace) + b.Spec.ChannelTemplate = channelTemplate + } + } b.Spec.SetDefaults(ctx) setUserInfoAnnotations(b, ctx) } diff --git a/pkg/apis/eventing/v1alpha1/broker_defaults_test.go b/pkg/apis/eventing/v1alpha1/broker_defaults_test.go index f8dc1302150..8a4a5cb61e8 100644 --- a/pkg/apis/eventing/v1alpha1/broker_defaults_test.go +++ b/pkg/apis/eventing/v1alpha1/broker_defaults_test.go @@ -18,11 +18,105 @@ package v1alpha1 import ( "context" + "github.com/google/go-cmp/cmp" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" "testing" ) -// No-op test because method does nothing. -func TestBrokerDefaults(t *testing.T) { - b := Broker{} - b.SetDefaults(context.TODO()) +var ( + defaultChannelTemplate = &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "InMemoryChannel", + }, + } +) + +func TestBrokerSetDefaults(t *testing.T) { + testCases := map[string]struct { + nilChannelDefaulter bool + channelTemplate *eventingduckv1alpha1.ChannelTemplateSpec + initial Broker + expected Broker + }{ + "nil ChannelDefaulter": { + nilChannelDefaulter: true, + expected: Broker{}, + }, + "unset ChannelDefaulter": { + expected: Broker{}, + }, + "set ChannelDefaulter": { + channelTemplate: defaultChannelTemplate, + expected: Broker{ + Spec: BrokerSpec{ + ChannelTemplate: defaultChannelTemplate, + }, + }, + }, + "deprecated template already set": { + channelTemplate: defaultChannelTemplate, + initial: Broker{ + Spec: BrokerSpec{ + DeprecatedChannelTemplate: &ChannelSpec{ + Provisioner: &corev1.ObjectReference{Kind: "mykind", APIVersion: "mapiversion"}, + }, + }, + }, + expected: Broker{ + Spec: BrokerSpec{ + DeprecatedChannelTemplate: &ChannelSpec{ + Provisioner: &corev1.ObjectReference{Kind: "mykind", APIVersion: "mapiversion"}, + }, + }, + }, + }, + "template already specified": { + channelTemplate: defaultChannelTemplate, + initial: Broker{ + Spec: BrokerSpec{ + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "OtherChannel", + }, + }, + }, + }, + expected: Broker{ + Spec: BrokerSpec{ + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "OtherChannel", + }, + }, + }, + }, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + if !tc.nilChannelDefaulter { + eventingduckv1alpha1.ChannelDefaulterSingleton = &brokerChannelDefaulter{ + channelTemplate: tc.channelTemplate, + } + defer func() { eventingduckv1alpha1.ChannelDefaulterSingleton = nil }() + } + tc.initial.SetDefaults(context.TODO()) + if diff := cmp.Diff(tc.expected, tc.initial); diff != "" { + t.Fatalf("Unexpected defaults (-want, +got): %s", diff) + } + }) + } +} + +type brokerChannelDefaulter struct { + channelTemplate *eventingduckv1alpha1.ChannelTemplateSpec +} + +func (cd *brokerChannelDefaulter) GetDefault(_ string) *eventingduckv1alpha1.ChannelTemplateSpec { + return cd.channelTemplate } diff --git a/pkg/apis/eventing/v1alpha1/broker_types.go b/pkg/apis/eventing/v1alpha1/broker_types.go index 8ef08030dff..224b2971a82 100644 --- a/pkg/apis/eventing/v1alpha1/broker_types.go +++ b/pkg/apis/eventing/v1alpha1/broker_types.go @@ -17,6 +17,7 @@ package v1alpha1 import ( + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -62,43 +63,19 @@ var ( _ kmeta.OwnerRefable = (*Broker)(nil) ) -// This should be duck so that Broker can also use this -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ChannelTemplateSpec struct { - metav1.TypeMeta `json:",inline"` - - // Spec defines the Spec to use for each channel created. Passed - // in verbatim to the Channel CRD as Spec section. - // +optional - Spec runtime.RawExtension `json:"spec"` -} - -// Internal version of ChannelTemplateSpec that includes ObjectMeta so that -// we can easily create new Channels off of it. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ChannelTemplateSpecInternal struct { - metav1.TypeMeta `json:",inline"` - - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Spec defines the Spec to use for each channel created. Passed - // in verbatim to the Channel CRD as Spec section. - // +optional - Spec runtime.RawExtension `json:"spec"` -} - type BrokerSpec struct { // DeprecatedChannelTemplate, if specified will be used to create all the Channels used internally by the // Broker. Only Provisioner and Arguments may be specified. If left unspecified, the default - // Channel for the namespace will be used. + // Channel CRD for the namespace will be used using the channelTemplateSpec attribute. // // +optional DeprecatedChannelTemplate *ChannelSpec `json:"channelTemplate,omitempty"` // ChannelTemplate specifies which Channel CRD to use to create all the Channels used internally by the - // Broker. - ChannelTemplate ChannelTemplateSpec `json:"channelTemplateSpec"` + // Broker. If left unspecified, it is set to the default Channel CRD for the namespace (or cluster, in case there + // are no defaults for the namespace). + // +optional + ChannelTemplate *eventingduckv1alpha1.ChannelTemplateSpec `json:"channelTemplateSpec,omitempty"` } // BrokerStatus represents the current state of a Broker. diff --git a/pkg/apis/eventing/v1alpha1/broker_validation.go b/pkg/apis/eventing/v1alpha1/broker_validation.go index a56ca993081..2dfcf7a5b8e 100644 --- a/pkg/apis/eventing/v1alpha1/broker_validation.go +++ b/pkg/apis/eventing/v1alpha1/broker_validation.go @@ -19,7 +19,7 @@ package v1alpha1 import ( "context" - "k8s.io/apimachinery/pkg/api/equality" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" "knative.dev/pkg/apis" ) @@ -30,16 +30,18 @@ func (b *Broker) Validate(ctx context.Context) *apis.FieldError { func (bs *BrokerSpec) Validate(ctx context.Context) *apis.FieldError { var errs *apis.FieldError - if bs.DeprecatedChannelTemplate != nil && !equality.Semantic.DeepEqual(bs.ChannelTemplate, ChannelTemplateSpec{}) { + if bs.DeprecatedChannelTemplate != nil && bs.ChannelTemplate != nil { errs = errs.Also(apis.ErrMultipleOneOf("channelTemplate", "channelTemplateSpec")) return errs } - if dcte := isValidDeprecatedChannelTemplate(bs.DeprecatedChannelTemplate); dcte != nil { - errs = errs.Also(dcte.ViaField("channelTemplate")) - } - - if !equality.Semantic.DeepEqual(bs.ChannelTemplate, ChannelTemplateSpec{}) { + if bs.ChannelTemplate == nil { + // If the new channelTemplate is nil, validate the DeprecatedChannelTemplate. + if dcte := isValidDeprecatedChannelTemplate(bs.DeprecatedChannelTemplate); dcte != nil { + errs = errs.Also(dcte.ViaField("channelTemplate")) + } + } else { + // Validate the new channelTemplate. if cte := isValidChannelTemplate(bs.ChannelTemplate); cte != nil { errs = errs.Also(cte.ViaField("channelTemplateSpec")) } @@ -64,7 +66,7 @@ func isValidDeprecatedChannelTemplate(dct *ChannelSpec) *apis.FieldError { return errs } -func isValidChannelTemplate(dct ChannelTemplateSpec) *apis.FieldError { +func isValidChannelTemplate(dct *eventingduckv1alpha1.ChannelTemplateSpec) *apis.FieldError { var errs *apis.FieldError if dct.Kind == "" { errs = errs.Also(apis.ErrMissingField("kind")) diff --git a/pkg/apis/eventing/v1alpha1/broker_validation_test.go b/pkg/apis/eventing/v1alpha1/broker_validation_test.go index bc1a2944f44..c7b5401f6c4 100644 --- a/pkg/apis/eventing/v1alpha1/broker_validation_test.go +++ b/pkg/apis/eventing/v1alpha1/broker_validation_test.go @@ -22,6 +22,7 @@ import ( "github.com/google/go-cmp/cmp" eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" @@ -101,7 +102,7 @@ func TestValidSpec(t *testing.T) { DeprecatedChannelTemplate: &ChannelSpec{ Provisioner: &corev1.ObjectReference{}, }, - ChannelTemplate: ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind"}}, + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind"}}, }, want: func() *apis.FieldError { var errs *apis.FieldError @@ -112,7 +113,7 @@ func TestValidSpec(t *testing.T) { }, { name: "invalid templatespec, missing kind", spec: BrokerSpec{ - ChannelTemplate: ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{APIVersion: "myapiversion"}}, + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{APIVersion: "myapiversion"}}, }, want: func() *apis.FieldError { var errs *apis.FieldError @@ -123,7 +124,7 @@ func TestValidSpec(t *testing.T) { }, { name: "invalid templatespec, missing apiVersion", spec: BrokerSpec{ - ChannelTemplate: ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind"}}, + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind"}}, }, want: func() *apis.FieldError { var errs *apis.FieldError @@ -134,7 +135,7 @@ func TestValidSpec(t *testing.T) { }, { name: "valid templatespec", spec: BrokerSpec{ - ChannelTemplate: ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind", APIVersion: "myapiversion"}}, + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind", APIVersion: "myapiversion"}}, }, want: nil, }} diff --git a/pkg/apis/eventing/v1alpha1/test_helper.go b/pkg/apis/eventing/v1alpha1/test_helper.go index 4f7b0006858..98b830c2050 100644 --- a/pkg/apis/eventing/v1alpha1/test_helper.go +++ b/pkg/apis/eventing/v1alpha1/test_helper.go @@ -89,7 +89,7 @@ func (t testHelper) ReadyBrokerStatus() *BrokerStatus { func (t testHelper) ReadyBrokerStatusDeprecated() *BrokerStatus { bs := &BrokerStatus{} - bs.MarkDeprecated("ClusterChannelProvisionerDeprecated", "Provisioners are deprecated and will be removed in 0.8. Recommended replacement is CRD based channels using spec.channelTemplateSpec.") + bs.MarkDeprecated("ClusterChannelProvisionerDeprecated", "Provisioners are deprecated and will be removed in 0.9. Recommended replacement is CRD based channels using spec.channelTemplateSpec.") bs.PropagateIngressDeploymentAvailability(t.AvailableDeployment()) bs.PropagateIngressChannelReadiness(t.ReadyChannelStatus()) bs.PropagateTriggerChannelReadiness(t.ReadyChannelStatus()) diff --git a/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go index c79033073fd..205bda4d156 100644 --- a/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/eventing/v1alpha1/zz_generated.deepcopy.go @@ -95,7 +95,11 @@ func (in *BrokerSpec) DeepCopyInto(out *BrokerSpec) { *out = new(ChannelSpec) (*in).DeepCopyInto(*out) } - in.ChannelTemplate.DeepCopyInto(&out.ChannelTemplate) + if in.ChannelTemplate != nil { + in, out := &in.ChannelTemplate, &out.ChannelTemplate + *out = new(duckv1alpha1.ChannelTemplateSpec) + (*in).DeepCopyInto(*out) + } return } @@ -253,59 +257,6 @@ func (in *ChannelStatus) DeepCopy() *ChannelStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChannelTemplateSpec) DeepCopyInto(out *ChannelTemplateSpec) { - *out = *in - out.TypeMeta = in.TypeMeta - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpec. -func (in *ChannelTemplateSpec) DeepCopy() *ChannelTemplateSpec { - if in == nil { - return nil - } - out := new(ChannelTemplateSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ChannelTemplateSpec) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChannelTemplateSpecInternal) DeepCopyInto(out *ChannelTemplateSpecInternal) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpecInternal. -func (in *ChannelTemplateSpecInternal) DeepCopy() *ChannelTemplateSpecInternal { - if in == nil { - return nil - } - out := new(ChannelTemplateSpecInternal) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ChannelTemplateSpecInternal) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterChannelProvisioner) DeepCopyInto(out *ClusterChannelProvisioner) { *out = *in diff --git a/pkg/apis/messaging/v1alpha1/channel_defaults.go b/pkg/apis/messaging/v1alpha1/channel_defaults.go new file mode 100644 index 00000000000..883d2919c53 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_defaults.go @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" +) + +func (c *Channel) SetDefaults(ctx context.Context) { + if c != nil && c.Spec.ChannelTemplate == nil { + // The singleton may not have been set, if so ignore it and validation will reject the + // Channel. + if cd := eventingduckv1alpha1.ChannelDefaulterSingleton; cd != nil { + channelTemplate := cd.GetDefault(c.Namespace) + c.Spec.ChannelTemplate = channelTemplate + } + } + c.Spec.SetDefaults(ctx) +} + +func (cs *ChannelSpec) SetDefaults(ctx context.Context) {} diff --git a/pkg/apis/messaging/v1alpha1/channel_defaults_test.go b/pkg/apis/messaging/v1alpha1/channel_defaults_test.go new file mode 100644 index 00000000000..4494fe1a4d7 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_defaults_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + + "github.com/google/go-cmp/cmp" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" +) + +var ( + defaultChannelTemplate = &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "InMemoryChannel", + }, + } +) + +func TestChannelSetDefaults(t *testing.T) { + testCases := map[string]struct { + nilChannelDefaulter bool + channelTemplate *eventingduckv1alpha1.ChannelTemplateSpec + initial Channel + expected Channel + }{ + "nil ChannelDefaulter": { + nilChannelDefaulter: true, + expected: Channel{}, + }, + "unset ChannelDefaulter": { + expected: Channel{}, + }, + "set ChannelDefaulter": { + channelTemplate: defaultChannelTemplate, + expected: Channel{ + Spec: ChannelSpec{ + ChannelTemplate: defaultChannelTemplate, + }, + }, + }, + "template already specified": { + channelTemplate: defaultChannelTemplate, + initial: Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "OtherChannel", + }, + }, + }, + }, + expected: Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "OtherChannel", + }, + }, + }, + }, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + if !tc.nilChannelDefaulter { + eventingduckv1alpha1.ChannelDefaulterSingleton = &channelDefaulter{ + channelTemplate: tc.channelTemplate, + } + defer func() { eventingduckv1alpha1.ChannelDefaulterSingleton = nil }() + } + tc.initial.SetDefaults(context.TODO()) + if diff := cmp.Diff(tc.expected, tc.initial); diff != "" { + t.Fatalf("Unexpected defaults (-want, +got): %s", diff) + } + }) + } +} + +type channelDefaulter struct { + channelTemplate *eventingduckv1alpha1.ChannelTemplateSpec +} + +func (cd *channelDefaulter) GetDefault(_ string) *eventingduckv1alpha1.ChannelTemplateSpec { + return cd.channelTemplate +} diff --git a/pkg/apis/messaging/v1alpha1/channel_lifecycle.go b/pkg/apis/messaging/v1alpha1/channel_lifecycle.go new file mode 100644 index 00000000000..5a026183ca1 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_lifecycle.go @@ -0,0 +1,92 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v1alpha1 + +import ( + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" + "knative.dev/pkg/apis/duck/v1alpha1" +) + +var chCondSet = apis.NewLivingConditionSet(ChannelConditionBackingChannelReady, ChannelConditionAddressable) + +const ( + // ChannelConditionReady has status True when all subconditions below have been set to True. + ChannelConditionReady = apis.ConditionReady + + // ChannelConditionBackingChannelReady has status True when the backing Channel CRD is ready. + ChannelConditionBackingChannelReady apis.ConditionType = "BackingChannelReady" + + // ChannelConditionAddressable has status true when this Channel meets + // the Addressable contract and has a non-empty hostname. + ChannelConditionAddressable apis.ConditionType = "Addressable" +) + +// GetCondition returns the condition currently associated with the given type, or nil. +func (cs *ChannelStatus) GetCondition(t apis.ConditionType) *apis.Condition { + return chCondSet.Manage(cs).GetCondition(t) +} + +// IsReady returns true if the resource is ready overall. +func (cs *ChannelStatus) IsReady() bool { + return chCondSet.Manage(cs).IsHappy() +} + +// InitializeConditions sets relevant unset conditions to Unknown state. +func (cs *ChannelStatus) InitializeConditions() { + chCondSet.Manage(cs).InitializeConditions() +} + +func (cs *ChannelStatus) SetAddress(address *v1alpha1.Addressable) { + if cs.Address == nil { + cs.Address = &v1alpha1.Addressable{} + } + if address != nil && address.URL != nil { + cs.Address.Hostname = address.URL.Host + cs.Address.URL = address.URL + chCondSet.Manage(cs).MarkTrue(ChannelConditionAddressable) + } else { + cs.Address.Hostname = "" + cs.Address.URL = nil + chCondSet.Manage(cs).MarkFalse(ChannelConditionAddressable, "EmptyHostname", "hostname is the empty string") + } +} + +func (cs *ChannelStatus) MarkBackingChannelFailed(reason, messageFormat string, messageA ...interface{}) { + chCondSet.Manage(cs).MarkFalse(ChannelConditionBackingChannelReady, reason, messageFormat, messageA...) +} + +func (cs *ChannelStatus) MarkBackingChannelReady() { + chCondSet.Manage(cs).MarkTrue(ChannelConditionBackingChannelReady) +} + +func (cs *ChannelStatus) PropagateStatuses(chs *eventingduck.ChannelableStatus) { + // TODO: Once you can get a Ready status from Channelable in a generic way, use it here. + readyCondition := chs.Status.GetCondition(apis.ConditionReady) + if readyCondition != nil { + if readyCondition.Status != corev1.ConditionTrue { + cs.MarkBackingChannelFailed(readyCondition.Reason, readyCondition.Message) + } else { + cs.MarkBackingChannelReady() + } + } + // Set the address and update the Addressable conditions. + cs.SetAddress(chs.AddressStatus.Address) + // Set the subscribable status. + cs.SubscribableStatus = chs.SubscribableStatus +} diff --git a/pkg/apis/messaging/v1alpha1/channel_lifecycle_test.go b/pkg/apis/messaging/v1alpha1/channel_lifecycle_test.go new file mode 100644 index 00000000000..b4776aef0ac --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_lifecycle_test.go @@ -0,0 +1,391 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" + duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" +) + +func TestChannelGetCondition(t *testing.T) { + tests := []struct { + name string + cs *ChannelStatus + condQuery apis.ConditionType + want *apis.Condition + }{{ + name: "single condition", + cs: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{ + condReady, + }, + }, + }, + condQuery: apis.ConditionReady, + want: &condReady, + }, { + name: "unknown condition", + cs: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{ + condReady, + }, + }, + }, + condQuery: apis.ConditionType("foo"), + want: nil, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.cs.GetCondition(test.condQuery) + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("unexpected condition (-want, +got) = %v", diff) + } + }) + } +} + +func TestChannelInitializeConditions(t *testing.T) { + tests := []struct { + name string + cs *ChannelStatus + want *ChannelStatus + }{{ + name: "empty", + cs: &ChannelStatus{}, + want: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: ChannelConditionAddressable, + Status: corev1.ConditionUnknown, + }, { + Type: ChannelConditionBackingChannelReady, + Status: corev1.ConditionUnknown, + }, { + Type: ChannelConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, + }, { + name: "one false", + cs: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: ChannelConditionAddressable, + Status: corev1.ConditionFalse, + }}, + }, + }, + want: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: ChannelConditionAddressable, + Status: corev1.ConditionFalse, + }, { + Type: ChannelConditionBackingChannelReady, + Status: corev1.ConditionUnknown, + }, { + Type: ChannelConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, + }, { + name: "one true", + cs: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: ChannelConditionBackingChannelReady, + Status: corev1.ConditionTrue, + }}, + }, + }, + want: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: ChannelConditionAddressable, + Status: corev1.ConditionUnknown, + }, { + Type: ChannelConditionBackingChannelReady, + Status: corev1.ConditionTrue, + }, { + Type: ChannelConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.cs.InitializeConditions() + ignore := cmpopts.IgnoreFields( + apis.Condition{}, + "LastTransitionTime", "Message", "Reason", "Severity") + if diff := cmp.Diff(test.want, test.cs, ignore); diff != "" { + t.Errorf("unexpected conditions (-want, +got) = %v", diff) + } + }) + } +} + +func TestChannelIsReady(t *testing.T) { + tests := []struct { + name string + setAddress bool + markBackingChannelReady bool + wantReady bool + }{{ + name: "all happy", + setAddress: true, + markBackingChannelReady: true, + wantReady: true, + }, { + name: "address not set", + setAddress: false, + markBackingChannelReady: true, + wantReady: false, + }, { + name: "backing channel not ready", + setAddress: true, + markBackingChannelReady: false, + wantReady: false, + }} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cs := &ChannelStatus{} + cs.InitializeConditions() + if test.setAddress { + cs.SetAddress(&duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + }) + } + if test.markBackingChannelReady { + cs.MarkBackingChannelReady() + } else { + cs.MarkBackingChannelFailed("ChannelFailure", "testing") + } + got := cs.IsReady() + if test.wantReady != got { + t.Errorf("unexpected readiness: want %v, got %v", test.wantReady, got) + } + }) + } +} + +func TestChannelSetAddressable(t *testing.T) { + testCases := map[string]struct { + address *duckv1alpha1.Addressable + want *ChannelStatus + }{ + "nil url": { + want: &ChannelStatus{ + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{ + { + Type: ChannelConditionAddressable, + Status: corev1.ConditionFalse, + }, + // Note that Ready is here because when the condition is marked False, duck + // automatically sets Ready to false. + { + Type: ChannelConditionReady, + Status: corev1.ConditionFalse, + }, + }, + }, + AddressStatus: duckv1alpha1.AddressStatus{Address: &duckv1alpha1.Addressable{}}, + }, + }, + "has domain": { + address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + want: &ChannelStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{ + { + Type: ChannelConditionAddressable, + Status: corev1.ConditionTrue, + }}, + }, + }, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + cs := &ChannelStatus{} + cs.SetAddress(tc.address) + ignore := cmpopts.IgnoreFields( + apis.Condition{}, + "LastTransitionTime", "Message", "Reason", "Severity") + if diff := cmp.Diff(tc.want, cs, ignore); diff != "" { + t.Errorf("unexpected conditions (-want, +got) = %v", diff) + } + }) + } +} + +func TestChannelPropagateStatuses(t *testing.T) { + testCases := map[string]struct { + channelableStatus *v1alpha1.ChannelableStatus + wantReady bool + }{ + "address set": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + }, + }, + wantReady: false, + }, + "address not set": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{}, + }, + }, + wantReady: false, + }, + "url not set": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{}, + Hostname: "test-domain", + }, + }, + }, + wantReady: false, + }, + "all set": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionReady, + Status: corev1.ConditionTrue, + }}, + }, + }, + wantReady: true, + }, + "backing channel not ready": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionReady, + Status: corev1.ConditionUnknown, + }}, + }, + }, + wantReady: false, + }, + "no condition ready in backing channel": { + channelableStatus: &v1alpha1.ChannelableStatus{ + AddressStatus: duckv1alpha1.AddressStatus{ + Address: &duckv1alpha1.Addressable{ + Addressable: duckv1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: "test-domain", + }, + }, + Hostname: "test-domain", + }, + }, + Status: duckv1beta1.Status{ + Conditions: []apis.Condition{{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }}, + }, + }, + wantReady: false, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + cs := &ChannelStatus{} + cs.PropagateStatuses(tc.channelableStatus) + got := cs.IsReady() + if tc.wantReady != got { + t.Errorf("unexpected readiness: want %v, got %v", tc.wantReady, got) + } + }) + } +} diff --git a/pkg/apis/messaging/v1alpha1/channel_types.go b/pkg/apis/messaging/v1alpha1/channel_types.go new file mode 100644 index 00000000000..2ece25b0ea7 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_types.go @@ -0,0 +1,101 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v1alpha1 + +import ( + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" + duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" + "knative.dev/pkg/webhook" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Channel represents a generic Channel. It is normally used when we want a Channel, but don't need a specific Channel implementation. +type Channel struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of the Channel. + Spec ChannelSpec `json:"spec,omitempty"` + + // Status represents the current state of the Channel. This data may be out of + // date. + // +optional + Status ChannelStatus `json:"status,omitempty"` +} + +// Check that Channel can be validated, can be defaulted, and has immutable fields. +var _ apis.Validatable = (*Channel)(nil) +var _ apis.Defaultable = (*Channel)(nil) +var _ runtime.Object = (*Channel)(nil) +var _ webhook.GenericCRD = (*Channel)(nil) + +// ChannelSpec defines which subscribers have expressed interest in receiving events from this Channel. +// It also defines the ChannelTemplate to use in order to create the CRD Channel backing this Channel. +type ChannelSpec struct { + + // ChannelTemplate specifies which Channel CRD to use to create the CRD Channel backing this Channel. + // This is immutable after creation. Normally this is set by the Channel defaulter, not directly by the user. + ChannelTemplate *eventingduck.ChannelTemplateSpec `json:"channelTemplate"` + + // Channel conforms to Duck type Subscribable. + Subscribable *eventingduck.Subscribable `json:"subscribable,omitempty"` +} + +// ChannelStatus represents the current state of a Channel. +type ChannelStatus struct { + // inherits duck/v1beta1 Status, which currently provides: + // * ObservedGeneration - the 'Generation' of the Service that was last processed by the controller. + // * Conditions - the latest available observations of a resource's current state. + duckv1beta1.Status `json:",inline"` + + // Channel is Addressable. It currently exposes the endpoint as a + // fully-qualified DNS name which will distribute traffic over the + // provided targets from inside the cluster. + // + // It generally has the form {channel}.{namespace}.svc.{cluster domain name} + duckv1alpha1.AddressStatus `json:",inline"` + + // Subscribers is populated with the statuses of each of the Channelable's subscribers. + eventingduck.SubscribableTypeStatus `json:",inline"` + + // Channel is an ObjectReference to the Channel CRD backing this Channel. + Channel *corev1.ObjectReference `json:"channel,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ChannelList is a collection of Channels. +type ChannelList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + Items []Channel `json:"items"` +} + +// GetGroupVersionKind returns GroupVersionKind for Channels. +func (dc *Channel) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind("Channel") +} diff --git a/pkg/apis/messaging/v1alpha1/channel_types_test.go b/pkg/apis/messaging/v1alpha1/channel_types_test.go new file mode 100644 index 00000000000..075b34691fd --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_types_test.go @@ -0,0 +1,27 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v1alpha1 + +import "testing" + +func TestChannel_GetGroupVersionKind(t *testing.T) { + c := Channel{} + gvk := c.GetGroupVersionKind() + if gvk.Kind != "Channel" { + t.Errorf("Should be Channel.") + } +} diff --git a/pkg/apis/messaging/v1alpha1/channel_validation.go b/pkg/apis/messaging/v1alpha1/channel_validation.go new file mode 100644 index 00000000000..22bcd063dc6 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_validation.go @@ -0,0 +1,94 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + "github.com/google/go-cmp/cmp/cmpopts" + "knative.dev/pkg/kmp" + + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "knative.dev/pkg/apis" +) + +func (c *Channel) Validate(ctx context.Context) *apis.FieldError { + return c.Spec.Validate(ctx).ViaField("spec") +} + +func (cs *ChannelSpec) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + + if cs.ChannelTemplate == nil { + // The Channel defaulter is expected to set this, not the users. + errs = errs.Also(apis.ErrMissingField("channelTemplate")) + } else { + if cte := isValidChannelTemplate(cs.ChannelTemplate); cte != nil { + errs = errs.Also(cte.ViaField("channelTemplate")) + } + } + + if cs.Subscribable != nil { + for i, subscriber := range cs.Subscribable.Subscribers { + if subscriber.ReplyURI == "" && subscriber.SubscriberURI == "" { + fe := apis.ErrMissingField("replyURI", "subscriberURI") + fe.Details = "expected at least one of, got none" + errs = errs.Also(fe.ViaField(fmt.Sprintf("subscriber[%d]", i)).ViaField("subscribable")) + } + } + } + + return errs +} + +func isValidChannelTemplate(ct *eventingduck.ChannelTemplateSpec) *apis.FieldError { + var errs *apis.FieldError + if ct.Kind == "" { + errs = errs.Also(apis.ErrMissingField("kind")) + } + if ct.APIVersion == "" { + errs = errs.Also(apis.ErrMissingField("apiVersion")) + } + return errs +} + +func (c *Channel) CheckImmutableFields(ctx context.Context, og apis.Immutable) *apis.FieldError { + if og == nil { + return nil + } + + original, ok := og.(*Channel) + if !ok { + return &apis.FieldError{Message: "The provided original was not a Channel"} + } + + ignoreArguments := cmpopts.IgnoreFields(ChannelSpec{}, "Subscribable") + if diff, err := kmp.ShortDiff(original.Spec, c.Spec, ignoreArguments); err != nil { + return &apis.FieldError{ + Message: "Failed to diff Channel", + Paths: []string{"spec"}, + Details: err.Error(), + } + } else if diff != "" { + return &apis.FieldError{ + Message: "Immutable fields changed (-old +new)", + Paths: []string{"spec"}, + Details: diff, + } + } + return nil +} diff --git a/pkg/apis/messaging/v1alpha1/channel_validation_test.go b/pkg/apis/messaging/v1alpha1/channel_validation_test.go new file mode 100644 index 00000000000..814e47d2645 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/channel_validation_test.go @@ -0,0 +1,285 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "testing" + + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "knative.dev/pkg/apis" +) + +func TestChannelValidation(t *testing.T) { + tests := []CRDTest{{ + name: "empty", + cr: &Channel{ + Spec: ChannelSpec{}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("spec.channelTemplate") + return fe + }(), + }, { + name: "channel template with no kind", + cr: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + }, + }}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("spec.channelTemplate.kind") + return fe + }(), + }, { + name: "channel template with no apiVersion", + cr: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + }, + }}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("spec.channelTemplate.apiVersion") + return fe + }(), + }, { + name: "valid subscribers array", + cr: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + }, + Subscribable: &eventingduck.Subscribable{ + Subscribers: []eventingduck.SubscriberSpec{{ + SubscriberURI: "subscriberendpoint", + ReplyURI: "resultendpoint", + }}, + }}, + }, + want: nil, + }, { + name: "empty subscriber at index 1", + cr: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + }, + Subscribable: &eventingduck.Subscribable{ + Subscribers: []eventingduck.SubscriberSpec{{ + SubscriberURI: "subscriberendpoint", + ReplyURI: "replyendpoint", + }, {}}, + }}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("spec.subscribable.subscriber[1].replyURI", "spec.subscribable.subscriber[1].subscriberURI") + fe.Details = "expected at least one of, got none" + return fe + }(), + }, { + name: "nil channelTemplate and empty subscriber at index 1", + cr: &Channel{ + Spec: ChannelSpec{ + Subscribable: &eventingduck.Subscribable{ + Subscribers: []eventingduck.SubscriberSpec{{ + SubscriberURI: "subscriberendpoint", + ReplyURI: "replyendpoint", + }, {}}, + }}, + }, + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrMissingField("spec.channelTemplate") + errs = errs.Also(fe) + fe = apis.ErrMissingField("spec.subscribable.subscriber[1].replyURI", "spec.subscribable.subscriber[1].subscriberURI") + fe.Details = "expected at least one of, got none" + errs = errs.Also(fe) + return errs + }(), + }, { + name: "2 empty subscribers", + cr: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + }, + Subscribable: &eventingduck.Subscribable{ + Subscribers: []eventingduck.SubscriberSpec{{}, {}}, + }, + }, + }, + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrMissingField("spec.subscribable.subscriber[0].replyURI", "spec.subscribable.subscriber[0].subscriberURI") + fe.Details = "expected at least one of, got none" + errs = errs.Also(fe) + fe = apis.ErrMissingField("spec.subscribable.subscriber[1].replyURI", "spec.subscribable.subscriber[1].subscriberURI") + fe.Details = "expected at least one of, got none" + errs = errs.Also(fe) + return errs + }(), + }} + + doValidateTest(t, tests) +} + +func TestChannelImmutableFields(t *testing.T) { + tests := []struct { + name string + current apis.Immutable + original apis.Immutable + want *apis.FieldError + }{{ + name: "good (no change)", + current: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + Spec: &runtime.RawExtension{ + Raw: []byte(`"foo":"baz"`), + }, + }, + }, + }, + original: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + Spec: &runtime.RawExtension{ + Raw: []byte(`"foo":"baz"`), + }, + }, + }, + }, + want: nil, + }, { + name: "new nil is ok", + current: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + Spec: &runtime.RawExtension{ + Raw: []byte(`"foo":"baz"`), + }, + }, + }, + }, + original: nil, + want: nil, + }, { + name: "bad (channelTemplate change)", + current: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "OtherChannel", + APIVersion: SchemeGroupVersion.String(), + }, + Spec: &runtime.RawExtension{ + Raw: []byte(`"foo":"baz"`), + }, + }, + }, + }, + original: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + Spec: &runtime.RawExtension{ + Raw: []byte(`"foo":"baz"`), + }, + }, + }, + }, + want: &apis.FieldError{ + Message: "Immutable fields changed (-old +new)", + Paths: []string{"spec"}, + Details: `{v1alpha1.ChannelSpec}.ChannelTemplate.TypeMeta.Kind: + -: "InMemoryChannel" + +: "OtherChannel" +`, + }, + }, { + name: "good (subscribable change)", + current: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + }, + Subscribable: &eventingduck.Subscribable{ + Subscribers: []eventingduck.SubscriberSpec{{ + SubscriberURI: "subscriberendpoint", + ReplyURI: "replyendpoint", + }}, + }, + }, + }, + original: &Channel{ + Spec: ChannelSpec{ + ChannelTemplate: &eventingduck.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + Kind: "InMemoryChannel", + APIVersion: SchemeGroupVersion.String(), + }, + }, + }, + }, + want: nil, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.current.CheckImmutableFields(context.TODO(), test.original) + if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" { + t.Errorf("CheckImmutableFields (-want, +got) = %v", diff) + } + }) + } +} diff --git a/pkg/apis/messaging/v1alpha1/in_memory_channel_lifecycle_test.go b/pkg/apis/messaging/v1alpha1/in_memory_channel_lifecycle_test.go index 0dea9475bce..f79b322e237 100644 --- a/pkg/apis/messaging/v1alpha1/in_memory_channel_lifecycle_test.go +++ b/pkg/apis/messaging/v1alpha1/in_memory_channel_lifecycle_test.go @@ -77,7 +77,7 @@ var ignoreAllButTypeAndStatus = cmpopts.IgnoreFields( var ignoreLastTransitionTime = cmpopts.IgnoreFields(apis.Condition{}, "LastTransitionTime") -func TestChannelGetCondition(t *testing.T) { +func TestInMemoryChannelGetCondition(t *testing.T) { tests := []struct { name string cs *InMemoryChannelStatus @@ -117,7 +117,7 @@ func TestChannelGetCondition(t *testing.T) { } } -func TestChannelInitializeConditions(t *testing.T) { +func TestInMemoryChannelInitializeConditions(t *testing.T) { tests := []struct { name string cs *InMemoryChannelStatus @@ -226,7 +226,7 @@ func TestChannelInitializeConditions(t *testing.T) { } } -func TestChannelIsReady(t *testing.T) { +func TestInMemoryChannelIsReady(t *testing.T) { tests := []struct { name string markServiceReady bool diff --git a/pkg/apis/messaging/v1alpha1/in_memory_channel_types_test.go b/pkg/apis/messaging/v1alpha1/in_memory_channel_types_test.go new file mode 100644 index 00000000000..84e5f81e951 --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/in_memory_channel_types_test.go @@ -0,0 +1,27 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v1alpha1 + +import "testing" + +func TestInMemoryChannel_GetGroupVersionKind(t *testing.T) { + imc := InMemoryChannel{} + gvk := imc.GetGroupVersionKind() + if gvk.Kind != "InMemoryChannel" { + t.Errorf("Should be InMemoryChannel.") + } +} diff --git a/pkg/apis/messaging/v1alpha1/in_memory_channel_validation_test.go b/pkg/apis/messaging/v1alpha1/in_memory_channel_validation_test.go index d109565d13c..ee4ea958e2e 100644 --- a/pkg/apis/messaging/v1alpha1/in_memory_channel_validation_test.go +++ b/pkg/apis/messaging/v1alpha1/in_memory_channel_validation_test.go @@ -23,7 +23,7 @@ import ( "knative.dev/pkg/apis" ) -func TestImMemoryChannelValidation(t *testing.T) { +func TestInMemoryChannelValidation(t *testing.T) { tests := []CRDTest{{ name: "empty", cr: &InMemoryChannel{ diff --git a/pkg/apis/messaging/v1alpha1/register.go b/pkg/apis/messaging/v1alpha1/register.go index ec9e1c7ce61..4f44a546034 100644 --- a/pkg/apis/messaging/v1alpha1/register.go +++ b/pkg/apis/messaging/v1alpha1/register.go @@ -49,6 +49,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &InMemoryChannelList{}, &Sequence{}, &SequenceList{}, + &Channel{}, + &ChannelList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/messaging/v1alpha1/sequence_types.go b/pkg/apis/messaging/v1alpha1/sequence_types.go index 41f3dccc87e..6899e9d9a59 100644 --- a/pkg/apis/messaging/v1alpha1/sequence_types.go +++ b/pkg/apis/messaging/v1alpha1/sequence_types.go @@ -17,6 +17,7 @@ package v1alpha1 import ( + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -55,39 +56,13 @@ var _ apis.Defaultable = (*Sequence)(nil) var _ runtime.Object = (*Sequence)(nil) var _ webhook.GenericCRD = (*Sequence)(nil) -// This should be duck so that Broker can also use this -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ChannelTemplateSpec struct { - metav1.TypeMeta `json:",inline"` - - // Spec defines the Spec to use for each channel created. Passed - // in verbatim to the Channel CRD as Spec section. - // +optional - Spec *runtime.RawExtension `json:"spec,omitempty"` -} - -// Internal version of ChannelTemplateSpec that includes ObjectMeta so that -// we can easily create new Channels off of it. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ChannelTemplateSpecInternal struct { - metav1.TypeMeta `json:",inline"` - - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Spec defines the Spec to use for each channel created. Passed - // in verbatim to the Channel CRD as Spec section. - // +optional - Spec *runtime.RawExtension `json:"spec,omitempty"` -} - type SequenceSpec struct { // Steps is the list of Subscribers (processors / functions) that will be called in the order // provided. Steps []eventingv1alpha1.SubscriberSpec `json:"steps"` // ChannelTemplate specifies which Channel CRD to use - ChannelTemplate ChannelTemplateSpec `json:"channelTemplate"` + ChannelTemplate eventingduckv1alpha1.ChannelTemplateSpec `json:"channelTemplate"` // Reply is a Reference to where the result of the last Subscriber gets sent to. // diff --git a/pkg/apis/messaging/v1alpha1/sequence_types_test.go b/pkg/apis/messaging/v1alpha1/sequence_types_test.go new file mode 100644 index 00000000000..ed14923857c --- /dev/null +++ b/pkg/apis/messaging/v1alpha1/sequence_types_test.go @@ -0,0 +1,27 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package v1alpha1 + +import "testing" + +func TestSequence_GetGroupVersionKind(t *testing.T) { + s := Sequence{} + gvk := s.GetGroupVersionKind() + if gvk.Kind != "Sequence" { + t.Errorf("Should be Sequence.") + } +} diff --git a/pkg/apis/messaging/v1alpha1/sequence_validation.go b/pkg/apis/messaging/v1alpha1/sequence_validation.go index 76db1d99ceb..80095758113 100644 --- a/pkg/apis/messaging/v1alpha1/sequence_validation.go +++ b/pkg/apis/messaging/v1alpha1/sequence_validation.go @@ -18,7 +18,7 @@ package v1alpha1 import ( "context" - + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" "knative.dev/pkg/apis" @@ -41,7 +41,7 @@ func (ps *SequenceSpec) Validate(ctx context.Context) *apis.FieldError { } } - if equality.Semantic.DeepEqual(ps.ChannelTemplate, ChannelTemplateSpec{}) { + if equality.Semantic.DeepEqual(ps.ChannelTemplate, eventingduck.ChannelTemplateSpec{}) { errs = errs.Also(apis.ErrMissingField("channelTemplate")) return errs } diff --git a/pkg/apis/messaging/v1alpha1/sequence_validation_test.go b/pkg/apis/messaging/v1alpha1/sequence_validation_test.go index 2d496fa94a2..49fb630710e 100644 --- a/pkg/apis/messaging/v1alpha1/sequence_validation_test.go +++ b/pkg/apis/messaging/v1alpha1/sequence_validation_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -64,12 +65,12 @@ func makeInvalidReply(channelName string) *corev1.ObjectReference { func TestSequenceSpecValidation(t *testing.T) { subscriberURI := "http://example.com" - validChannelTemplate := ChannelTemplateSpec{ - metav1.TypeMeta{ + validChannelTemplate := eventingduck.ChannelTemplateSpec{ + TypeMeta: metav1.TypeMeta{ Kind: "mykind", APIVersion: "myapiversion", }, - &runtime.RawExtension{}, + Spec: &runtime.RawExtension{}, } tests := []struct { name string @@ -103,7 +104,7 @@ func TestSequenceSpecValidation(t *testing.T) { }, { name: "invalid channeltemplatespec missing APIVersion", ts: &SequenceSpec{ - ChannelTemplate: ChannelTemplateSpec{metav1.TypeMeta{Kind: "mykind"}, &runtime.RawExtension{}}, + ChannelTemplate: eventingduck.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{Kind: "mykind"}, Spec: &runtime.RawExtension{}}, Steps: []eventingv1alpha1.SubscriberSpec{{URI: &subscriberURI}}, }, want: func() *apis.FieldError { @@ -113,7 +114,7 @@ func TestSequenceSpecValidation(t *testing.T) { }, { name: "invalid channeltemplatespec missing Kind", ts: &SequenceSpec{ - ChannelTemplate: ChannelTemplateSpec{metav1.TypeMeta{APIVersion: "myapiversion"}, &runtime.RawExtension{}}, + ChannelTemplate: eventingduck.ChannelTemplateSpec{TypeMeta: metav1.TypeMeta{APIVersion: "myapiversion"}, Spec: &runtime.RawExtension{}}, Steps: []eventingv1alpha1.SubscriberSpec{{URI: &subscriberURI}}, }, want: func() *apis.FieldError { diff --git a/pkg/apis/messaging/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/messaging/v1alpha1/zz_generated.deepcopy.go index 7df53c750bf..876ea568f7c 100644 --- a/pkg/apis/messaging/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/messaging/v1alpha1/zz_generated.deepcopy.go @@ -28,29 +28,27 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChannelTemplateSpec) DeepCopyInto(out *ChannelTemplateSpec) { +func (in *Channel) DeepCopyInto(out *Channel) { *out = *in out.TypeMeta = in.TypeMeta - if in.Spec != nil { - in, out := &in.Spec, &out.Spec - *out = new(runtime.RawExtension) - (*in).DeepCopyInto(*out) - } + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpec. -func (in *ChannelTemplateSpec) DeepCopy() *ChannelTemplateSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Channel. +func (in *Channel) DeepCopy() *Channel { if in == nil { return nil } - out := new(ChannelTemplateSpec) + out := new(Channel) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ChannelTemplateSpec) DeepCopyObject() runtime.Object { +func (in *Channel) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -58,36 +56,88 @@ func (in *ChannelTemplateSpec) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ChannelTemplateSpecInternal) DeepCopyInto(out *ChannelTemplateSpecInternal) { +func (in *ChannelList) DeepCopyInto(out *ChannelList) { *out = *in out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.Spec != nil { - in, out := &in.Spec, &out.Spec - *out = new(runtime.RawExtension) - (*in).DeepCopyInto(*out) + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Channel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelTemplateSpecInternal. -func (in *ChannelTemplateSpecInternal) DeepCopy() *ChannelTemplateSpecInternal { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelList. +func (in *ChannelList) DeepCopy() *ChannelList { if in == nil { return nil } - out := new(ChannelTemplateSpecInternal) + out := new(ChannelList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ChannelTemplateSpecInternal) DeepCopyObject() runtime.Object { +func (in *ChannelList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelSpec) DeepCopyInto(out *ChannelSpec) { + *out = *in + if in.ChannelTemplate != nil { + in, out := &in.ChannelTemplate, &out.ChannelTemplate + *out = new(duckv1alpha1.ChannelTemplateSpec) + (*in).DeepCopyInto(*out) + } + if in.Subscribable != nil { + in, out := &in.Subscribable, &out.Subscribable + *out = new(duckv1alpha1.Subscribable) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelSpec. +func (in *ChannelSpec) DeepCopy() *ChannelSpec { + if in == nil { + return nil + } + out := new(ChannelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChannelStatus) DeepCopyInto(out *ChannelStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + in.AddressStatus.DeepCopyInto(&out.AddressStatus) + in.SubscribableTypeStatus.DeepCopyInto(&out.SubscribableTypeStatus) + if in.Channel != nil { + in, out := &in.Channel, &out.Channel + *out = new(v1.ObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChannelStatus. +func (in *ChannelStatus) DeepCopy() *ChannelStatus { + if in == nil { + return nil + } + out := new(ChannelStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InMemoryChannel) DeepCopyInto(out *InMemoryChannel) { *out = *in diff --git a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/channel.go b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/channel.go new file mode 100644 index 00000000000..55341f51721 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/channel.go @@ -0,0 +1,174 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + scheme "github.com/knative/eventing/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ChannelsGetter has a method to return a ChannelInterface. +// A group's client should implement this interface. +type ChannelsGetter interface { + Channels(namespace string) ChannelInterface +} + +// ChannelInterface has methods to work with Channel resources. +type ChannelInterface interface { + Create(*v1alpha1.Channel) (*v1alpha1.Channel, error) + Update(*v1alpha1.Channel) (*v1alpha1.Channel, error) + UpdateStatus(*v1alpha1.Channel) (*v1alpha1.Channel, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.Channel, error) + List(opts v1.ListOptions) (*v1alpha1.ChannelList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Channel, err error) + ChannelExpansion +} + +// channels implements ChannelInterface +type channels struct { + client rest.Interface + ns string +} + +// newChannels returns a Channels +func newChannels(c *MessagingV1alpha1Client, namespace string) *channels { + return &channels{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the channel, and returns the corresponding channel object, and an error if there is any. +func (c *channels) Get(name string, options v1.GetOptions) (result *v1alpha1.Channel, err error) { + result = &v1alpha1.Channel{} + err = c.client.Get(). + Namespace(c.ns). + Resource("channels"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Channels that match those selectors. +func (c *channels) List(opts v1.ListOptions) (result *v1alpha1.ChannelList, err error) { + result = &v1alpha1.ChannelList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("channels"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested channels. +func (c *channels) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("channels"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a channel and creates it. Returns the server's representation of the channel, and an error, if there is any. +func (c *channels) Create(channel *v1alpha1.Channel) (result *v1alpha1.Channel, err error) { + result = &v1alpha1.Channel{} + err = c.client.Post(). + Namespace(c.ns). + Resource("channels"). + Body(channel). + Do(). + Into(result) + return +} + +// Update takes the representation of a channel and updates it. Returns the server's representation of the channel, and an error, if there is any. +func (c *channels) Update(channel *v1alpha1.Channel) (result *v1alpha1.Channel, err error) { + result = &v1alpha1.Channel{} + err = c.client.Put(). + Namespace(c.ns). + Resource("channels"). + Name(channel.Name). + Body(channel). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *channels) UpdateStatus(channel *v1alpha1.Channel) (result *v1alpha1.Channel, err error) { + result = &v1alpha1.Channel{} + err = c.client.Put(). + Namespace(c.ns). + Resource("channels"). + Name(channel.Name). + SubResource("status"). + Body(channel). + Do(). + Into(result) + return +} + +// Delete takes name of the channel and deletes it. Returns an error if one occurs. +func (c *channels) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("channels"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *channels) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("channels"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched channel. +func (c *channels) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Channel, err error) { + result = &v1alpha1.Channel{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("channels"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_channel.go b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_channel.go new file mode 100644 index 00000000000..72813b3a9d1 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_channel.go @@ -0,0 +1,140 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeChannels implements ChannelInterface +type FakeChannels struct { + Fake *FakeMessagingV1alpha1 + ns string +} + +var channelsResource = schema.GroupVersionResource{Group: "messaging.knative.dev", Version: "v1alpha1", Resource: "channels"} + +var channelsKind = schema.GroupVersionKind{Group: "messaging.knative.dev", Version: "v1alpha1", Kind: "Channel"} + +// Get takes name of the channel, and returns the corresponding channel object, and an error if there is any. +func (c *FakeChannels) Get(name string, options v1.GetOptions) (result *v1alpha1.Channel, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(channelsResource, c.ns, name), &v1alpha1.Channel{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Channel), err +} + +// List takes label and field selectors, and returns the list of Channels that match those selectors. +func (c *FakeChannels) List(opts v1.ListOptions) (result *v1alpha1.ChannelList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(channelsResource, channelsKind, c.ns, opts), &v1alpha1.ChannelList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ChannelList{ListMeta: obj.(*v1alpha1.ChannelList).ListMeta} + for _, item := range obj.(*v1alpha1.ChannelList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested channels. +func (c *FakeChannels) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(channelsResource, c.ns, opts)) + +} + +// Create takes the representation of a channel and creates it. Returns the server's representation of the channel, and an error, if there is any. +func (c *FakeChannels) Create(channel *v1alpha1.Channel) (result *v1alpha1.Channel, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(channelsResource, c.ns, channel), &v1alpha1.Channel{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Channel), err +} + +// Update takes the representation of a channel and updates it. Returns the server's representation of the channel, and an error, if there is any. +func (c *FakeChannels) Update(channel *v1alpha1.Channel) (result *v1alpha1.Channel, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(channelsResource, c.ns, channel), &v1alpha1.Channel{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Channel), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeChannels) UpdateStatus(channel *v1alpha1.Channel) (*v1alpha1.Channel, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(channelsResource, "status", c.ns, channel), &v1alpha1.Channel{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Channel), err +} + +// Delete takes name of the channel and deletes it. Returns an error if one occurs. +func (c *FakeChannels) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(channelsResource, c.ns, name), &v1alpha1.Channel{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeChannels) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(channelsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.ChannelList{}) + return err +} + +// Patch applies the patch and returns the patched channel. +func (c *FakeChannels) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Channel, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(channelsResource, c.ns, name, data, subresources...), &v1alpha1.Channel{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Channel), err +} diff --git a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_messaging_client.go b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_messaging_client.go index f0317d0cfa1..90a70c27b96 100644 --- a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_messaging_client.go +++ b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/fake/fake_messaging_client.go @@ -28,6 +28,10 @@ type FakeMessagingV1alpha1 struct { *testing.Fake } +func (c *FakeMessagingV1alpha1) Channels(namespace string) v1alpha1.ChannelInterface { + return &FakeChannels{c, namespace} +} + func (c *FakeMessagingV1alpha1) InMemoryChannels(namespace string) v1alpha1.InMemoryChannelInterface { return &FakeInMemoryChannels{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/generated_expansion.go index 107a069c119..c3eadee3c40 100644 --- a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/generated_expansion.go @@ -18,6 +18,8 @@ limitations under the License. package v1alpha1 +type ChannelExpansion interface{} + type InMemoryChannelExpansion interface{} type SequenceExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/messaging_client.go b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/messaging_client.go index 762dcbf29e3..6ede09a4b2b 100644 --- a/pkg/client/clientset/versioned/typed/messaging/v1alpha1/messaging_client.go +++ b/pkg/client/clientset/versioned/typed/messaging/v1alpha1/messaging_client.go @@ -27,6 +27,7 @@ import ( type MessagingV1alpha1Interface interface { RESTClient() rest.Interface + ChannelsGetter InMemoryChannelsGetter SequencesGetter } @@ -36,6 +37,10 @@ type MessagingV1alpha1Client struct { restClient rest.Interface } +func (c *MessagingV1alpha1Client) Channels(namespace string) ChannelInterface { + return newChannels(c, namespace) +} + func (c *MessagingV1alpha1Client) InMemoryChannels(namespace string) InMemoryChannelInterface { return newInMemoryChannels(c, namespace) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 003f68c8e3f..ad007df212f 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -69,6 +69,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Eventing().V1alpha1().Triggers().Informer()}, nil // Group=messaging.knative.dev, Version=v1alpha1 + case messagingv1alpha1.SchemeGroupVersion.WithResource("channels"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Messaging().V1alpha1().Channels().Informer()}, nil case messagingv1alpha1.SchemeGroupVersion.WithResource("inmemorychannels"): return &genericInformer{resource: resource.GroupResource(), informer: f.Messaging().V1alpha1().InMemoryChannels().Informer()}, nil case messagingv1alpha1.SchemeGroupVersion.WithResource("sequences"): diff --git a/pkg/client/informers/externalversions/messaging/v1alpha1/channel.go b/pkg/client/informers/externalversions/messaging/v1alpha1/channel.go new file mode 100644 index 00000000000..3fb450a747c --- /dev/null +++ b/pkg/client/informers/externalversions/messaging/v1alpha1/channel.go @@ -0,0 +1,89 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + versioned "github.com/knative/eventing/pkg/client/clientset/versioned" + internalinterfaces "github.com/knative/eventing/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/knative/eventing/pkg/client/listers/messaging/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ChannelInformer provides access to a shared informer and lister for +// Channels. +type ChannelInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ChannelLister +} + +type channelInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewChannelInformer constructs a new informer for Channel type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewChannelInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredChannelInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredChannelInformer constructs a new informer for Channel type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredChannelInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.MessagingV1alpha1().Channels(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.MessagingV1alpha1().Channels(namespace).Watch(options) + }, + }, + &messagingv1alpha1.Channel{}, + resyncPeriod, + indexers, + ) +} + +func (f *channelInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredChannelInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *channelInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&messagingv1alpha1.Channel{}, f.defaultInformer) +} + +func (f *channelInformer) Lister() v1alpha1.ChannelLister { + return v1alpha1.NewChannelLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/messaging/v1alpha1/interface.go b/pkg/client/informers/externalversions/messaging/v1alpha1/interface.go index 4e66545178e..6a61f938e30 100644 --- a/pkg/client/informers/externalversions/messaging/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/messaging/v1alpha1/interface.go @@ -24,6 +24,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Channels returns a ChannelInformer. + Channels() ChannelInformer // InMemoryChannels returns a InMemoryChannelInformer. InMemoryChannels() InMemoryChannelInformer // Sequences returns a SequenceInformer. @@ -41,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Channels returns a ChannelInformer. +func (v *version) Channels() ChannelInformer { + return &channelInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // InMemoryChannels returns a InMemoryChannelInformer. func (v *version) InMemoryChannels() InMemoryChannelInformer { return &inMemoryChannelInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/injection/informers/messaging/v1alpha1/channel/channel.go b/pkg/client/injection/informers/messaging/v1alpha1/channel/channel.go new file mode 100644 index 00000000000..5e720aedfe6 --- /dev/null +++ b/pkg/client/injection/informers/messaging/v1alpha1/channel/channel.go @@ -0,0 +1,52 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package channel + +import ( + "context" + + v1alpha1 "github.com/knative/eventing/pkg/client/informers/externalversions/messaging/v1alpha1" + factory "github.com/knative/eventing/pkg/client/injection/informers/messaging/factory" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterInformer(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct{} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := factory.Get(ctx) + inf := f.Messaging().V1alpha1().Channels() + return context.WithValue(ctx, Key{}, inf), inf.Informer() +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) v1alpha1.ChannelInformer { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Fatalf( + "Unable to fetch %T from context.", (v1alpha1.ChannelInformer)(nil)) + } + return untyped.(v1alpha1.ChannelInformer) +} diff --git a/pkg/client/injection/informers/messaging/v1alpha1/channel/fake/fake.go b/pkg/client/injection/informers/messaging/v1alpha1/channel/fake/fake.go new file mode 100644 index 00000000000..d21e31ba8a7 --- /dev/null +++ b/pkg/client/injection/informers/messaging/v1alpha1/channel/fake/fake.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + fake "github.com/knative/eventing/pkg/client/injection/informers/messaging/factory/fake" + channel "github.com/knative/eventing/pkg/client/injection/informers/messaging/v1alpha1/channel" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" +) + +var Get = channel.Get + +func init() { + injection.Fake.RegisterInformer(withInformer) +} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := fake.Get(ctx) + inf := f.Messaging().V1alpha1().Channels() + return context.WithValue(ctx, channel.Key{}, inf), inf.Informer() +} diff --git a/pkg/client/listers/messaging/v1alpha1/channel.go b/pkg/client/listers/messaging/v1alpha1/channel.go new file mode 100644 index 00000000000..a900780ef87 --- /dev/null +++ b/pkg/client/listers/messaging/v1alpha1/channel.go @@ -0,0 +1,94 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ChannelLister helps list Channels. +type ChannelLister interface { + // List lists all Channels in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.Channel, err error) + // Channels returns an object that can list and get Channels. + Channels(namespace string) ChannelNamespaceLister + ChannelListerExpansion +} + +// channelLister implements the ChannelLister interface. +type channelLister struct { + indexer cache.Indexer +} + +// NewChannelLister returns a new ChannelLister. +func NewChannelLister(indexer cache.Indexer) ChannelLister { + return &channelLister{indexer: indexer} +} + +// List lists all Channels in the indexer. +func (s *channelLister) List(selector labels.Selector) (ret []*v1alpha1.Channel, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Channel)) + }) + return ret, err +} + +// Channels returns an object that can list and get Channels. +func (s *channelLister) Channels(namespace string) ChannelNamespaceLister { + return channelNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ChannelNamespaceLister helps list and get Channels. +type ChannelNamespaceLister interface { + // List lists all Channels in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.Channel, err error) + // Get retrieves the Channel from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.Channel, error) + ChannelNamespaceListerExpansion +} + +// channelNamespaceLister implements the ChannelNamespaceLister +// interface. +type channelNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Channels in the indexer for a given namespace. +func (s channelNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Channel, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Channel)) + }) + return ret, err +} + +// Get retrieves the Channel from the indexer for a given namespace and name. +func (s channelNamespaceLister) Get(name string) (*v1alpha1.Channel, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("channel"), name) + } + return obj.(*v1alpha1.Channel), nil +} diff --git a/pkg/client/listers/messaging/v1alpha1/expansion_generated.go b/pkg/client/listers/messaging/v1alpha1/expansion_generated.go index e19acc3311b..2d242f716bb 100644 --- a/pkg/client/listers/messaging/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/messaging/v1alpha1/expansion_generated.go @@ -18,6 +18,14 @@ limitations under the License. package v1alpha1 +// ChannelListerExpansion allows custom methods to be added to +// ChannelLister. +type ChannelListerExpansion interface{} + +// ChannelNamespaceListerExpansion allows custom methods to be added to +// ChannelNamespaceLister. +type ChannelNamespaceListerExpansion interface{} + // InMemoryChannelListerExpansion allows custom methods to be added to // InMemoryChannelLister. type InMemoryChannelListerExpansion interface{} diff --git a/pkg/defaultchannel/channel_defaulter.go b/pkg/defaultchannel/channel_defaulter.go new file mode 100644 index 00000000000..c84f3e0d611 --- /dev/null +++ b/pkg/defaultchannel/channel_defaulter.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package defaultchannel + +import ( + "encoding/json" + "github.com/ghodss/yaml" + "sync/atomic" + + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" +) + +const ( + // ConfigMapName is the name of the ConfigMap that contains the configuration for the default + // channel CRD. + ConfigMapName = "default-ch-webhook" + + // channelDefaulterKey is the key in the ConfigMap to get the name of the default + // Channel CRD. + channelDefaulterKey = "default-ch-config" +) + +// Configuration is the data structure serialized to YAML in the config map. When a Channel needs to be +// defaulted, the Channel's namespace will be used as a key into NamespaceDefaults, if there is +// something present, then that is used. If not, then the ClusterDefault is used. +type Config struct { + // NamespaceDefaultChannels are the default Channels CRDs for each namespace. namespace is the + // key, the value is the default ChannelTemplate to use. + NamespaceDefaults map[string]*eventingduckv1alpha1.ChannelTemplateSpec `json:"namespaceDefaults,omitempty"` + // ClusterDefaultChannel is the default Channel CRD for all namespaces that are not in + // NamespaceDefaultChannels. + ClusterDefault *eventingduckv1alpha1.ChannelTemplateSpec `json:"clusterDefault,omitempty"` +} + +// ChannelDefaulter adds a default Channel CRD to Channels that do not have any +// CRD specified. The default is stored in a ConfigMap and can be updated at runtime. +type ChannelDefaulter struct { + // The current default Channel CRD to set. This should only be accessed via + // getConfig() and setConfig(), as they correctly enforce the type we require (*Config). + config atomic.Value + logger *zap.Logger +} + +var _ eventingduckv1alpha1.ChannelDefaulter = &ChannelDefaulter{} + +// New creates a new ChannelDefaulter. The caller is expected to set this as the global singleton. +// +// channelDefaulter := channeldefaulter.New(logger) +// messagingv1alpha1.ChannelDefaulterSingleton = channelDefaulter +// configMapWatcher.Watch(channelDefaulter.ConfigMapName, channelDefaulter.UpdateConfigMap) +func New(logger *zap.Logger) *ChannelDefaulter { + return &ChannelDefaulter{ + logger: logger.With(zap.String("role", "channelDefaulter")), + } +} + +// UpdateConfigMap reads in a ConfigMap and updates the internal default Channel CRD to use. +func (cd *ChannelDefaulter) UpdateConfigMap(cm *corev1.ConfigMap) { + if cm == nil { + cd.logger.Info("UpdateConfigMap on a nil map") + return + } + defaultChannelConfig, present := cm.Data[channelDefaulterKey] + if !present { + cd.logger.Info("ConfigMap is missing key", zap.String("key", channelDefaulterKey), zap.Any("configMap", cm)) + return + } + + if defaultChannelConfig == "" { + cd.logger.Info("ConfigMap's value was the empty string, ignoring it.", zap.Any("configMap", cm)) + return + } + + defaultChannelConfigJson, err := yaml.YAMLToJSON([]byte(defaultChannelConfig)) + if err != nil { + cd.logger.Error("ConfigMap's value could not be converted to JSON.", zap.Error(err), zap.String("defaultChannelConfig", defaultChannelConfig)) + return + } + + config := &Config{} + if err := json.Unmarshal([]byte(defaultChannelConfigJson), config); err != nil { + cd.logger.Error("ConfigMap's value could not be unmarshaled.", zap.Error(err), zap.Any("configMap", cm)) + return + } + + cd.logger.Info("Updated channelDefaulter config", zap.Any("config", config)) + cd.setConfig(config) +} + +// setConfig is a typed wrapper around config. +func (cd *ChannelDefaulter) setConfig(config *Config) { + cd.config.Store(config) +} + +// getConfig is a typed wrapper around config. +func (cd *ChannelDefaulter) getConfig() *Config { + if config, ok := cd.config.Load().(*Config); ok { + return config + } + return nil +} + +// GetDefault determines the default Channel CRD and arguments for the provided namespace. If there is no default +// for the provided namespace, then use the cluster default. +func (cd *ChannelDefaulter) GetDefault(namespace string) *eventingduckv1alpha1.ChannelTemplateSpec { + // Because we are treating this as a singleton, be tolerant to it having not been setup at all. + if cd == nil { + return nil + } + config := cd.getConfig() + if config == nil { + return nil + } + channelTemplate := getDefaultChannelTemplate(config, namespace) + cd.logger.Debug("Defaulting the Channel", zap.Any("defaultChannelTemplate", channelTemplate)) + return channelTemplate +} + +func getDefaultChannelTemplate(config *Config, namespace string) *eventingduckv1alpha1.ChannelTemplateSpec { + if template, ok := config.NamespaceDefaults[namespace]; ok { + return template + } + return config.ClusterDefault +} diff --git a/pkg/defaultchannel/channel_defaulter_test.go b/pkg/defaultchannel/channel_defaulter_test.go new file mode 100644 index 00000000000..e72d1af6259 --- /dev/null +++ b/pkg/defaultchannel/channel_defaulter_test.go @@ -0,0 +1,201 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package defaultchannel + +import ( + "testing" + + "encoding/json" + "github.com/ghodss/yaml" + "github.com/google/go-cmp/cmp" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + config = &Config{ + ClusterDefault: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: messagingv1alpha1.SchemeGroupVersion.String(), + Kind: "InMemoryChannel", + }, + }, + } + // configYaml is the YAML form of config. It is generated in init(). + configYaml string + + configWithNamespace = &Config{ + ClusterDefault: &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: v1.TypeMeta{ + APIVersion: messagingv1alpha1.SchemeGroupVersion.String(), + Kind: "InMemoryChannel", + }, + }, + NamespaceDefaults: map[string]*eventingduckv1alpha1.ChannelTemplateSpec{ + "testNamespace": { + TypeMeta: v1.TypeMeta{ + APIVersion: messagingv1alpha1.SchemeGroupVersion.String(), + Kind: "OtherChannel", + }, + }, + }, + } +) + +func init() { + configJsonBytes, _ := json.Marshal(config) + configYamlBytes, _ := yaml.JSONToYAML(configJsonBytes) + configYaml = string(configYamlBytes) +} + +func TestChannelDefaulter_GetDefault(t *testing.T) { + testCases := map[string]struct { + config *Config + channel *messagingv1alpha1.Channel + expectedChannelTemplate *eventingduckv1alpha1.ChannelTemplateSpec + }{ + "no default set": { + channel: &messagingv1alpha1.Channel{}, + expectedChannelTemplate: nil, + }, + "cluster defaulted": { + config: config, + channel: &messagingv1alpha1.Channel{}, + expectedChannelTemplate: config.ClusterDefault, + }, + "namespace defaulted": { + config: configWithNamespace, + channel: &messagingv1alpha1.Channel{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "testNamespace", + }, + }, + expectedChannelTemplate: configWithNamespace.NamespaceDefaults["testNamespace"], + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + cd := New(zap.NewNop()) + if tc.config != nil { + cd.setConfig(tc.config) + } + channelTemplate := cd.GetDefault(tc.channel.Namespace) + if diff := cmp.Diff(tc.expectedChannelTemplate, channelTemplate); diff != "" { + t.Fatalf("Unexpected provisioner (-want, +got): %s", diff) + } + }) + } +} + +func TestChannelDefaulter_UpdateConfigMap(t *testing.T) { + testCases := map[string]struct { + initialConfig *corev1.ConfigMap + expectedAfterInitial *eventingduckv1alpha1.ChannelTemplateSpec + updatedConfig *corev1.ConfigMap + expectedAfterUpdate *eventingduckv1alpha1.ChannelTemplateSpec + }{ + "nil config map": { + expectedAfterInitial: nil, + expectedAfterUpdate: nil, + }, + "key missing in update": { + initialConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterInitial: config.ClusterDefault, + updatedConfig: &corev1.ConfigMap{}, + expectedAfterUpdate: config.ClusterDefault, + }, + "bad yaml is ignored": { + initialConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterInitial: config.ClusterDefault, + updatedConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: "foo -> bar", + }, + }, + expectedAfterUpdate: config.ClusterDefault, + }, + "empty config is accepted": { + initialConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterInitial: config.ClusterDefault, + updatedConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: "{}", + }, + }, + expectedAfterUpdate: nil, + }, + "empty string is ignored": { + initialConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterInitial: config.ClusterDefault, + updatedConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: "", + }, + }, + expectedAfterUpdate: config.ClusterDefault, + }, + "update to same channel": { + initialConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterInitial: config.ClusterDefault, + updatedConfig: &corev1.ConfigMap{ + Data: map[string]string{ + channelDefaulterKey: configYaml, + }, + }, + expectedAfterUpdate: config.ClusterDefault, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + cd := New(zap.NewNop()) + cd.UpdateConfigMap(tc.initialConfig) + + channelTemplate := cd.GetDefault("testNamespace") + if diff := cmp.Diff(tc.expectedAfterInitial, channelTemplate); diff != "" { + t.Fatalf("Unexpected difference after initial configMap update (-want, +got): %s", diff) + } + cd.UpdateConfigMap(tc.updatedConfig) + channelTemplate = cd.GetDefault("testNamespace") + if diff := cmp.Diff(tc.expectedAfterUpdate, channelTemplate); diff != "" { + t.Fatalf("Unexpected difference after update configMap update (-want, +got): %s", diff) + } + }) + } +} diff --git a/pkg/provisioners/inmemory/channel/reconcile.go b/pkg/provisioners/inmemory/channel/reconcile.go index 31f74d59b63..c71194f0a6e 100644 --- a/pkg/provisioners/inmemory/channel/reconcile.go +++ b/pkg/provisioners/inmemory/channel/reconcile.go @@ -40,7 +40,7 @@ const ( channelUpdateStatusFailed = "ChannelUpdateStatusFailed" k8sServiceCreateFailed = "K8sServiceCreateFailed" - deprecatedMessage = "The `in-memory` ClusterChannelProvisioner is deprecated and will be removed in 0.8. Recommended replacement is using `InMemoryChannel` CRD." + deprecatedMessage = "The `in-memory` ClusterChannelProvisioner is deprecated and will be removed in 0.9. Recommended replacement is using `InMemoryChannel` CRD." ) type reconciler struct { diff --git a/pkg/reconciler/broker/broker.go b/pkg/reconciler/broker/broker.go index 6d4cfd1bd74..6f726385e41 100644 --- a/pkg/reconciler/broker/broker.go +++ b/pkg/reconciler/broker/broker.go @@ -59,7 +59,7 @@ const ( brokerUpdateStatusFailed = "BrokerUpdateStatusFailed" ingressSubscriptionDeleteFailed = "IngressSubscriptionDeleteFailed" ingressSubscriptionCreateFailed = "IngressSubscriptionCreateFailed" - deprecatedMessage = "Provisioners are deprecated and will be removed in 0.8. Recommended replacement is CRD based channels using spec.channelTemplateSpec." + deprecatedMessage = "Provisioners are deprecated and will be removed in 0.9. Recommended replacement is CRD based channels using spec.channelTemplateSpec." ) type Reconciler struct { @@ -137,7 +137,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, key string) error { func (r *Reconciler) reconcile(ctx context.Context, b *v1alpha1.Broker) error { logging.FromContext(ctx).Debug("Reconciling", zap.Any("Broker", b)) - if b.Spec.ChannelTemplate.Kind != "" && b.Spec.ChannelTemplate.APIVersion != "" { + if b.Spec.ChannelTemplate != nil { return r.reconcileCRD(ctx, b) } else { return r.reconcileLegacy(ctx, b) diff --git a/pkg/reconciler/broker/broker_test.go b/pkg/reconciler/broker/broker_test.go index d0b6386c7b7..53ef216c880 100644 --- a/pkg/reconciler/broker/broker_test.go +++ b/pkg/reconciler/broker/broker_test.go @@ -1855,7 +1855,6 @@ func createChannelCRD(namespace string, t channelType, ready bool) *unstructured }, "labels": labels, }, - "spec": nil, }, } } diff --git a/pkg/reconciler/broker/resources/channel.go b/pkg/reconciler/broker/resources/channel.go index db7323256cd..f5a8ec14945 100644 --- a/pkg/reconciler/broker/resources/channel.go +++ b/pkg/reconciler/broker/resources/channel.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "knative.dev/pkg/kmeta" + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" v1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -37,12 +38,12 @@ func BrokerChannelName(brokerName, channelType string) string { // for a given Broker. func NewChannel(channelType string, b *v1alpha1.Broker, l map[string]string) (*unstructured.Unstructured, error) { // Set the name of the resource we're creating as well as the namespace, etc. - template := v1alpha1.ChannelTemplateSpecInternal{ - metav1.TypeMeta{ + template := eventingduck.ChannelTemplateSpecInternal{ + TypeMeta: metav1.TypeMeta{ Kind: b.Spec.ChannelTemplate.Kind, APIVersion: b.Spec.ChannelTemplate.APIVersion, }, - metav1.ObjectMeta{ + ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{ *kmeta.NewControllerRef(b), }, @@ -50,7 +51,7 @@ func NewChannel(channelType string, b *v1alpha1.Broker, l map[string]string) (*u Namespace: b.Namespace, Labels: l, }, - b.Spec.ChannelTemplate.Spec, + Spec: b.Spec.ChannelTemplate.Spec, } raw, err := json.Marshal(template) if err != nil { diff --git a/pkg/reconciler/channel/channel.go b/pkg/reconciler/channel/channel.go index 1c285f02b58..da0d5dab143 100644 --- a/pkg/reconciler/channel/channel.go +++ b/pkg/reconciler/channel/channel.go @@ -19,15 +19,26 @@ package channel import ( "context" "fmt" + duckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + eventingduck "github.com/knative/eventing/pkg/duck" + "github.com/knative/eventing/pkg/reconciler/channel/resources" + "github.com/knative/eventing/pkg/utils" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "knative.dev/pkg/apis/duck" + duckapis "knative.dev/pkg/apis/duck" "reflect" "time" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" + duckroot "knative.dev/pkg/apis" - "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" - listers "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + listers "github.com/knative/eventing/pkg/client/listers/messaging/v1alpha1" "github.com/knative/eventing/pkg/logging" "github.com/knative/eventing/pkg/reconciler" "go.uber.org/zap" @@ -37,6 +48,7 @@ import ( const ( channelReadinessChanged = "ChannelReadinessChanged" channelReconciled = "ChannelReconciled" + channelReconcileError = "ChannelReconcileError" channelUpdateStatusFailed = "ChannelUpdateStatusFailed" ) @@ -44,14 +56,13 @@ type Reconciler struct { *reconciler.Base // listers index properties about resources - channelLister listers.ChannelLister + channelLister listers.ChannelLister + resourceTracker eventingduck.ResourceTracker } // Check that our Reconciler implements controller.Reconciler var _ controller.Reconciler = (*Reconciler)(nil) -// Reconcile will check if the channel is being watched by provisioner's channel controller -// This will improve UX. See https://github.com/knative/eventing/issues/779 func (r *Reconciler) Reconcile(ctx context.Context, key string) error { // Convert the namespace/name string into a distinct namespace and name namespace, name, err := cache.SplitMetaNamespaceKey(key) @@ -83,6 +94,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, key string) error { reconcileErr := r.reconcile(ctx, channel) if reconcileErr != nil { logging.FromContext(ctx).Error("Error reconciling Channel", zap.Error(reconcileErr)) + r.Recorder.Eventf(channel, corev1.EventTypeWarning, channelReconcileError, fmt.Sprintf("Channel reconcile error: %v", reconcileErr)) } else { logging.FromContext(ctx).Debug("Successfully reconciled Channel") r.Recorder.Eventf(channel, corev1.EventTypeNormal, channelReconciled, "Channel reconciled: %s", key) @@ -98,28 +110,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, key string) error { return reconcileErr } -func (r *Reconciler) reconcile(ctx context.Context, ch *v1alpha1.Channel) error { - // Do not Initialize() Status in channel-default-controller. It will set ChannelConditionProvisionerInstalled=True - // Directly call GetCondition(). If the Status was never initialized then GetCondition() will return nil and - // IsUnknown() will return true - c := ch.Status.GetCondition(v1alpha1.ChannelConditionProvisionerInstalled) +func (r *Reconciler) reconcile(ctx context.Context, c *v1alpha1.Channel) error { + c.Status.InitializeConditions() - if c == nil || c.IsUnknown() { + // 1. Create the backing Channel CRD, if it doesn't exist. + // 2. Patch the subscriptions from this Channel into the backing Channel CRD. + // 3. Propagate the backing Channel CRD Status, Address, and SubscribableStatus into this Channel. - var proName string - var proKind string - if ch.Spec.Provisioner != nil { - proName = ch.Spec.Provisioner.Name - proKind = ch.Spec.Provisioner.Kind - } + if c.DeletionTimestamp != nil { + // Everything is cleaned up by the garbage collector. + return nil + } + + channelResourceInterface := r.DynamicClientSet.Resource(duckroot.KindToResource(c.Spec.ChannelTemplate.GetObjectKind().GroupVersionKind())).Namespace(c.Namespace) + + // Tell tracker to reconcile this Channel whenever the backing Channel CRD changes. + track := r.resourceTracker.TrackInNamespace(c) + + backingChannel, err := r.reconcileBackingChannel(ctx, channelResourceInterface, c) + if err != nil { + c.Status.MarkBackingChannelFailed("ChannelFailure", "%v", err) + return fmt.Errorf("problem reconciling the backing channel: %v", err) + } + + // Start tracking the backing Channel CRD... + if err = track(utils.ObjectRef(backingChannel, backingChannel.GroupVersionKind())); err != nil { + return fmt.Errorf("unable to track changes to the backing channel: %v", err) + } - ch.Status.MarkProvisionerNotInstalled( - "Provisioner not found.", - "Specified provisioner [Name:%s Kind:%s] is not installed or not controlling the channel.", - proName, - proKind, - ) + c.Status.Channel = &corev1.ObjectReference{ + Kind: backingChannel.Kind, + APIVersion: backingChannel.APIVersion, + Name: backingChannel.Name, + Namespace: backingChannel.Namespace, } + + err = r.patchBackingChannelSubscriptions(ctx, channelResourceInterface, c, backingChannel) + if err != nil { + c.Status.MarkBackingChannelFailed("ChannelFailure", "%v", err) + return fmt.Errorf("problem patching subscriptions in the backing channel: %v", err) + } + + c.Status.PropagateStatuses(&backingChannel.Status) return nil } @@ -140,7 +172,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, desired *v1alpha1.Channel existing := channel.DeepCopy() existing.Status = desired.Status - c, err := r.EventingClientSet.EventingV1alpha1().Channels(desired.Namespace).UpdateStatus(existing) + c, err := r.EventingClientSet.MessagingV1alpha1().Channels(desired.Namespace).UpdateStatus(existing) if err == nil && becomesReady { duration := time.Since(c.ObjectMeta.CreationTimestamp.Time) @@ -153,3 +185,61 @@ func (r *Reconciler) updateStatus(ctx context.Context, desired *v1alpha1.Channel return c, err } + +// reconcileBackingChannel reconciles Channel's 'c' underlying CRD channel. +func (r *Reconciler) reconcileBackingChannel(ctx context.Context, resourceClient dynamic.ResourceInterface, c *v1alpha1.Channel) (*duckv1alpha1.Channelable, error) { + channel, err := resourceClient.Get(c.Name, metav1.GetOptions{}) + channelable := &duckv1alpha1.Channelable{} + + // If the resource doesn't exist, we'll create it + if err != nil { + if apierrs.IsNotFound(err) { + newChannel, err := resources.NewChannel(c) + if err != nil { + logging.FromContext(ctx).Error("Failed to create Channel from ChannelTemplate", zap.Any("channelTemplate", c.Spec.ChannelTemplate), zap.Error(err)) + return nil, err + } + channel, err = resourceClient.Create(newChannel, metav1.CreateOptions{}) + if err != nil { + logging.FromContext(ctx).Error("Failed to create Channel", zap.Any("channel", newChannel), zap.Error(err)) + return nil, err + } + logging.FromContext(ctx).Info("Created Channel", zap.Any("channel", newChannel)) + } else { + logging.FromContext(ctx).Error(fmt.Sprintf("Failed to get Channel: %s/%s", c.Namespace, c.Name), zap.Error(err)) + return nil, err + } + } + logging.FromContext(ctx).Debug("Found Channel", zap.Any("channel", c)) + err = duckapis.FromUnstructured(channel, channelable) + if err != nil { + logging.FromContext(ctx).Error("Failed to convert to Channelable Object", zap.Error(err), zap.Any("channel", channel)) + return nil, err + } + return channelable, nil +} + +func (r *Reconciler) patchBackingChannelSubscriptions(ctx context.Context, resourceClient dynamic.ResourceInterface, channel *v1alpha1.Channel, backingChannel *duckv1alpha1.Channelable) error { + if equality.Semantic.DeepEqual(channel.Spec.Subscribable, backingChannel.Spec.Subscribable) { + logging.FromContext(ctx).Debug("Subscribable in sync, no need to patch") + return nil + } + + after := backingChannel.DeepCopy() + after.Spec.Subscribable = channel.Spec.Subscribable + + patch, err := duck.CreateMergePatch(backingChannel, after) + + if err != nil { + logging.FromContext(ctx).Warn("Failed to create mergePatch", zap.Error(err)) + return err + } + + patched, err := resourceClient.Patch(backingChannel.GetName(), types.MergePatchType, patch, metav1.UpdateOptions{}) + if err != nil { + logging.FromContext(ctx).Warn("Failed to patch the Channel", zap.Error(err), zap.Any("patch", patch)) + return err + } + logging.FromContext(ctx).Debug("Patched resource", zap.Any("patched", patched)) + return nil +} diff --git a/pkg/reconciler/channel/channel_test.go b/pkg/reconciler/channel/channel_test.go index fea0deea4c0..fac91e637f4 100644 --- a/pkg/reconciler/channel/channel_test.go +++ b/pkg/reconciler/channel/channel_test.go @@ -18,45 +18,72 @@ package channel import ( "context" + "encoding/json" + "fmt" "testing" - "knative.dev/pkg/configmap" - + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + "github.com/knative/eventing/pkg/reconciler" + . "github.com/knative/eventing/pkg/reconciler/testing" + "github.com/knative/eventing/pkg/utils" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" clientgotesting "k8s.io/client-go/testing" - - eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" - "github.com/knative/eventing/pkg/reconciler" + "knative.dev/pkg/configmap" "knative.dev/pkg/controller" logtesting "knative.dev/pkg/logging/testing" - - . "github.com/knative/eventing/pkg/reconciler/testing" . "knative.dev/pkg/reconciler/testing" ) const ( - testNS = "testnamespace" - channeName = "testchannel" - provisionerName = "testprovisioner" + testNS = "test-namespace" + channelName = "test-channel" ) var ( - provisionerGVK = metav1.GroupVersionKind{ - Group: "eventing.knative.dev", + trueVal = true + + testKey = fmt.Sprintf("%s/%s", testNS, channelName) + + backingChannelHostname = fmt.Sprintf("foo.bar.svc.%s", utils.GetClusterDomainName()) + + channelGVK = metav1.GroupVersionKind{ + Group: "messaging.knative.dev", + Version: "v1alpha1", + Kind: "Channel", + } + + imcGVK = metav1.GroupVersionKind{ + Group: "messaging.knative.dev", Version: "v1alpha1", - Kind: "ClusterChannelProvisioner", + Kind: "InMemoryChannel", } ) func init() { // Add types to scheme - _ = eventingv1alpha1.AddToScheme(scheme.Scheme) + _ = v1alpha1.AddToScheme(scheme.Scheme) +} + +type fakeResourceTracker struct{} + +func (fakeResourceTracker) TrackInNamespace(metav1.Object) func(corev1.ObjectReference) error { + return func(corev1.ObjectReference) error { return nil } +} + +func (fakeResourceTracker) Track(ref corev1.ObjectReference, obj interface{}) error { + return nil } -func TestAllCases(t *testing.T) { +func (fakeResourceTracker) OnChanged(obj interface{}) { +} + +func TestReconcile(t *testing.T) { + table := TableTest{ { Name: "bad workqueue key", @@ -67,93 +94,276 @@ func TestAllCases(t *testing.T) { // Make sure Reconcile handles good keys that don't exist. Key: "foo/not-found", }, - //{ // TODO: there is a bug in the controller, it reconcile for empty provisioner. - // Name: "incomplete channel", - // Objects: []runtime.Object{ - // NewChannel(channelName, testNS), - // }, - // Key: testNS + "/" + channeName, - // WantErr: true, - // WantEvents: []string{ - // Eventf(corev1.EventTypeWarning, "TODO", ""), - //}, { - Name: "unclaimed channel, empty provisioner", + Name: "Channel not found", + Key: testKey, + }, + { + Name: "Channel is being deleted", + Key: testKey, Objects: []runtime.Object{ - NewChannel(channeName, testNS), + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelDeleted), + }, + }, + { + Name: "Backing Channel.Create error", + Key: testKey, + Objects: []runtime.Object{ + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions), + }, + WantCreates: []runtime.Object{ + createChannelCRD(testNS, channelName, false), }, - Key: testNS + "/" + channeName, WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ - Object: NewChannel(channeName, testNS, - WithChannelProvisionerNotFound("", ""), // TODO: THIS IS A BUG, there is no validation checking. - ), + Object: NewMessagingChannel(channelName, testNS, + WithInitMessagingChannelConditions, + WithMessagingChannelTemplate(channelCRD()), + WithBackingChannelFailed("ChannelFailure", "inducing failure for create inmemorychannels")), }}, - WantErr: true, + WithReactors: []clientgotesting.ReactionFunc{ + InduceFailure("create", "inmemorychannels"), + }, WantEvents: []string{ - Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), - Eventf(corev1.EventTypeWarning, "ChannelUpdateStatusFailed", "Failed to update channel status: %s/%s", testNS, channeName), + Eventf(corev1.EventTypeWarning, channelReconcileError, "Channel reconcile error: problem reconciling the backing channel: %v", "inducing failure for create inmemorychannels"), }, - }, { - Name: "unclaimed channel", + WantErr: true, + }, + { + Name: "Backing Channel.Patch Subscriptions failed", + Key: testKey, Objects: []runtime.Object{ - NewChannel(channeName, testNS, - WithChannelProvisioner(provisionerGVK, provisionerName), - ), + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelSubscribers(subscribers())), + NewInMemoryChannel(channelName, testNS, + WithInitInMemoryChannelConditions), + }, + WantPatches: []clientgotesting.PatchActionImpl{ + patchSubscribers(testNS, channelName, subscribers()), + }, + WithReactors: []clientgotesting.ReactionFunc{ + InduceFailure("patch", "inmemorychannels"), }, - Key: testNS + "/" + channeName, WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ - Object: NewChannel(channeName, testNS, - WithChannelProvisioner(provisionerGVK, provisionerName), - // Status Update: - WithChannelProvisionerNotFound(provisionerName, provisionerGVK.Kind), - ), + Object: NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelSubscribers(subscribers()), + WithBackingChannelObjRef(backingChannelObjRef()), + WithBackingChannelFailed("ChannelFailure", "inducing failure for patch inmemorychannels")), }}, - WantErr: false, WantEvents: []string{ - Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + Eventf(corev1.EventTypeWarning, channelReconcileError, "Channel reconcile error: problem patching subscriptions in the backing channel: %v", "inducing failure for patch inmemorychannels"), }, - }, { - Name: "controller defaulted channel", + WantErr: true, + }, + { + Name: "Successful reconciliation", + Key: testKey, Objects: []runtime.Object{ - NewChannel(channeName, testNS, - WithInitChannelConditions, - ), + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelSubscribers(subscribers())), + NewInMemoryChannel(channelName, testNS, + WithInitInMemoryChannelConditions, + WithInMemoryChannelDeploymentReady(), + WithInMemoryChannelServiceReady(), + WithInMemoryChannelEndpointsReady(), + WithInMemoryChannelChannelServiceReady(), + WithInMemoryChannelAddress(backingChannelHostname)), + }, + WantPatches: []clientgotesting.PatchActionImpl{ + patchSubscribers(testNS, channelName, subscribers()), }, - Key: testNS + "/" + channeName, - WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelSubscribers(subscribers()), + WithBackingChannelObjRef(backingChannelObjRef()), + WithBackingChannelReady, + WithMessagingChannelAddress(backingChannelHostname)), + }}, WantEvents: []string{ - Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + Eventf(corev1.EventTypeNormal, channelReconciled, "Channel reconciled: %s", testKey), + Eventf(corev1.EventTypeNormal, channelReadinessChanged, "Channel %q became ready", channelName), }, - }, { - Name: "valid claimed channel", + }, + { + Name: "Already reconciled", + Key: testKey, Objects: []runtime.Object{ - NewChannel(channeName, testNS, - WithChannelProvisioner(provisionerGVK, provisionerName), - WithInitChannelConditions, - ), + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithBackingChannelObjRef(backingChannelObjRef()), + WithMessagingChannelSubscribers(subscribers()), + WithBackingChannelReady, + WithMessagingChannelAddress(backingChannelHostname)), + NewInMemoryChannel(channelName, testNS, + WithInitInMemoryChannelConditions, + WithInMemoryChannelDeploymentReady(), + WithInMemoryChannelServiceReady(), + WithInMemoryChannelEndpointsReady(), + WithInMemoryChannelChannelServiceReady(), + WithInMemoryChannelSubscribers(subscribers()), + WithInMemoryChannelAddress(backingChannelHostname)), }, - Key: testNS + "/" + channeName, - WantErr: false, WantEvents: []string{ - Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + Eventf(corev1.EventTypeNormal, channelReconciled, "Channel reconciled: %s", testKey), }, - }, { - Name: "channel deleted is no-op", + }, + { + Name: "Updating subscribers statuses", + Key: testKey, Objects: []runtime.Object{ - NewChannel(channeName, testNS, - WithInitChannelConditions, - WithChannelDeleted, - ), + NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithBackingChannelObjRef(backingChannelObjRef()), + WithBackingChannelReady, + WithMessagingChannelAddress(backingChannelHostname), + WithMessagingChannelSubscribers(subscribers())), + NewInMemoryChannel(channelName, testNS, + WithInitInMemoryChannelConditions, + WithInMemoryChannelDeploymentReady(), + WithInMemoryChannelServiceReady(), + WithInMemoryChannelEndpointsReady(), + WithInMemoryChannelChannelServiceReady(), + WithInMemoryChannelAddress(backingChannelHostname), + WithInMemoryChannelSubscribers(subscribers()), + WithInMemoryChannelStatusSubscribers(subscriberStatuses())), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewMessagingChannel(channelName, testNS, + WithMessagingChannelTemplate(channelCRD()), + WithInitMessagingChannelConditions, + WithMessagingChannelSubscribers(subscribers()), + WithBackingChannelObjRef(backingChannelObjRef()), + WithBackingChannelReady, + WithMessagingChannelAddress(backingChannelHostname), + WithMesssagingChannelSubscriberStatuses(subscriberStatuses())), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, channelReconciled, "Channel reconciled: %s", testKey), }, - Key: testNS + "/" + channeName, }, } defer logtesting.ClearAll() table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler { return &Reconciler{ - Base: reconciler.NewBase(ctx, controllerAgentName, cmw), - channelLister: listers.GetChannelLister(), + Base: reconciler.NewBase(ctx, controllerAgentName, cmw), + channelLister: listers.GetMessagingChannelLister(), + resourceTracker: fakeResourceTracker{}, } - }, false)) + }, + false, + )) +} + +func channelCRD() metav1.TypeMeta { + return metav1.TypeMeta{ + APIVersion: "messaging.knative.dev/v1alpha1", + Kind: "InMemoryChannel", + } +} + +func subscribers() []eventingduckv1alpha1.SubscriberSpec { + return []eventingduckv1alpha1.SubscriberSpec{{ + UID: "2f9b5e8e-deb6-11e8-9f32-f2801f1b9fd1", + Generation: 1, + SubscriberURI: "call1", + ReplyURI: "sink2", + }, { + UID: "34c5aec8-deb6-11e8-9f32-f2801f1b9fd1", + Generation: 2, + SubscriberURI: "call2", + ReplyURI: "sink2", + }} +} + +func subscriberStatuses() []eventingduckv1alpha1.SubscriberStatus { + return []eventingduckv1alpha1.SubscriberStatus{{ + UID: "2f9b5e8e-deb6-11e8-9f32-f2801f1b9fd1", + ObservedGeneration: 1, + Ready: "True", + }, { + UID: "34c5aec8-deb6-11e8-9f32-f2801f1b9fd1", + ObservedGeneration: 2, + Ready: "True", + }} +} + +func patchSubscribers(namespace, name string, subscribers []eventingduckv1alpha1.SubscriberSpec) clientgotesting.PatchActionImpl { + action := clientgotesting.PatchActionImpl{} + action.Name = name + action.Namespace = namespace + + var spec string + if subscribers != nil { + b, err := json.Marshal(subscribers) + ss := make([]map[string]interface{}, 0) + err = json.Unmarshal(b, &ss) + subs, err := json.Marshal(ss) + if err != nil { + return action + } + spec = fmt.Sprintf(`{"subscribable":{"subscribers":%s}}`, subs) + } else { + spec = `{"subscribable":{}}` + } + + patch := `{"spec":` + spec + `}` + action.Patch = []byte(patch) + return action +} + +func createChannelCRD(namespace, name string, ready bool) *unstructured.Unstructured { + unstructured := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "messaging.knative.dev/v1alpha1", + "kind": "InMemoryChannel", + "metadata": map[string]interface{}{ + "creationTimestamp": nil, + "namespace": namespace, + "name": name, + "ownerReferences": []interface{}{ + map[string]interface{}{ + "apiVersion": "messaging.knative.dev/v1alpha1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Channel", + "name": name, + "uid": "", + }, + }, + }, + }, + } + if ready { + unstructured.Object["status"] = map[string]interface{}{ + "address": map[string]interface{}{ + "hostname": backingChannelHostname, + "url": fmt.Sprintf("http://%s", backingChannelHostname), + }, + } + } + return unstructured +} + +func backingChannelObjRef() *corev1.ObjectReference { + return &corev1.ObjectReference{ + APIVersion: "messaging.knative.dev/v1alpha1", + Kind: "InMemoryChannel", + Namespace: testNS, + Name: channelName, + } } diff --git a/pkg/reconciler/channel/controller.go b/pkg/reconciler/channel/controller.go index 310df88bb3d..3c827c68cef 100644 --- a/pkg/reconciler/channel/controller.go +++ b/pkg/reconciler/channel/controller.go @@ -18,12 +18,13 @@ package channel import ( "context" + "github.com/knative/eventing/pkg/duck" "github.com/knative/eventing/pkg/reconciler" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" - channelinformer "github.com/knative/eventing/pkg/client/injection/informers/eventing/v1alpha1/channel" + channelinformer "github.com/knative/eventing/pkg/client/injection/informers/messaging/v1alpha1/channel" ) const ( @@ -31,7 +32,7 @@ const ( ReconcilerName = "Channels" // controllerAgentName is the string used by this controller to identify // itself when creating events. - controllerAgentName = "channel-default-controller" + controllerAgentName = "ch-default-controller" ) // NewController initializes the controller and is called by the generated code @@ -42,6 +43,7 @@ func NewController( ) *controller.Impl { channelInformer := channelinformer.Get(ctx) + resourceInformer := duck.NewResourceInformer(ctx) r := &Reconciler{ Base: reconciler.NewBase(ctx, controllerAgentName, cmw), @@ -49,6 +51,8 @@ func NewController( } impl := controller.NewImpl(r, r.Logger, ReconcilerName) + r.resourceTracker = resourceInformer.NewTracker(impl.EnqueueKey, controller.GetTrackerLease(ctx)) + r.Logger.Info("Setting up event handlers") channelInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) diff --git a/pkg/reconciler/channel/controller_test.go b/pkg/reconciler/channel/controller_test.go index b96a7d12e1c..459bae7c42f 100644 --- a/pkg/reconciler/channel/controller_test.go +++ b/pkg/reconciler/channel/controller_test.go @@ -25,7 +25,9 @@ import ( . "knative.dev/pkg/reconciler/testing" // Fake injection informers - _ "github.com/knative/eventing/pkg/client/injection/informers/eventing/v1alpha1/channel/fake" + _ "github.com/knative/eventing/pkg/client/injection/informers/messaging/v1alpha1/channel/fake" + _ "knative.dev/pkg/injection/clients/dynamicclient/fake" + _ "knative.dev/pkg/injection/clients/kubeclient/fake" ) func TestNew(t *testing.T) { diff --git a/pkg/reconciler/channel/resources/channel.go b/pkg/reconciler/channel/resources/channel.go new file mode 100644 index 00000000000..768c832459d --- /dev/null +++ b/pkg/reconciler/channel/resources/channel.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "knative.dev/pkg/kmeta" + + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewChannel returns an unstructured.Unstructured based on the ChannelTemplateSpec for a given Channel. +func NewChannel(c *v1alpha1.Channel) (*unstructured.Unstructured, error) { + // Set the name of the resource we're creating as well as the namespace, etc. + template := eventingduck.ChannelTemplateSpecInternal{ + TypeMeta: metav1.TypeMeta{ + Kind: c.Spec.ChannelTemplate.Kind, + APIVersion: c.Spec.ChannelTemplate.APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(c), + }, + Name: c.Name, + Namespace: c.Namespace, + }, + Spec: c.Spec.ChannelTemplate.Spec, + } + raw, err := json.Marshal(template) + if err != nil { + return nil, err + } + u := &unstructured.Unstructured{} + err = json.Unmarshal(raw, u) + if err != nil { + return nil, err + } + return u, nil +} diff --git a/pkg/reconciler/eventingchannel/channel.go b/pkg/reconciler/eventingchannel/channel.go new file mode 100644 index 00000000000..150eadd6c38 --- /dev/null +++ b/pkg/reconciler/eventingchannel/channel.go @@ -0,0 +1,155 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package eventingchannel + +import ( + "context" + "fmt" + "reflect" + "time" + + corev1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/tools/cache" + + "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + listers "github.com/knative/eventing/pkg/client/listers/eventing/v1alpha1" + "github.com/knative/eventing/pkg/logging" + "github.com/knative/eventing/pkg/reconciler" + "go.uber.org/zap" + "knative.dev/pkg/controller" +) + +const ( + channelReadinessChanged = "ChannelReadinessChanged" + channelReconciled = "ChannelReconciled" + channelUpdateStatusFailed = "ChannelUpdateStatusFailed" +) + +type Reconciler struct { + *reconciler.Base + + // listers index properties about resources + channelLister listers.ChannelLister +} + +// Check that our Reconciler implements controller.Reconciler +var _ controller.Reconciler = (*Reconciler)(nil) + +// Reconcile will check if the channel is being watched by provisioner's channel controller +// This will improve UX. See https://github.com/knative/eventing/issues/779 +func (r *Reconciler) Reconcile(ctx context.Context, key string) error { + // Convert the namespace/name string into a distinct namespace and name + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + logging.FromContext(ctx).Error("invalid resource key") + return nil + } + + // Get the Channel resource with this namespace/name + original, err := r.channelLister.Channels(namespace).Get(name) + if apierrs.IsNotFound(err) { + // The resource may no longer exist, in which case we stop processing. + logging.FromContext(ctx).Error("Channel key in work queue no longer exists") + return nil + } else if err != nil { + return err + } + + // Delete is a no-op. + if original.DeletionTimestamp != nil { + return nil + } + + // Don't modify the informers copy + channel := original.DeepCopy() + + // Reconcile this copy of the Channel and then write back any status + // updates regardless of whether the reconcile error out. + reconcileErr := r.reconcile(ctx, channel) + if reconcileErr != nil { + logging.FromContext(ctx).Error("Error reconciling Channel", zap.Error(reconcileErr)) + } else { + logging.FromContext(ctx).Debug("Successfully reconciled Channel") + r.Recorder.Eventf(channel, corev1.EventTypeNormal, channelReconciled, "Channel reconciled: %s", key) + } + + if _, updateStatusErr := r.updateStatus(ctx, channel.DeepCopy()); updateStatusErr != nil { + logging.FromContext(ctx).Warn("Error updating Channel status", zap.Error(updateStatusErr)) + r.Recorder.Eventf(channel, corev1.EventTypeWarning, channelUpdateStatusFailed, "Failed to update channel status: %s", key) + return updateStatusErr + } + + // Requeue if the resource is not ready: + return reconcileErr +} + +func (r *Reconciler) reconcile(ctx context.Context, ch *v1alpha1.Channel) error { + // Do not Initialize() Status in channel-default-controller. It will set ChannelConditionProvisionerInstalled=True + // Directly call GetCondition(). If the Status was never initialized then GetCondition() will return nil and + // IsUnknown() will return true + c := ch.Status.GetCondition(v1alpha1.ChannelConditionProvisionerInstalled) + + if c == nil || c.IsUnknown() { + + var proName string + var proKind string + if ch.Spec.Provisioner != nil { + proName = ch.Spec.Provisioner.Name + proKind = ch.Spec.Provisioner.Kind + } + + ch.Status.MarkProvisionerNotInstalled( + "Provisioner not found.", + "Specified provisioner [Name:%s Kind:%s] is not installed or not controlling the channel.", + proName, + proKind, + ) + } + return nil +} + +func (r *Reconciler) updateStatus(ctx context.Context, desired *v1alpha1.Channel) (*v1alpha1.Channel, error) { + channel, err := r.channelLister.Channels(desired.Namespace).Get(desired.Name) + if err != nil { + return nil, err + } + + // If there's nothing to update, just return. + if reflect.DeepEqual(channel.Status, desired.Status) { + return channel, nil + } + + becomesReady := desired.Status.IsReady() && !channel.Status.IsReady() + + // Don't modify the informers copy. + existing := channel.DeepCopy() + existing.Status = desired.Status + + c, err := r.EventingClientSet.EventingV1alpha1().Channels(desired.Namespace).UpdateStatus(existing) + + if err == nil && becomesReady { + duration := time.Since(c.ObjectMeta.CreationTimestamp.Time) + logging.FromContext(ctx).Sugar().Infof("Channel %q became ready after %v", channel.Name, duration) + r.Recorder.Event(channel, corev1.EventTypeNormal, channelReadinessChanged, fmt.Sprintf("Channel %q became ready", channel.Name)) + if err := r.StatsReporter.ReportReady("Channel", channel.Namespace, channel.Name, duration); err != nil { + logging.FromContext(ctx).Sugar().Infof("failed to record ready for Channel, %v", err) + } + } + + return c, err +} diff --git a/pkg/reconciler/eventingchannel/channel_test.go b/pkg/reconciler/eventingchannel/channel_test.go new file mode 100644 index 00000000000..bb798f048ce --- /dev/null +++ b/pkg/reconciler/eventingchannel/channel_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package eventingchannel + +import ( + "context" + "testing" + + "knative.dev/pkg/configmap" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + clientgotesting "k8s.io/client-go/testing" + + eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" + "github.com/knative/eventing/pkg/reconciler" + "knative.dev/pkg/controller" + logtesting "knative.dev/pkg/logging/testing" + + . "github.com/knative/eventing/pkg/reconciler/testing" + . "knative.dev/pkg/reconciler/testing" +) + +const ( + testNS = "testnamespace" + channeName = "testchannel" + provisionerName = "testprovisioner" +) + +var ( + provisionerGVK = metav1.GroupVersionKind{ + Group: "eventing.knative.dev", + Version: "v1alpha1", + Kind: "ClusterChannelProvisioner", + } +) + +func init() { + // Add types to scheme + _ = eventingv1alpha1.AddToScheme(scheme.Scheme) +} + +func TestAllCases(t *testing.T) { + table := TableTest{ + { + Name: "bad workqueue key", + // Make sure Reconcile handles bad keys. + Key: "too/many/parts", + }, { + Name: "key not found", + // Make sure Reconcile handles good keys that don't exist. + Key: "foo/not-found", + }, + //{ // TODO: there is a bug in the controller, it reconcile for empty provisioner. + // Name: "incomplete channel", + // Objects: []runtime.Object{ + // NewChannel(channelName, testNS), + // }, + // Key: testNS + "/" + channeName, + // WantErr: true, + // WantEvents: []string{ + // Eventf(corev1.EventTypeWarning, "TODO", ""), + //}, + { + Name: "unclaimed channel, empty provisioner", + Objects: []runtime.Object{ + NewChannel(channeName, testNS), + }, + Key: testNS + "/" + channeName, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewChannel(channeName, testNS, + WithChannelProvisionerNotFound("", ""), // TODO: THIS IS A BUG, there is no validation checking. + ), + }}, + WantErr: true, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + Eventf(corev1.EventTypeWarning, "ChannelUpdateStatusFailed", "Failed to update channel status: %s/%s", testNS, channeName), + }, + }, { + Name: "unclaimed channel", + Objects: []runtime.Object{ + NewChannel(channeName, testNS, + WithChannelProvisioner(provisionerGVK, provisionerName), + ), + }, + Key: testNS + "/" + channeName, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: NewChannel(channeName, testNS, + WithChannelProvisioner(provisionerGVK, provisionerName), + // Status Update: + WithChannelProvisionerNotFound(provisionerName, provisionerGVK.Kind), + ), + }}, + WantErr: false, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + }, + }, { + Name: "controller defaulted channel", + Objects: []runtime.Object{ + NewChannel(channeName, testNS, + WithInitChannelConditions, + ), + }, + Key: testNS + "/" + channeName, + WantErr: false, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + }, + }, { + Name: "valid claimed channel", + Objects: []runtime.Object{ + NewChannel(channeName, testNS, + WithChannelProvisioner(provisionerGVK, provisionerName), + WithInitChannelConditions, + ), + }, + Key: testNS + "/" + channeName, + WantErr: false, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "ChannelReconciled", "Channel reconciled: %s/%s", testNS, channeName), + }, + }, { + Name: "channel deleted is no-op", + Objects: []runtime.Object{ + NewChannel(channeName, testNS, + WithInitChannelConditions, + WithChannelDeleted, + ), + }, + Key: testNS + "/" + channeName, + }, + } + + defer logtesting.ClearAll() + table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler { + return &Reconciler{ + Base: reconciler.NewBase(ctx, controllerAgentName, cmw), + channelLister: listers.GetChannelLister(), + } + }, false)) +} diff --git a/pkg/reconciler/eventingchannel/controller.go b/pkg/reconciler/eventingchannel/controller.go new file mode 100644 index 00000000000..38ca553c596 --- /dev/null +++ b/pkg/reconciler/eventingchannel/controller.go @@ -0,0 +1,56 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package eventingchannel + +import ( + "context" + + "github.com/knative/eventing/pkg/reconciler" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + + channelinformer "github.com/knative/eventing/pkg/client/injection/informers/eventing/v1alpha1/channel" +) + +const ( + // ReconcilerName is the name of the reconciler + ReconcilerName = "Channels" + // controllerAgentName is the string used by this controller to identify + // itself when creating events. + controllerAgentName = "channel-default-controller" +) + +// NewController initializes the controller and is called by the generated code +// Registers event handlers to enqueue events +func NewController( + ctx context.Context, + cmw configmap.Watcher, +) *controller.Impl { + + channelInformer := channelinformer.Get(ctx) + + r := &Reconciler{ + Base: reconciler.NewBase(ctx, controllerAgentName, cmw), + channelLister: channelInformer.Lister(), + } + impl := controller.NewImpl(r, r.Logger, ReconcilerName) + + r.Logger.Info("Setting up event handlers") + channelInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + + return impl +} diff --git a/pkg/reconciler/eventingchannel/controller_test.go b/pkg/reconciler/eventingchannel/controller_test.go new file mode 100644 index 00000000000..15338b3e847 --- /dev/null +++ b/pkg/reconciler/eventingchannel/controller_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package eventingchannel + +import ( + "testing" + + "knative.dev/pkg/configmap" + + logtesting "knative.dev/pkg/logging/testing" + . "knative.dev/pkg/reconciler/testing" + + // Fake injection informers + _ "github.com/knative/eventing/pkg/client/injection/informers/eventing/v1alpha1/channel/fake" +) + +func TestNew(t *testing.T) { + defer logtesting.ClearAll() + ctx, _ := SetupFakeContext(t) + + c := NewController(ctx, configmap.NewFixedWatcher()) + + if c == nil { + t.Fatal("Expected NewController to return a non-nil value") + } +} diff --git a/pkg/reconciler/sequence/resources/channel.go b/pkg/reconciler/sequence/resources/channel.go index 509f26d09fd..f9b4c8eb56e 100644 --- a/pkg/reconciler/sequence/resources/channel.go +++ b/pkg/reconciler/sequence/resources/channel.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "knative.dev/pkg/kmeta" + eventingduck "github.com/knative/eventing/pkg/apis/duck/v1alpha1" v1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,7 +37,7 @@ func SequenceChannelName(sequenceName string, step int) string { // for a given sequence. func NewChannel(name string, p *v1alpha1.Sequence) (*unstructured.Unstructured, error) { // Set the name of the resource we're creating as well as the namespace, etc. - template := v1alpha1.ChannelTemplateSpecInternal{ + template := eventingduck.ChannelTemplateSpecInternal{ TypeMeta: metav1.TypeMeta{ Kind: p.Spec.ChannelTemplate.Kind, APIVersion: p.Spec.ChannelTemplate.APIVersion, diff --git a/pkg/reconciler/sequence/sequence_test.go b/pkg/reconciler/sequence/sequence_test.go index 79decf867b7..a6790872f89 100644 --- a/pkg/reconciler/sequence/sequence_test.go +++ b/pkg/reconciler/sequence/sequence_test.go @@ -34,6 +34,7 @@ import ( logtesting "knative.dev/pkg/logging/testing" . "knative.dev/pkg/reconciler/testing" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" "github.com/knative/eventing/pkg/reconciler" @@ -105,12 +106,12 @@ func createSubscriber(stepNumber int) eventingv1alpha1.SubscriberSpec { func TestAllCases(t *testing.T) { pKey := testNS + "/" + sequenceName - imc := v1alpha1.ChannelTemplateSpec{ - metav1.TypeMeta{ + imc := eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: metav1.TypeMeta{ APIVersion: "messaging.knative.dev/v1alpha1", Kind: "inmemorychannel", }, - &runtime.RawExtension{Raw: []byte("{}")}, + Spec: &runtime.RawExtension{Raw: []byte("{}")}, } table := TableTest{ diff --git a/pkg/reconciler/testing/broker.go b/pkg/reconciler/testing/broker.go index 02204a83d68..66e44a0841a 100644 --- a/pkg/reconciler/testing/broker.go +++ b/pkg/reconciler/testing/broker.go @@ -20,6 +20,7 @@ import ( "context" "time" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -66,7 +67,7 @@ func WithBrokerChannelProvisioner(provisioner *corev1.ObjectReference) BrokerOpt // WithBrokerChannelCRD sets the Broker's ChannelTemplateSpec to the specified CRD. func WithBrokerChannelCRD(crdType metav1.TypeMeta) BrokerOption { return func(b *v1alpha1.Broker) { - b.Spec.ChannelTemplate = v1alpha1.ChannelTemplateSpec{ + b.Spec.ChannelTemplate = &eventingduckv1alpha1.ChannelTemplateSpec{ TypeMeta: crdType, } } @@ -147,7 +148,7 @@ func WithBrokerIngressChannelReady() BrokerOption { func WithBrokerDeprecated() BrokerOption { return func(b *v1alpha1.Broker) { - b.Status.MarkDeprecated("ClusterChannelProvisionerDeprecated", "Provisioners are deprecated and will be removed in 0.8. Recommended replacement is CRD based channels using spec.channelTemplateSpec.") + b.Status.MarkDeprecated("ClusterChannelProvisionerDeprecated", "Provisioners are deprecated and will be removed in 0.9. Recommended replacement is CRD based channels using spec.channelTemplateSpec.") } } diff --git a/pkg/reconciler/testing/listers.go b/pkg/reconciler/testing/listers.go index 4b3bde158b4..0acde9142dd 100644 --- a/pkg/reconciler/testing/listers.go +++ b/pkg/reconciler/testing/listers.go @@ -135,6 +135,10 @@ func (l *Listers) GetChannelLister() eventinglisters.ChannelLister { return eventinglisters.NewChannelLister(l.indexerFor(&eventingv1alpha1.Channel{})) } +func (l *Listers) GetMessagingChannelLister() messaginglisters.ChannelLister { + return messaginglisters.NewChannelLister(l.indexerFor(&messagingv1alpha1.Channel{})) +} + func (l *Listers) GetSequenceLister() messaginglisters.SequenceLister { return messaginglisters.NewSequenceLister(l.indexerFor(&messagingv1alpha1.Sequence{})) } diff --git a/pkg/reconciler/testing/messagingchannel.go b/pkg/reconciler/testing/messagingchannel.go new file mode 100644 index 00000000000..d93f04d44df --- /dev/null +++ b/pkg/reconciler/testing/messagingchannel.go @@ -0,0 +1,119 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "context" + "k8s.io/api/core/v1" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" + "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" + "knative.dev/pkg/apis" + duck "knative.dev/pkg/apis/duck/v1alpha1" + "knative.dev/pkg/apis/duck/v1beta1" +) + +// TODO once we remove Channel from eventing, we should rename this to be just Channel. + +// MessagingChannelOption enables further configuration of a Channel. +type MessagingChannelOption func(*v1alpha1.Channel) + +// NewMessagingChannel creates a Channel with MessagingChannelOptions +func NewMessagingChannel(name, namespace string, o ...MessagingChannelOption) *v1alpha1.Channel { + c := &v1alpha1.Channel{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "messaging.knative.dev/v1alpha1", + Kind: "Channel", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + for _, opt := range o { + opt(c) + } + c.SetDefaults(context.Background()) + return c +} + +// WithInitMessagingChannelConditions initializes the Channel's conditions. +func WithInitMessagingChannelConditions(c *v1alpha1.Channel) { + c.Status.InitializeConditions() +} + +func WithMessagingChannelDeleted(c *v1alpha1.Channel) { + t := metav1.NewTime(time.Unix(1e9, 0)) + c.ObjectMeta.SetDeletionTimestamp(&t) +} + +func WithMessagingChannelTemplate(typeMeta metav1.TypeMeta) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + c.Spec.ChannelTemplate = &eventingduckv1alpha1.ChannelTemplateSpec{ + TypeMeta: typeMeta, + } + } +} + +func WithBackingChannelFailed(reason, msg string) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + c.Status.MarkBackingChannelFailed(reason, msg) + } +} + +func WithBackingChannelReady(c *v1alpha1.Channel) { + c.Status.MarkBackingChannelReady() +} + +func WithBackingChannelObjRef(objRef *v1.ObjectReference) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + c.Status.Channel = objRef + } +} + +func WithMessagingChannelAddress(hostname string) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + address := &duck.Addressable{ + Addressable: v1beta1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: hostname, + }, + }, + } + c.Status.SetAddress(address) + } +} + +func WithMessagingChannelSubscribers(subscribers []eventingduckv1alpha1.SubscriberSpec) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + c.Spec.Subscribable = &eventingduckv1alpha1.Subscribable{ + Subscribers: subscribers, + } + } +} + +func WithMesssagingChannelSubscriberStatuses(subscriberStatuses []eventingduckv1alpha1.SubscriberStatus) MessagingChannelOption { + return func(c *v1alpha1.Channel) { + c.Status.SubscribableStatus = &eventingduckv1alpha1.SubscribableStatus{ + Subscribers: subscriberStatuses, + } + } +} diff --git a/pkg/reconciler/testing/sequence.go b/pkg/reconciler/testing/sequence.go index ad1b8164015..b8efa0f3aef 100644 --- a/pkg/reconciler/testing/sequence.go +++ b/pkg/reconciler/testing/sequence.go @@ -20,6 +20,7 @@ import ( "context" "time" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" corev1 "k8s.io/api/core/v1" @@ -54,7 +55,7 @@ func WithSequenceDeleted(p *v1alpha1.Sequence) { p.ObjectMeta.SetDeletionTimestamp(&deleteTime) } -func WithSequenceChannelTemplateSpec(cts v1alpha1.ChannelTemplateSpec) SequenceOption { +func WithSequenceChannelTemplateSpec(cts eventingduckv1alpha1.ChannelTemplateSpec) SequenceOption { return func(p *v1alpha1.Sequence) { p.Spec.ChannelTemplate = cts } diff --git a/test/base/resources/eventing.go b/test/base/resources/eventing.go index c70f7b8224f..d67f9c61713 100644 --- a/test/base/resources/eventing.go +++ b/test/base/resources/eventing.go @@ -19,6 +19,7 @@ package resources // This file contains functions that construct Eventing resources. import ( + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -114,7 +115,7 @@ func WithDeprecatedChannelTemplateForBroker(provisionerName string) BrokerOption // WithChannelTemplateForBroker returns a function that adds a ChannelTemplate for the given Broker. func WithChannelTemplateForBroker(channelTypeMeta metav1.TypeMeta) BrokerOption { return func(b *eventingv1alpha1.Broker) { - channelTemplate := eventingv1alpha1.ChannelTemplateSpec{ + channelTemplate := &eventingduckv1alpha1.ChannelTemplateSpec{ TypeMeta: channelTypeMeta, } b.Spec.ChannelTemplate = channelTemplate diff --git a/test/base/resources/messaging.go b/test/base/resources/messaging.go index 57c764597b3..2f8dfaa87d5 100644 --- a/test/base/resources/messaging.go +++ b/test/base/resources/messaging.go @@ -21,6 +21,7 @@ package resources import ( kafkamessagingv1alpha1 "github.com/knative/eventing/contrib/kafka/pkg/apis/messaging/v1alpha1" natssmessagingv1alpha1 "github.com/knative/eventing/contrib/natss/pkg/apis/messaging/v1alpha1" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -70,7 +71,7 @@ func WithReplyForSequence(name string, typemeta *metav1.TypeMeta) SequenceOption func Sequence( name string, steps []eventingv1alpha1.SubscriberSpec, - channelTemplate messagingv1alpha1.ChannelTemplateSpec, + channelTemplate eventingduckv1alpha1.ChannelTemplateSpec, options ...SequenceOption, ) *messagingv1alpha1.Sequence { sequence := &messagingv1alpha1.Sequence{ diff --git a/test/common/creation.go b/test/common/creation.go index 4147a5f788d..b7080b97dd6 100644 --- a/test/common/creation.go +++ b/test/common/creation.go @@ -17,8 +17,8 @@ limitations under the License. package common import ( + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" - messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" sourcesv1alpha1 "github.com/knative/eventing/pkg/apis/sources/v1alpha1" "github.com/knative/eventing/test/base/resources" corev1 "k8s.io/api/core/v1" @@ -153,7 +153,7 @@ func (client *Client) CreateTriggerOrFail(name string, options ...resources.Trig func (client *Client) CreateSequenceOrFail( name string, steps []eventingv1alpha1.SubscriberSpec, - channelTemplate messagingv1alpha1.ChannelTemplateSpec, + channelTemplate eventingduckv1alpha1.ChannelTemplateSpec, options ...resources.SequenceOption, ) { namespace := client.Namespace diff --git a/test/e2e/sequence_test.go b/test/e2e/sequence_test.go index a3a1590dd88..c3bc4e5fc7c 100644 --- a/test/e2e/sequence_test.go +++ b/test/e2e/sequence_test.go @@ -22,8 +22,8 @@ import ( "fmt" "testing" + eventingduckv1alpha1 "github.com/knative/eventing/pkg/apis/duck/v1alpha1" eventingv1alpha1 "github.com/knative/eventing/pkg/apis/eventing/v1alpha1" - messagingv1alpha1 "github.com/knative/eventing/pkg/apis/messaging/v1alpha1" "github.com/knative/eventing/test/base/resources" "github.com/knative/eventing/test/common" "k8s.io/apimachinery/pkg/util/uuid" @@ -74,7 +74,7 @@ func TestSequence(t *testing.T) { } // create channelTemplate for the Sequence - channelTemplate := messagingv1alpha1.ChannelTemplateSpec{ + channelTemplate := eventingduckv1alpha1.ChannelTemplateSpec{ TypeMeta: *(channelTypeMeta), }