diff --git a/test/conformance/helpers/channel_crd_metadata_test_helper.go b/test/conformance/helpers/channel_crd_metadata_test_helper.go index 7fadc843a00..0723716dc23 100644 --- a/test/conformance/helpers/channel_crd_metadata_test_helper.go +++ b/test/conformance/helpers/channel_crd_metadata_test_helper.go @@ -74,7 +74,7 @@ func channelCRDHasRequiredLabels(client *testlib.Client, channel metav1.TypeMeta // label of messaging.knative.dev/subscribable: "true" // label of duck.knative.dev/addressable: "true" - validateRequiredLabels(client, channel, channelLabels) + ValidateRequiredLabels(client, channel, channelLabels) } func channelCRDHasProperCategory(st *testing.T, client *testlib.Client, channel metav1.TypeMeta) { diff --git a/test/conformance/helpers/metadata.go b/test/conformance/helpers/metadata.go index f1346f1e3ba..e00e1fb1a6c 100644 --- a/test/conformance/helpers/metadata.go +++ b/test/conformance/helpers/metadata.go @@ -22,7 +22,7 @@ import ( testlib "knative.dev/eventing/test/lib" ) -func validateRequiredLabels(client *testlib.Client, object metav1.TypeMeta, labels map[string]string) { +func ValidateRequiredLabels(client *testlib.Client, object metav1.TypeMeta, labels map[string]string) { for k, v := range labels { if !objectHasRequiredLabel(client, object, k, v) { client.T.Fatalf("can't find label '%s=%s' in CRD %q", k, v, object) diff --git a/test/conformance/helpers/source_crd_metadata_test_helper.go b/test/conformance/helpers/sources/source_crd_metadata_test_helper.go similarity index 86% rename from test/conformance/helpers/source_crd_metadata_test_helper.go rename to test/conformance/helpers/sources/source_crd_metadata_test_helper.go index fa931175073..68133f066b6 100644 --- a/test/conformance/helpers/source_crd_metadata_test_helper.go +++ b/test/conformance/helpers/sources/source_crd_metadata_test_helper.go @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helpers +package sources import ( + "knative.dev/eventing/test/conformance/helpers" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -41,8 +42,8 @@ func SourceCRDMetadataTestHelperWithChannelTestRunner( // From spec: // Each source MUST have the following: // label of duck.knative.dev/source: "true" - t.Run("Source CRD has required label", func(t *testing.T) { - validateRequiredLabels(client, source, sourceLabels) + st.Run("Source CRD has required label", func(t *testing.T) { + helpers.ValidateRequiredLabels(client, source, sourceLabels) }) }) diff --git a/test/conformance/helpers/sources/source_status_test_helper.go b/test/conformance/helpers/sources/source_status_test_helper.go new file mode 100644 index 00000000000..e2746f62a4e --- /dev/null +++ b/test/conformance/helpers/sources/source_status_test_helper.go @@ -0,0 +1,128 @@ +/* +Copyright 2020 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 sources + +import ( + "fmt" + "github.com/pkg/errors" + "knative.dev/eventing/test/lib/duck" + "knative.dev/eventing/test/lib/resources" + "knative.dev/pkg/apis" + duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + testlib "knative.dev/eventing/test/lib" +) + +// SourceStatusTestHelperWithComponentsTestRunner runs the Source status +// conformance tests for all sources in the ComponentsTestRunner. This test +// needs an instance created of each source which should be initialized via +// ComponentsTestRunner.AddComponentSetupClientOption. +// +// Note: The source object name must be the lower case Kind name (e.g. +// apiserversource for the Kind: ApiServerSource source) +func SourceStatusTestHelperWithComponentsTestRunner( + t *testing.T, + componentsTestRunner testlib.ComponentsTestRunner, + options ...testlib.SetupClientOption, +) { + table := [] struct { + name string + feature testlib.Feature + // Sources report success via either a Ready condition or a Succeeded condition + want apis.ConditionType + } { + { + "Long living sources have Ready status condition", + testlib.FeatureLongLiving, + apis.ConditionReady, + }, + { + "Batch sources have Succeeded status condition", + testlib.FeatureBatch, + apis.ConditionSucceeded, + }, + + } + for _, tc := range table { + n := tc.name + f := tc.feature + w := tc.want + t.Run(n, func(t *testing.T) { + componentsTestRunner.RunTestsWithComponentOptions(t, f, true, + func(st *testing.T, source metav1.TypeMeta, + componentOptions ...testlib.SetupClientOption) { + st.Log("About to setup client") + options = append(options, componentOptions...) + client := testlib.Setup(st, true, options...) + defer testlib.TearDown(client) + validateSourceStatus(st, client, source, w, options...) + }) + }) + } +} + +func validateSourceStatus(st *testing.T, client *testlib.Client, source metav1.TypeMeta, successCondition apis.ConditionType, options ...testlib.SetupClientOption) { + const( + sourceName = "source-req-status" + ) + + st.Logf("Running source status conformance test with source %q", source) + + v1beta1Src, err := getSourceAsV1Beta1Source(client, source) + if err != nil { + st.Fatalf("unable to get source %q with v1beta1 duck type: %v", source, err) + } + + // SPEC: Sources MUST implement conditions with a Ready condition for long lived sources, and Succeeded for batch style sources. + if ! hasCondition(v1beta1Src, successCondition){ + st.Fatalf("%q does not have %q", source, successCondition) + } + + // SPEC: Sources MUST propagate the sinkUri to their status to signal to the cluster where their events are being sent. + if v1beta1Src.Status.SinkURI.Host == "" { + st.Fatalf("sinkUri was not propagated for source %q", source) + } +} + +func getSourceAsV1Beta1Source(client *testlib.Client, + source metav1.TypeMeta) (*duckv1beta1.Source, error){ + srcName := strings.ToLower(fmt.Sprintf("%s", source.Kind)) + metaResource := resources.NewMetaResource(srcName, client.Namespace, + &source) + obj, err := duck.GetGenericObject(client.Dynamic, metaResource, + &duckv1beta1.Source{}) + if err != nil { + return nil, errors.Wrapf(err, "unable to get the source as v1beta1 Source duck type: %q", source) + } + srcObj, ok := obj.(*duckv1beta1.Source) + if !ok { + return nil, errors.Errorf("unable to cast source %q to v1beta1 Source" + + " duck type", source) + } + return srcObj, nil +} + +func hasCondition(src *duckv1beta1.Source, t apis.ConditionType) bool{ + if src.Status.GetCondition(t) == nil{ + return false + } + return true +} diff --git a/test/conformance/main_test.go b/test/conformance/main_test.go index fff80a2f272..b12ed719016 100644 --- a/test/conformance/main_test.go +++ b/test/conformance/main_test.go @@ -18,8 +18,10 @@ limitations under the License. package conformance import ( + "fmt" "log" "os" + "strings" "testing" "knative.dev/pkg/test/zipkin" @@ -27,6 +29,12 @@ import ( "knative.dev/eventing/test" testlib "knative.dev/eventing/test/lib" "knative.dev/eventing/test/lib/resources" + "knative.dev/eventing/test/lib/setupclientoptions" +) +const ( + roleName = "event-watcher-r" + serviceAccountName = "event-watcher-sa" + recordEventsPodName = "api-server-source-logger-pod" ) var channelTestRunner testlib.ComponentsTestRunner @@ -43,12 +51,14 @@ func TestMain(m *testing.M) { ComponentsToTest: test.EventingFlags.Channels, } sourcesTestRunner = testlib.ComponentsTestRunner{ - ComponentsToTest: test.EventingFlags.Sources, + ComponentFeatureMap: testlib.SourceFeatureMap, + ComponentsToTest: test.EventingFlags.Sources, } brokerClass = test.EventingFlags.BrokerClass brokerName = test.EventingFlags.BrokerName brokerNamespace = test.EventingFlags.BrokerNamespace + addSourcesInitializers() // Any tests may SetupZipkinTracing, it will only actually be done once. This should be the ONLY // place that cleans it up. If an individual test calls this instead, then it will break other // tests that need the tracing in place. @@ -58,3 +68,14 @@ func TestMain(m *testing.M) { return m.Run() }()) } + +func addSourcesInitializers() { + name := strings.ToLower(fmt.Sprintf("%s", + testlib.ApiServerSourceTypeMeta.Kind)) + sourcesTestRunner.AddComponentSetupClientOption( + testlib.ApiServerSourceTypeMeta, + setupclientoptions.ApiServerSourceClientSetupOption(name, + "Reference", + recordEventsPodName, roleName, serviceAccountName), + ) +} diff --git a/test/conformance/source_crd_metadata_test.go b/test/conformance/source_crd_metadata_test.go index 8cab967f6b0..3a810740dfd 100644 --- a/test/conformance/source_crd_metadata_test.go +++ b/test/conformance/source_crd_metadata_test.go @@ -21,10 +21,10 @@ package conformance import ( "testing" - "knative.dev/eventing/test/conformance/helpers" + srchelpers "knative.dev/eventing/test/conformance/helpers/sources" testlib "knative.dev/eventing/test/lib" ) func TestSourceCRDMetadata(t *testing.T) { - helpers.SourceCRDMetadataTestHelperWithChannelTestRunner(t, sourcesTestRunner, testlib.SetupClientOptionNoop) + srchelpers.SourceCRDMetadataTestHelperWithChannelTestRunner(t, sourcesTestRunner, testlib.SetupClientOptionNoop) } diff --git a/test/conformance/source_status_test.go b/test/conformance/source_status_test.go new file mode 100644 index 00000000000..2fb34d0ed59 --- /dev/null +++ b/test/conformance/source_status_test.go @@ -0,0 +1,30 @@ +// +build e2e + +/* +Copyright 2020 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 conformance + +import ( + srchelpers "knative.dev/eventing/test/conformance/helpers/sources" + testlib "knative.dev/eventing/test/lib" + "testing" +) + +func TestSourceStatus(t *testing.T) { + srchelpers.SourceStatusTestHelperWithComponentsTestRunner(t, + sourcesTestRunner, testlib.SetupClientOptionNoop) +} diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index a3eb49b1aa8..80eeabd222e 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -39,6 +39,6 @@ install_sugar || fail_test "Could not install Sugar Controller" unleash_duck || fail_test "Could not unleash the chaos duck" echo "Running tests with Multi Tenant Channel Based Broker" -go_test_e2e -timeout=30m -parallel=12 ./test/e2e ./test/conformance -brokerclass=MTChannelBasedBroker -channels=messaging.knative.dev/v1beta1:Channel,messaging.knative.dev/v1beta1:InMemoryChannel,messaging.knative.dev/v1:Channel,messaging.knative.dev/v1:InMemoryChannel -sources=sources.knative.dev/v1alpha2:ApiServerSource,sources.knative.dev/v1alpha2:ContainerSource,sources.knative.dev/v1alpha2:PingSource || fail_test +go_test_e2e -timeout=30m -parallel=12 ./test/e2e ./test/conformance -brokerclass=MTChannelBasedBroker -channels=messaging.knative.dev/v1beta1:Channel,messaging.knative.dev/v1beta1:InMemoryChannel,messaging.knative.dev/v1:Channel,messaging.knative.dev/v1:InMemoryChannel -sources=sources.knative.dev/v1beta1:ApiServerSource,sources.knative.dev/v1alpha2:ContainerSource,sources.knative.dev/v1alpha2:PingSource || fail_test success diff --git a/test/lib/config.go b/test/lib/config.go index c2832206b52..98bba5c477d 100644 --- a/test/lib/config.go +++ b/test/lib/config.go @@ -46,17 +46,36 @@ var ChannelFeatureMap = map[metav1.TypeMeta][]Feature{ MessagingChannelTypeMeta: {FeatureBasic}, } +var ApiServerSourceTypeMeta = metav1.TypeMeta{ + APIVersion: resources.SourcesAPIVersion, + Kind: resources.ApiServerSourceKind, +} + +var PingSourceTypeMeta = metav1.TypeMeta{ + APIVersion: resources.SourcesAPIVersion, + Kind: resources.PingSourceKind, +} + +var SourceFeatureMap = map[metav1.TypeMeta][]Feature{ + ApiServerSourceTypeMeta: {FeatureBasic, FeatureLongLiving}, + PingSourceTypeMeta: {FeatureBasic, FeatureLongLiving}, +} + // Feature is the feature supported by the channel. type Feature string const ( - // FeatureBasic is the feature that should be supported by all channels. + // FeatureBasic is the feature that should be supported by all components. FeatureBasic Feature = "basic" // FeatureRedelivery means if downstream rejects an event, that request will be attempted again. FeatureRedelivery Feature = "redelivery" // FeaturePersistence means if channel's Pod goes down, all events already ACKed by the channel // will persist and be retransmitted when the Pod restarts. FeaturePersistence Feature = "persistence" + // A long living component + FeatureLongLiving Feature = "longliving" + // A batch style of components that run once and complete + FeatureBatch Feature = "batch" ) const ( diff --git a/test/lib/resources/constants.go b/test/lib/resources/constants.go index fbe36905d40..fd635e7893c 100644 --- a/test/lib/resources/constants.go +++ b/test/lib/resources/constants.go @@ -28,6 +28,7 @@ const ( MessagingAPIVersion = "messaging.knative.dev/v1beta1" FlowsAPIVersion = "flows.knative.dev/v1beta1" ServingAPIVersion = "serving.knative.dev/v1" + SourcesAPIVersion = "sources.knative.dev/v1beta1" ) // Kind for Knative resources. @@ -76,3 +77,9 @@ const ( FlowsSequenceKind string = "Sequence" FlowsParallelKind string = "Parallel" ) + +//Kind for sources resources that exist in Eventing core +const( + ApiServerSourceKind string = "ApiServerSource" + PingSourceKind string = "PingSource" +) diff --git a/test/lib/setupclientoptions/sources.go b/test/lib/setupclientoptions/sources.go new file mode 100644 index 00000000000..9c420477866 --- /dev/null +++ b/test/lib/setupclientoptions/sources.go @@ -0,0 +1,85 @@ +/* +Copyright 2020 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 setupclientoptions + +import ( + "fmt" + rbacv1 "k8s.io/api/rbac/v1" + sourcesv1beta1 "knative.dev/eventing/pkg/apis/sources/v1beta1" + eventingtesting "knative.dev/eventing/pkg/reconciler/testing" + testlib "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/lib/recordevents" + "knative.dev/eventing/test/lib/resources" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +// ApiServerSourceClientSetupOption returns a ClientSetupOption that can be used +// to create a new ApiServerSource. It creates a ServiceAccount, a Role, a +// RoleBinding, a RecordEvents pod and an ApiServerSource object with the event +// mode and RecordEvent pod as its sink. +func ApiServerSourceClientSetupOption(name string, mode string, recordEventsPodName string, + roleName string, serviceAccountName string) testlib.SetupClientOption{ + return func(client *testlib.Client) { + // create needed RBAC SA, Role & RoleBinding + createRbacObjects(client, roleName, serviceAccountName) + + // create event record + recordevents.StartEventRecordOrFail(client, + recordEventsPodName) + + spec := sourcesv1beta1.ApiServerSourceSpec{ + Resources: []sourcesv1beta1.APIVersionKindSelector{{ + APIVersion: "v1", + Kind: "Event", + }}, + EventMode: mode, + ServiceAccountName: serviceAccountName, + } + spec.Sink = duckv1.Destination{Ref: resources.ServiceKRef(recordEventsPodName)} + + apiServerSource := eventingtesting.NewApiServerSourceV1Beta1( + name, + client.Namespace, + eventingtesting.WithApiServerSourceSpecV1B1(spec), + ) + + client.CreateApiServerSourceV1Beta1OrFail(apiServerSource) + + // wait for all test resources to be ready + client.WaitForAllTestResourcesReadyOrFail() + } +} + +func createRbacObjects(client *testlib.Client, roleName string, + serviceAccountName string) { + // creates ServiceAccount and RoleBinding with a role for reading pods + // and events + r := resources.Role(roleName, + resources.WithRuleForRole(&rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"events", "pods"}, + Verbs: []string{"get", "list", "watch"}})) + client.CreateServiceAccountOrFail(serviceAccountName) + client.CreateRoleOrFail(r) + client.CreateRoleBindingOrFail( + serviceAccountName, + testlib.RoleKind, + roleName, + fmt.Sprintf("%s-%s", serviceAccountName, roleName), + client.Namespace, + ) +} diff --git a/test/lib/test_runner.go b/test/lib/test_runner.go index 7fd86ed0f5d..e63ef2fd0e0 100644 --- a/test/lib/test_runner.go +++ b/test/lib/test_runner.go @@ -49,6 +49,7 @@ const ( type ComponentsTestRunner struct { ComponentFeatureMap map[metav1.TypeMeta][]Feature ComponentsToTest []metav1.TypeMeta + componentOptions map[metav1.TypeMeta][]SetupClientOption } // RunTests will use all components that support the given feature, to run @@ -73,6 +74,48 @@ func (tr *ComponentsTestRunner) RunTests( } } +// RunTestsWithComponentOptions will use all components that support the given +// feature, to run a test for the testFunc while passing the component specific +// SetupClientOptions to testFunc. You should used this method instead of +// RunTests if you have used AddComponentSetupClientOption to add some component +// specific initialization code. If strict is set to true, tests will not run +// for components that don't exist in the ComponentFeatureMap. +func (tr *ComponentsTestRunner) RunTestsWithComponentOptions( + t *testing.T, + feature Feature, + strict bool, + testFunc func(st *testing.T, component metav1.TypeMeta, + options ...SetupClientOption), +) { + t.Parallel() + for _, component := range tr.ComponentsToTest { + // If in strict mode and a component is not present in the map, then + // don't run the tests + features, present := tr.ComponentFeatureMap[component] + if !strict || ( present && contains(features, feature)) { + t.Run(fmt.Sprintf("%s-%s", component.Kind, component.APIVersion), func(st *testing.T) { + testFunc(st, component, tr.componentOptions[component]...) + }) + } + } +} + +// AddComponentSetupClientOption adds a SetupClientOption that should only run when +// component gets selected to run. This should be used when there's an expensive +// initialization code should take place conditionally (e.g. create an instance +// of a source or a channel) as opposed to other cheap initialization code that +// is safe to be called in all cases (e.g. installation of a CRD) +func (tr *ComponentsTestRunner) AddComponentSetupClientOption(component metav1.TypeMeta, + options ...SetupClientOption){ + if tr.componentOptions == nil { + tr.componentOptions = make(map[metav1.TypeMeta][]SetupClientOption) + } + if _, ok := tr.componentOptions[component]; !ok { + tr.componentOptions[component] = make([]SetupClientOption, 0) + } + tr.componentOptions[component] = append(tr.componentOptions[component], options...) +} + func contains(features []Feature, feature Feature) bool { for _, f := range features { if f == feature {