diff --git a/go.mod b/go.mod index dd999ff5d..cb1c871ce 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/cloudevents/sdk-go/v2 v2.8.0 github.com/ghodss/yaml v1.0.0 + github.com/google/go-containerregistry v0.8.1-0.20220120151853-ac864e57b117 github.com/google/uuid v1.3.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/magefile/mage v1.11.0 diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index bb3cd4586..b73281b06 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -28,6 +28,6 @@ set -Eeuo pipefail ./mage publish -go_test_e2e ./test/... +go_test_e2e -timeout 10m ./test/... || fail_test 'kn-event e2e tests' success diff --git a/test/e2e/ics_send_test.go b/test/e2e/ics_send_test.go index b892e17a0..f57e946be 100644 --- a/test/e2e/ics_send_test.go +++ b/test/e2e/ics_send_test.go @@ -16,7 +16,7 @@ import ( func TestInClusterSender(t *testing.T) { test.MaybeSkip(t) - e2e.RegisterPackages() + e2e.ConfigureImages(t) t.Parallel() diff --git a/test/e2e/images.go b/test/e2e/images.go new file mode 100644 index 000000000..bac7eef75 --- /dev/null +++ b/test/e2e/images.go @@ -0,0 +1,18 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "knative.dev/kn-plugin-event/test/images" + "knative.dev/reconciler-test/pkg/environment" +) + +// ConfigureImages will register packages to be built into test images. +func ConfigureImages(t images.TestingT) { + environment.RegisterPackage(watholaForwarderPackage) + images.ResolveImages(t, []string{ + "knative.dev/reconciler-test/cmd/eventshub", + watholaForwarderPackage, + }) +} diff --git a/test/e2e/kn_service.go b/test/e2e/kn_service.go index e5b6ad9cd..68a67ac45 100644 --- a/test/e2e/kn_service.go +++ b/test/e2e/kn_service.go @@ -29,11 +29,6 @@ import ( const watholaForwarderPackage = "knative.dev/eventing/test/test_images/wathola-forwarder" -// RegisterPackages will register packages to be built into test images. -func RegisterPackages() { - environment.RegisterPackage(watholaForwarderPackage) -} - // SendEventToKnService returns a feature.Feature that verifies the kn-event // can send to Knative service. func SendEventToKnService() *feature.Feature { diff --git a/test/images/envbased.go b/test/images/envbased.go new file mode 100644 index 000000000..cec2f190b --- /dev/null +++ b/test/images/envbased.go @@ -0,0 +1,66 @@ +package images + +import ( + "errors" + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/google/go-containerregistry/pkg/name" +) + +var ( + // ErrNotFound is returned when there is no environment variable set for + // given KO path. + ErrNotFound = errors.New("expected environment variable not found") + nonAlphaNumeric = regexp.MustCompile("[^A-Z0-9]+") +) + +// EnvironmentalBasedResolver will try to resolve the images from prefixed +// environment variables. +type EnvironmentalBasedResolver struct { + Prefix string +} + +func (c *EnvironmentalBasedResolver) Applicable() bool { + prefix := c.normalizedPrefix() + for _, environment := range os.Environ() { + const equalitySignParts = 2 + parts := strings.SplitN(environment, "=", equalitySignParts) + key := parts[0] + if strings.HasPrefix(key, prefix) { + return true + } + } + return false +} + +func (c *EnvironmentalBasedResolver) Resolve(kopath string) (name.Reference, error) { + prefix := c.normalizedPrefix() + shortName := normalize(path.Base(kopath)) + key := fmt.Sprintf("%s_%s", prefix, shortName) + val, ok := os.LookupEnv(key) + if !ok { + return nil, fmt.Errorf("%w: '%s' - kopath: %s", ErrNotFound, key, kopath) + } + ref, err := name.ParseReference(val) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrNotFound, err) + } + return ref, nil +} + +func (c *EnvironmentalBasedResolver) normalizedPrefix() string { + return normalize(c.Prefix) +} + +func normalize(in string) string { + return strings.Trim( + nonAlphaNumeric.ReplaceAllString(strings.ToUpper(in), "_"), + "_", + ) +} + +var _ Resolver = &EnvironmentalBasedResolver{} diff --git a/test/images/register.go b/test/images/register.go new file mode 100644 index 000000000..f7696469b --- /dev/null +++ b/test/images/register.go @@ -0,0 +1,5 @@ +package images + +// Resolvers an array of resolvers to which resolvers could be added for +// downstream projects. +var Resolvers = make([]Resolver, 0, 1) //nolint:gochecknoglobals diff --git a/test/images/resolver.go b/test/images/resolver.go new file mode 100644 index 000000000..04ffefcee --- /dev/null +++ b/test/images/resolver.go @@ -0,0 +1,56 @@ +package images + +import ( + "github.com/google/go-containerregistry/pkg/name" + "k8s.io/apimachinery/pkg/util/json" + "knative.dev/reconciler-test/pkg/environment" +) + +// Resolver will resolve given KO package paths into real OCI images references. +// This interface probably should be moved into reconciler-test framework. See: +// https://github.com/knative-sandbox/reconciler-test/issues/303 +type Resolver interface { + // Resolve will resolve given KO package path into real OCI image reference. + Resolve(kopath string) (name.Reference, error) + // Applicable will tell that given resolver is applicable to current runtime + // environment, or not. + Applicable() bool +} + +// TestingT a subset of testing.T. +type TestingT interface { + Logf(fmt string, args ...interface{}) + Fatal(args ...interface{}) + Fatalf(fmt string, args ...interface{}) +} + +// ResolveImages will try to resolve the images, using given resolver(s). +func ResolveImages(t TestingT, packages []string) { + for _, resolver := range Resolvers { + if resolver.Applicable() { + resolveImagesWithResolver(t, packages, resolver) + return + } + } + if len(Resolvers) > 0 { + t.Fatalf("Couldn't resolve images with registered resolvers: %+q", Resolvers) + } +} + +func resolveImagesWithResolver(t TestingT, packages []string, resolver Resolver) { + resolved := make(map[string]string) + for _, pack := range packages { + kopath := "ko://" + pack + image, err := resolver.Resolve(kopath) + if err != nil { + t.Fatal(err) + } + resolved[kopath] = image.String() + } + repr, err := json.Marshal(resolved) + if err != nil { + t.Fatal(err) + } + t.Logf("Images resolved to: %s", string(repr)) + environment.WithImages(resolved) +} diff --git a/test/pkg/clients.go b/test/pkg/clients.go index b8cff0bba..66331a6e4 100644 --- a/test/pkg/clients.go +++ b/test/pkg/clients.go @@ -49,6 +49,11 @@ func WithKnTest(tb testing.TB, handler func(c *TestContext)) { it, err := clienttest.NewKnTest() assert.NilError(tb, err) tb.Cleanup(func() { + if tb.Failed() { + tb.Logf("Skipping '%s' namespace teardown because '%s' test is failing", + it.Namespace(), tb.Name()) + return + } assert.NilError(tb, it.Teardown()) }) handler(&TestContext{ diff --git a/test/pkg/k8s/addressresolver_test.go b/test/pkg/k8s/addressresolver_test.go index fd1d9dd6e..3f3278a47 100644 --- a/test/pkg/k8s/addressresolver_test.go +++ b/test/pkg/k8s/addressresolver_test.go @@ -5,17 +5,17 @@ package k8s_test import ( "context" - "fmt" "testing" "time" "gotest.tools/v3/assert" + "gotest.tools/v3/poll" 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/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" clienteventingv1 "knative.dev/client/pkg/eventing/v1" clientmessagingv1 "knative.dev/client/pkg/messaging/v1" clientservingv1 "knative.dev/client/pkg/serving/v1" @@ -38,8 +38,11 @@ func TestResolveAddress(t *testing.T) { t.Parallel() k8stest.EnsureResolveAddress(t, tc, func() (k8s.Clients, func(tb testing.TB)) { deploy(t, tc, c.Clients) - cleanup := func(tb testing.TB) { - tb.Helper() + cleanup := func(tb testing.TB) { // nolint:thelper + if tb.Failed() { + tb.Logf("Skipping undeploy, because test '%s' failed", tb.Name()) + return + } undeploy(tb, tc, c.Clients) } return c.Clients, cleanup @@ -49,8 +52,7 @@ func TestResolveAddress(t *testing.T) { }) } -func deploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Clients) { - tb.Helper() +func deploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Clients) { // nolint:thelper for _, object := range tc.Objects { switch v := object.(type) { case *servingv1.Service: @@ -67,8 +69,7 @@ func deploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Client } } -func undeploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Clients) { - tb.Helper() +func undeploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Clients) { // nolint:thelper for _, object := range tc.Objects { switch v := object.(type) { case *servingv1.Service: @@ -85,23 +86,20 @@ func undeploy(tb testing.TB, tc k8stest.ResolveAddressTestCase, clients k8s.Clie } } -func deployK8sService(tb testing.TB, clients k8s.Clients, service corev1.Service) { - tb.Helper() +func deployK8sService(tb testing.TB, clients k8s.Clients, service corev1.Service) { // nolint:thelper service.Status = corev1.ServiceStatus{} _, err := clients.Typed().CoreV1().Services(service.Namespace). Create(clients.Context(), &service, metav1.CreateOptions{}) assert.NilError(tb, err) } -func undeployK8sService(tb testing.TB, clients k8s.Clients, service corev1.Service) { - tb.Helper() +func undeployK8sService(tb testing.TB, clients k8s.Clients, service corev1.Service) { // nolint:thelper err := clients.Typed().CoreV1().Services(service.Namespace). Delete(clients.Context(), service.Name, metav1.DeleteOptions{}) assert.NilError(tb, err) } -func deployKnService(tb testing.TB, clients k8s.Clients, service servingv1.Service) { - tb.Helper() +func deployKnService(tb testing.TB, clients k8s.Clients, service servingv1.Service) { // nolint:thelper service.Status = servingv1.ServiceStatus{} ctx := clients.Context() knclient := clientservingv1.NewKnServingClient(clients.Serving(), service.Namespace) @@ -112,81 +110,85 @@ func deployKnService(tb testing.TB, clients k8s.Clients, service servingv1.Servi assert.NilError(tb, err) } -func undeployKnService(tb testing.TB, clients k8s.Clients, service servingv1.Service) { - tb.Helper() +func undeployKnService(tb testing.TB, clients k8s.Clients, service servingv1.Service) { // nolint:thelper err := clientservingv1. NewKnServingClient(clients.Serving(), service.Namespace). DeleteService(clients.Context(), service.GetName(), time.Minute) assert.NilError(tb, err) } -func deployBroker(tb testing.TB, clients k8s.Clients, broker eventingv1.Broker) { - tb.Helper() +func deployBroker(tb testing.TB, clients k8s.Clients, broker eventingv1.Broker) { // nolint:thelper broker.Status = eventingv1.BrokerStatus{} ctx := clients.Context() knclient := clienteventingv1.NewKnEventingClient(clients.Eventing(), broker.Namespace) assert.NilError(tb, knclient.CreateBroker(ctx, &broker)) - assert.NilError(tb, waitForReady(clients, &broker, 30*time.Second)) + waitForReady(tb, clients, &broker, time.Minute) } -func undeployBroker(tb testing.TB, clients k8s.Clients, broker eventingv1.Broker) { - tb.Helper() +func undeployBroker(tb testing.TB, clients k8s.Clients, broker eventingv1.Broker) { // nolint:thelper err := clients.Eventing().Brokers(broker.Namespace). Delete(clients.Context(), broker.Name, metav1.DeleteOptions{}) assert.NilError(tb, err) } -func deployChannel(tb testing.TB, clients k8s.Clients, channel messagingv1.Channel) { - tb.Helper() +func deployChannel(tb testing.TB, clients k8s.Clients, channel messagingv1.Channel) { // nolint:thelper channel.Status = messagingv1.ChannelStatus{} knclient := clientmessagingv1.NewKnMessagingClient(clients.Messaging(), channel.Namespace).ChannelsClient() assert.NilError(tb, knclient.CreateChannel(clients.Context(), &channel)) - assert.NilError(tb, waitForReady(clients, &channel, 30*time.Second)) + waitForReady(tb, clients, &channel, time.Minute) } -func undeployChannel(tb testing.TB, clients k8s.Clients, channel messagingv1.Channel) { - tb.Helper() +func undeployChannel(tb testing.TB, clients k8s.Clients, channel messagingv1.Channel) { // nolint:thelper err := clients.Messaging().Channels(channel.Namespace). Delete(clients.Context(), channel.Name, metav1.DeleteOptions{}) assert.NilError(tb, err) } -type accessorWatchMaker struct { - clients k8s.Clients - acccessor kmeta.Accessor -} - -func (a accessorWatchMaker) watchMaker( - ctx context.Context, name, _ string, _ time.Duration, -) (watch.Interface, error) { - dynclient := a.clients.Dynamic().Resource(a.gvr()). - Namespace(a.acccessor.GetNamespace()) - return dynclient.Watch(ctx, metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - }) -} - -func (a accessorWatchMaker) gvr() schema.GroupVersionResource { - gvk := a.acccessor.GroupVersionKind() +func gvr(accessor kmeta.Accessor) schema.GroupVersionResource { + gvk := accessor.GroupVersionKind() return apis.KindToResource(gvk) } -func waitForReady(clients k8s.Clients, acccessor kmeta.Accessor, timeout time.Duration) error { - awm := accessorWatchMaker{clients: clients, acccessor: acccessor} - ready := clientwait.NewWaitForReady(awm.gvr().Resource, awm.watchMaker, dynamicConditionExtractor) - err, _ := ready.Wait( - clients.Context(), - acccessor.GetName(), - "0", - clientwait.Options{Timeout: &timeout}, - clientwait.NoopMessageCallback(), - ) - return err +func waitForReady(t poll.TestingT, clients k8s.Clients, accessor kmeta.Accessor, timeout time.Duration) { + ctx := clients.Context() + dynclient := clients.Dynamic() + poll.WaitOn(t, isReady(ctx, dynclient, accessor), + poll.WithTimeout(timeout), poll.WithDelay(time.Second)) +} + +func isReady(ctx context.Context, dynclient dynamic.Interface, accessor kmeta.Accessor) poll.Check { + resources := dynclient.Resource(gvr(accessor)). + Namespace(accessor.GetNamespace()) + return func(t poll.LogT) poll.Result { + res, err := resources.Get(ctx, accessor.GetName(), metav1.GetOptions{}) + if err != nil { + return poll.Error(err) + } + kres, err := toKResource(res) + if err != nil { + return poll.Error(err) + } + for _, cond := range kres.Status.Conditions { + if cond.Type == apis.ConditionReady { + if cond.Status == corev1.ConditionTrue { + return poll.Success() + } + return poll.Continue( + "%s named '%s' in namespace '%s' is not ready '%s', reason '%s'", + accessor.GroupVersionKind(), accessor.GetName(), + accessor.GetNamespace(), cond.Status, cond.Reason) + } + } + return poll.Continue( + "%s named '%s' in namespace '%s' does not have ready condition", + accessor.GroupVersionKind(), accessor.GetName(), + accessor.GetNamespace()) + } } -func dynamicConditionExtractor(obj runtime.Object) (apis.Conditions, error) { +func toKResource(obj runtime.Object) (*duckv1.KResource, error) { un, ok := obj.(*unstructured.Unstructured) if !ok { return nil, k8s.ErrUnexcpected @@ -197,5 +199,5 @@ func dynamicConditionExtractor(obj runtime.Object) (apis.Conditions, error) { if err != nil { return nil, err } - return kresource.GetStatus().GetConditions(), nil + return &kresource, nil } diff --git a/vendor/gotest.tools/v3/poll/check.go b/vendor/gotest.tools/v3/poll/check.go new file mode 100644 index 000000000..060b09989 --- /dev/null +++ b/vendor/gotest.tools/v3/poll/check.go @@ -0,0 +1,39 @@ +package poll + +import ( + "net" + "os" +) + +// Check is a function which will be used as check for the WaitOn method. +type Check func(t LogT) Result + +// FileExists looks on filesystem and check that path exists. +func FileExists(path string) Check { + return func(t LogT) Result { + _, err := os.Stat(path) + if os.IsNotExist(err) { + t.Logf("waiting on file %s to exist", path) + return Continue("file %s does not exist", path) + } + if err != nil { + return Error(err) + } + + return Success() + } +} + +// Connection try to open a connection to the address on the +// named network. See net.Dial for a description of the network and +// address parameters. +func Connection(network, address string) Check { + return func(t LogT) Result { + _, err := net.Dial(network, address) + if err != nil { + t.Logf("waiting on socket %s://%s to be available...", network, address) + return Continue("socket %s://%s not available", network, address) + } + return Success() + } +} diff --git a/vendor/gotest.tools/v3/poll/poll.go b/vendor/gotest.tools/v3/poll/poll.go new file mode 100644 index 000000000..29c5b40e1 --- /dev/null +++ b/vendor/gotest.tools/v3/poll/poll.go @@ -0,0 +1,171 @@ +/*Package poll provides tools for testing asynchronous code. + */ +package poll // import "gotest.tools/v3/poll" + +import ( + "fmt" + "strings" + "time" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/assert" +) + +// TestingT is the subset of testing.T used by WaitOn +type TestingT interface { + LogT + Fatalf(format string, args ...interface{}) +} + +// LogT is a logging interface that is passed to the WaitOn check function +type LogT interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) +} + +type helperT interface { + Helper() +} + +// Settings are used to configure the behaviour of WaitOn +type Settings struct { + // Timeout is the maximum time to wait for the condition. Defaults to 10s. + Timeout time.Duration + // Delay is the time to sleep between checking the condition. Defaults to + // 100ms. + Delay time.Duration +} + +func defaultConfig() *Settings { + return &Settings{Timeout: 10 * time.Second, Delay: 100 * time.Millisecond} +} + +// SettingOp is a function which accepts and modifies Settings +type SettingOp func(config *Settings) + +// WithDelay sets the delay to wait between polls +func WithDelay(delay time.Duration) SettingOp { + return func(config *Settings) { + config.Delay = delay + } +} + +// WithTimeout sets the timeout +func WithTimeout(timeout time.Duration) SettingOp { + return func(config *Settings) { + config.Timeout = timeout + } +} + +// Result of a check performed by WaitOn +type Result interface { + // Error indicates that the check failed and polling should stop, and the + // the has failed + Error() error + // Done indicates that polling should stop, and the test should proceed + Done() bool + // Message provides the most recent state when polling has not completed + Message() string +} + +type result struct { + done bool + message string + err error +} + +func (r result) Done() bool { + return r.done +} + +func (r result) Message() string { + return r.message +} + +func (r result) Error() error { + return r.err +} + +// Continue returns a Result that indicates to WaitOn that it should continue +// polling. The message text will be used as the failure message if the timeout +// is reached. +func Continue(message string, args ...interface{}) Result { + return result{message: fmt.Sprintf(message, args...)} +} + +// Success returns a Result where Done() returns true, which indicates to WaitOn +// that it should stop polling and exit without an error. +func Success() Result { + return result{done: true} +} + +// Error returns a Result that indicates to WaitOn that it should fail the test +// and stop polling. +func Error(err error) Result { + return result{err: err} +} + +// WaitOn a condition or until a timeout. Poll by calling check and exit when +// check returns a done Result. To fail a test and exit polling with an error +// return a error result. +func WaitOn(t TestingT, check Check, pollOps ...SettingOp) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + config := defaultConfig() + for _, pollOp := range pollOps { + pollOp(config) + } + + var lastMessage string + after := time.After(config.Timeout) + chResult := make(chan Result) + for { + go func() { + chResult <- check(t) + }() + select { + case <-after: + if lastMessage == "" { + lastMessage = "first check never completed" + } + t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage) + case result := <-chResult: + switch { + case result.Error() != nil: + t.Fatalf("polling check failed: %s", result.Error()) + case result.Done(): + return + } + time.Sleep(config.Delay) + lastMessage = result.Message() + } + } +} + +// Compare values using the cmp.Comparison. If the comparison fails return a +// result which indicates to WaitOn that it should continue waiting. +// If the comparison is successful then WaitOn stops polling. +func Compare(compare cmp.Comparison) Result { + buf := new(logBuffer) + if assert.RunComparison(buf, assert.ArgsAtZeroIndex, compare) { + return Success() + } + return Continue(buf.String()) +} + +type logBuffer struct { + log [][]interface{} +} + +func (c *logBuffer) Log(args ...interface{}) { + c.log = append(c.log, args) +} + +func (c *logBuffer) String() string { + b := new(strings.Builder) + for _, item := range c.log { + b.WriteString(fmt.Sprint(item...) + " ") + } + return b.String() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e037ff190..ddd9218cc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -179,6 +179,7 @@ github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function github.com/google/go-cmp/cmp/internal/value # github.com/google/go-containerregistry v0.8.1-0.20220120151853-ac864e57b117 +## explicit github.com/google/go-containerregistry/cmd/crane/cmd github.com/google/go-containerregistry/internal/and github.com/google/go-containerregistry/internal/estargz @@ -645,6 +646,7 @@ gotest.tools/v3/internal/assert gotest.tools/v3/internal/difflib gotest.tools/v3/internal/format gotest.tools/v3/internal/source +gotest.tools/v3/poll # k8s.io/api v0.22.5 ## explicit k8s.io/api/admissionregistration/v1