feat(wait): add wait for delete operation (#682)

* feat(wait): add wait for delete operation

* feat(wait): refactor initial impl

* fix(lint): add docs to new functions

* fix: reflect code review comments

* feat(wait): add changelog entry

* fix: changelog entry to render correctly

* fix: usage message of wait flag

* feat(wait): set foreground propagationPolicy in sync service delete

* feat(wait): add sync revision delete operation

* Revert "feat(wait): add sync revision delete operation"

This reverts commit 9bb60ac25e.
This commit is contained in:
David Simansky 2020-02-25 11:38:50 +01:00 committed by GitHub
parent a44833c849
commit 345653bfe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 224 additions and 34 deletions

View File

@ -51,6 +51,10 @@
| 🐛
| Fix `--image` flag to only allow single occurence
| https://github.com/knative/client/pull/647[#647]
| 🧽
| Add `--wait` and `--no-wait` to service delete operation. Change service delete to wait by default.
| https://github.com/knative/client/pull/682[#682]
|===
## v0.12.0 (2020-01-29)

View File

@ -44,7 +44,7 @@ kn service create NAME --image IMAGE [flags]
```
--annotation stringArray Service annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-).
--arg stringArray Add argument to the container command. Example: --arg myArg1 --arg --myArg2 --arg myArg3=3. You can use this flag multiple times.
--async DEPRECATED: please use --no-wait instead. Create service and don't wait for it to become ready.
--async DEPRECATED: please use --no-wait instead. Create service and don't wait for it to be ready.
--autoscale-window string Duration to look back for making auto-scaling decisions. The service is scaled to zero if no request was received in during that time. (eg: 10s)
--cmd string Specify command to be used as entrypoint instead of default one. Example: --cmd /app/start or --cmd /app/start --arg myArg to pass aditional arguments.
--concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica.
@ -63,7 +63,7 @@ kn service create NAME --image IMAGE [flags]
--mount stringArray Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
-n, --namespace string Specify the namespace to operate in.
--no-lock-to-digest Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
--no-wait Create service and don't wait for it to become ready.
--no-wait Create service and don't wait for it to be ready.
-p, --port int32 The port where application listens on.
--pull-secret string Image pull secret to set. An empty argument ("") clears the pull secret. The referenced secret must exist in the service's namespace.
--requests-cpu string The requested CPU (e.g., 250m).

View File

@ -24,8 +24,11 @@ kn service delete NAME [flags]
### Options
```
--async DEPRECATED: please use --no-wait instead. Delete service and don't wait for it to be deleted.
-h, --help help for delete
-n, --namespace string Specify the namespace to operate in.
--no-wait Delete service and don't wait for it to be deleted.
--wait-timeout int Seconds to wait before giving up on waiting for service to be deleted. (default 600)
```
### Options inherited from parent commands

View File

@ -40,7 +40,7 @@ kn service update NAME [flags]
```
--annotation stringArray Service annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-).
--arg stringArray Add argument to the container command. Example: --arg myArg1 --arg --myArg2 --arg myArg3=3. You can use this flag multiple times.
--async DEPRECATED: please use --no-wait instead. Update service and don't wait for it to become ready.
--async DEPRECATED: please use --no-wait instead. Update service and don't wait for it to be ready.
--autoscale-window string Duration to look back for making auto-scaling decisions. The service is scaled to zero if no request was received in during that time. (eg: 10s)
--cmd string Specify command to be used as entrypoint instead of default one. Example: --cmd /app/start or --cmd /app/start --arg myArg to pass aditional arguments.
--concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica.
@ -58,7 +58,7 @@ kn service update NAME [flags]
--mount stringArray Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
-n, --namespace string Specify the namespace to operate in.
--no-lock-to-digest Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
--no-wait Update service and don't wait for it to become ready.
--no-wait Update service and don't wait for it to be ready.
-p, --port int32 The port where application listens on.
--pull-secret string Image pull secret to set. An empty argument ("") clears the pull secret. The referenced secret must exist in the service's namespace.
--requests-cpu string The requested CPU (e.g., 250m).

View File

@ -114,7 +114,7 @@ func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {
}
commands.AddNamespaceFlags(serviceCreateCommand.Flags(), false)
editFlags.AddCreateFlags(serviceCreateCommand)
waitFlags.AddConditionWaitFlags(serviceCreateCommand, commands.WaitDefaultTimeout, "Create", "service")
waitFlags.AddConditionWaitFlags(serviceCreateCommand, commands.WaitDefaultTimeout, "Create", "service", "ready")
return serviceCreateCommand
}

View File

@ -17,6 +17,7 @@ package service
import (
"errors"
"fmt"
"time"
"github.com/spf13/cobra"
@ -25,6 +26,8 @@ import (
// NewServiceDeleteCommand represent 'service delete' command
func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
var waitFlags commands.WaitFlags
serviceDeleteCommand := &cobra.Command{
Use: "delete NAME",
Short: "Delete a service.",
@ -48,9 +51,12 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
if err != nil {
return err
}
for _, name := range args {
err = client.DeleteService(name)
timeout := time.Duration(0)
if !waitFlags.NoWait {
timeout = time.Duration(waitFlags.TimeoutInSeconds) * time.Second
}
err = client.DeleteService(name, timeout)
if err != nil {
fmt.Fprintf(cmd.OutOrStdout(), "%s.\n", err)
} else {
@ -61,5 +67,6 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
},
}
commands.AddNamespaceFlags(serviceDeleteCommand.Flags(), false)
waitFlags.AddConditionWaitFlags(serviceDeleteCommand, commands.WaitDefaultTimeout, "Delete", "service", "deleted")
return serviceDeleteCommand
}

View File

@ -21,6 +21,7 @@ import (
clientservingv1 "knative.dev/client/pkg/serving/v1"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/util/mock"
)
func TestServiceDeleteMock(t *testing.T) {
@ -30,7 +31,7 @@ func TestServiceDeleteMock(t *testing.T) {
// Recording:
r := client.Recorder()
r.DeleteService("foo", nil)
r.DeleteService("foo", mock.Any(), nil)
output, err := executeServiceCommand(client, "delete", "foo")
assert.NilError(t, err)
@ -40,16 +41,34 @@ func TestServiceDeleteMock(t *testing.T) {
}
func TestServiceDeleteMockNoWait(t *testing.T) {
// New mock client
client := clientservingv1.NewMockKnServiceClient(t)
// Recording:
r := client.Recorder()
r.DeleteService("foo", mock.Any(), nil)
output, err := executeServiceCommand(client, "delete", "foo", "--no-wait")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "deleted", "foo", "default"))
r.Validate()
}
func TestMultipleServiceDeleteMock(t *testing.T) {
// New mock client
client := clientservingv1.NewMockKnServiceClient(t)
// Recording:
r := client.Recorder()
// Wait for delete event
r.DeleteService("foo", nil)
r.DeleteService("bar", nil)
r.DeleteService("baz", nil)
r.DeleteService("foo", mock.Any(), nil)
r.DeleteService("bar", mock.Any(), nil)
r.DeleteService("baz", mock.Any(), nil)
output, err := executeServiceCommand(client, "delete", "foo", "bar", "baz")
assert.NilError(t, err)

View File

@ -17,12 +17,17 @@ package service
import (
"testing"
"github.com/pkg/errors"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
client_testing "k8s.io/client-go/testing"
clienttesting "k8s.io/client-go/testing"
"knative.dev/client/pkg/kn/commands"
"knative.dev/client/pkg/util"
"knative.dev/client/pkg/wait"
)
func fakeServiceDelete(args []string) (action client_testing.Action, name string, output string, err error) {
@ -35,6 +40,17 @@ func fakeServiceDelete(args []string) (action client_testing.Action, name string
name = deleteAction.GetName()
return true, nil, nil
})
fakeServing.AddWatchReactor("services",
func(a client_testing.Action) (bool, watch.Interface, error) {
watchAction := a.(clienttesting.WatchAction)
_, found := watchAction.GetWatchRestrictions().Fields.RequiresExactMatch("metadata.name")
if !found {
return true, nil, errors.New("no field selector on metadata.name found")
}
w := wait.NewFakeWatch(getServiceDeleteEvents("test-service"))
w.Start()
return true, w, nil
})
cmd.SetArgs(args)
err = cmd.Execute()
if err != nil {
@ -79,3 +95,11 @@ func TestMultipleServiceDelete(t *testing.T) {
assert.Check(t, util.ContainsAll(output, "Service", sevName2, "deleted", "namespace", commands.FakeNamespace))
assert.Check(t, util.ContainsAll(output, "Service", sevName3, "deleted", "namespace", commands.FakeNamespace))
}
func getServiceDeleteEvents(name string) []watch.Event {
return []watch.Event{
{watch.Added, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", "msg1")},
{watch.Modified, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionTrue, "", "msg2")},
{watch.Deleted, wait.CreateTestServiceWithConditions(name, corev1.ConditionTrue, corev1.ConditionTrue, "", "")},
}
}

View File

@ -136,7 +136,7 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
commands.AddNamespaceFlags(serviceUpdateCommand.Flags(), false)
editFlags.AddUpdateFlags(serviceUpdateCommand)
waitFlags.AddConditionWaitFlags(serviceUpdateCommand, commands.WaitDefaultTimeout, "Update", "service")
waitFlags.AddConditionWaitFlags(serviceUpdateCommand, commands.WaitDefaultTimeout, "Update", "service", "ready")
trafficFlags.Add(serviceUpdateCommand)
return serviceUpdateCommand
}

View File

@ -39,12 +39,12 @@ type WaitFlags struct {
// Add flags which influence the sync/async behaviour when creating or updating
// resources. Set `waitDefault` argument if the default behaviour is synchronous.
// Use `what` for describing what is waited for.
func (p *WaitFlags) AddConditionWaitFlags(command *cobra.Command, waitTimeoutDefault int, action string, what string) {
waitUsage := fmt.Sprintf("%s %s and don't wait for it to become ready.", action, what)
func (p *WaitFlags) AddConditionWaitFlags(command *cobra.Command, waitTimeoutDefault int, action, what, until string) {
waitUsage := fmt.Sprintf("%s %s and don't wait for it to be %s.", action, what, until)
//TODO: deprecated flag should be removed in next release
command.Flags().BoolVar(&p.Async, "async", false, "DEPRECATED: please use --no-wait instead. "+waitUsage)
command.Flags().BoolVar(&p.NoWait, "no-wait", false, waitUsage)
timeoutUsage := fmt.Sprintf("Seconds to wait before giving up on waiting for %s to be ready.", what)
timeoutUsage := fmt.Sprintf("Seconds to wait before giving up on waiting for %s to be %s.", what, until)
command.Flags().IntVar(&p.TimeoutInSeconds, "wait-timeout", waitTimeoutDefault, timeoutUsage)
}

View File

@ -42,7 +42,7 @@ func TestAddWaitForReadyDeprecatedFlags(t *testing.T) {
flags := &WaitFlags{}
cmd := cobra.Command{}
flags.AddConditionWaitFlags(&cmd, 60, "Create", "service")
flags.AddConditionWaitFlags(&cmd, 60, "Create", "service", "ready")
err := cmd.ParseFlags(tc.args)
if err != nil && !tc.isParseErrorExpected {
@ -76,7 +76,7 @@ func TestAddWaitForReadyFlags(t *testing.T) {
flags := &WaitFlags{}
cmd := cobra.Command{}
flags.AddConditionWaitFlags(&cmd, 60, "Create", "service")
flags.AddConditionWaitFlags(&cmd, 60, "Create", "service", "ready")
err := cmd.ParseFlags(tc.args)
if err != nil && !tc.isParseErrorExpected {
@ -101,7 +101,7 @@ func TestAddWaitUsageMessage(t *testing.T) {
flags := &WaitFlags{}
cmd := cobra.Command{}
flags.AddConditionWaitFlags(&cmd, 60, "bla", "blub")
flags.AddConditionWaitFlags(&cmd, 60, "bla", "blub", "deleted")
if !strings.Contains(cmd.UsageString(), "blub") {
t.Error("no type returned in usage")
}
@ -111,4 +111,7 @@ func TestAddWaitUsageMessage(t *testing.T) {
if !strings.Contains(cmd.UsageString(), "60") {
t.Error("default timeout not contained")
}
if !strings.Contains(cmd.UsageString(), "deleted") {
t.Error("wrong until message")
}
}

View File

@ -69,7 +69,7 @@ type KnServingClient interface {
UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, nrRetries int) error
// Delete a service by name
DeleteService(name string) error
DeleteService(name string, timeout time.Duration) error
// Wait for a service to become ready, but not longer than provided timeout.
// Return error and how long has been waited
@ -253,10 +253,29 @@ func updateServiceWithRetry(cl KnServingClient, name string, updateFunc serviceU
}
// Delete a service by name
func (cl *knServingClient) DeleteService(serviceName string) error {
// Param `timeout` represents a duration to wait for a delete op to finish.
// For `timeout == 0` delete is performed async without any wait.
func (cl *knServingClient) DeleteService(serviceName string, timeout time.Duration) error {
if timeout == 0 {
return cl.deleteService(serviceName, v1.DeletePropagationBackground)
}
waitC := make(chan error)
go func() {
waitForEvent := wait.NewWaitForEvent("service", cl.WatchService, func(evt *watch.Event) bool { return evt.Type == watch.Deleted })
err, _ := waitForEvent.Wait(serviceName, timeout, wait.NoopMessageCallback())
waitC <- err
}()
err := cl.deleteService(serviceName, v1.DeletePropagationForeground)
if err != nil {
return err
}
return <-waitC
}
func (cl *knServingClient) deleteService(serviceName string, propagationPolicy v1.DeletionPropagation) error {
err := cl.client.Services(cl.namespace).Delete(
serviceName,
&v1.DeleteOptions{},
&v1.DeleteOptions{PropagationPolicy: &propagationPolicy},
)
if err != nil {
return clienterrors.GetError(err)

View File

@ -106,12 +106,12 @@ func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc ser
}
// Delete a service by name
func (sr *ServingRecorder) DeleteService(name interface{}, err error) {
sr.r.Add("DeleteService", []interface{}{name}, []interface{}{err})
func (sr *ServingRecorder) DeleteService(name, timeout interface{}, err error) {
sr.r.Add("DeleteService", []interface{}{name, timeout}, []interface{}{err})
}
func (c *MockKnServingClient) DeleteService(name string) error {
call := c.recorder.r.VerifyCall("DeleteService", name)
func (c *MockKnServingClient) DeleteService(name string, timeout time.Duration) error {
call := c.recorder.r.VerifyCall("DeleteService", name, timeout)
return mock.ErrorOrNil(call.Result[0])
}

View File

@ -36,7 +36,7 @@ func TestMockKnClient(t *testing.T) {
recorder.ListServices(mock.Any(), nil, nil)
recorder.CreateService(&servingv1.Service{}, nil)
recorder.UpdateService(&servingv1.Service{}, nil)
recorder.DeleteService("hello", nil)
recorder.DeleteService("hello", time.Duration(10)*time.Second, nil)
recorder.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback(), nil, 10*time.Second)
recorder.GetRevision("hello", nil, nil)
recorder.ListRevisions(mock.Any(), nil, nil)
@ -50,7 +50,7 @@ func TestMockKnClient(t *testing.T) {
client.ListServices(WithName("blub"))
client.CreateService(&servingv1.Service{})
client.UpdateService(&servingv1.Service{})
client.DeleteService("hello")
client.DeleteService("hello", time.Duration(10)*time.Second)
client.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback())
client.GetRevision("hello")
client.ListRevisions(WithName("blub"))

View File

@ -179,19 +179,38 @@ func TestDeleteService(t *testing.T) {
}
return true, nil, errors.NewNotFound(servingv1.Resource("service"), name)
})
serving.AddWatchReactor("services",
func(a clienttesting.Action) (bool, watch.Interface, error) {
watchAction := a.(clienttesting.WatchAction)
name, found := watchAction.GetWatchRestrictions().Fields.RequiresExactMatch("metadata.name")
if !found {
return true, nil, errors.NewNotFound(servingv1.Resource("service"), name)
}
w := wait.NewFakeWatch(getServiceDeleteEvents("test-service"))
w.Start()
return true, w, nil
})
t.Run("delete existing service returns no error", func(t *testing.T) {
err := client.DeleteService(serviceName)
err := client.DeleteService(serviceName, time.Duration(10)*time.Second)
assert.NilError(t, err)
})
t.Run("trying to delete non-existing service returns error", func(t *testing.T) {
err := client.DeleteService(nonExistingServiceName)
err := client.DeleteService(nonExistingServiceName, time.Duration(10)*time.Second)
assert.ErrorContains(t, err, "not found")
assert.ErrorContains(t, err, nonExistingServiceName)
})
}
func getServiceDeleteEvents(name string) []watch.Event {
return []watch.Event{
{watch.Added, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", "msg1")},
{watch.Modified, wait.CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionTrue, "", "msg2")},
{watch.Deleted, wait.CreateTestServiceWithConditions(name, corev1.ConditionTrue, corev1.ConditionTrue, "", "")},
}
}
func TestGetRevision(t *testing.T) {
serving, client := setup()

View File

@ -36,9 +36,19 @@ type waitForReadyConfig struct {
kind string
}
// Callbacks and configuration used while waiting for event
type waitForEvent struct {
watchMaker WatchMaker
eventDone EventDone
kind string
}
// EventDone is a marker to stop actual waiting on given event state
type EventDone func(ev *watch.Event) bool
// Interface used for waiting of a resource of a given name to reach a definitive
// state in its "Ready" condition.
type WaitForReady interface {
type Wait interface {
// Wait on resource the resource with this name until a given timeout
// and write event messages for unknown event to the status writer.
@ -56,7 +66,7 @@ type ConditionsExtractor func(obj runtime.Object) (apis.Conditions, error)
type MessageCallback func(durationSinceState time.Duration, message string)
// Constructor with resource type specific configuration
func NewWaitForReady(kind string, watchMaker WatchMaker, extractor ConditionsExtractor) WaitForReady {
func NewWaitForReady(kind string, watchMaker WatchMaker, extractor ConditionsExtractor) Wait {
return &waitForReadyConfig{
kind: kind,
watchMaker: watchMaker,
@ -64,6 +74,14 @@ func NewWaitForReady(kind string, watchMaker WatchMaker, extractor ConditionsExt
}
}
func NewWaitForEvent(kind string, watchMaker WatchMaker, eventDone EventDone) Wait {
return &waitForEvent{
kind: kind,
watchMaker: watchMaker,
eventDone: eventDone,
}
}
// A simple message callback which prints out messages line by line
func SimpleMessageCallback(out io.Writer) MessageCallback {
oldMessage := ""
@ -119,6 +137,7 @@ func (w *waitForReadyConfig) waitForReadyCondition(start time.Time, name string,
// channel used to transport the error
errChan := make(chan error)
var errorTimer *time.Timer
// Stop error timer if it has been started because of
// a ConditionReady has been set to false
@ -178,9 +197,32 @@ func (w *waitForReadyConfig) waitForReadyCondition(start time.Time, name string,
}
}
// Going over Unstructured to keep that function generally applicable.
// Alternative implemenentation: Add a func-field to waitForReadyConfig which has to be
// provided for every resource (like the conditions extractor)
// Wait until the expected EventDone is satisfied
func (w *waitForEvent) Wait(name string, timeout time.Duration, msgCallback MessageCallback) (error, time.Duration) {
watcher, err := w.watchMaker(name, timeout)
if err != nil {
return err, 0
}
defer watcher.Stop()
start := time.Now()
// channel used to transport the error
errChan := make(chan error)
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case <-timer.C:
return fmt.Errorf("timeout: %s '%s' not ready after %d seconds", w.kind, name, int(timeout/time.Second)), time.Since(start)
case err = <-errChan:
return err, time.Since(start)
case event := <-watcher.ResultChan():
if w.eventDone(&event) {
return nil, time.Since(start)
}
}
}
}
func isGivenEqualsObservedGeneration(object runtime.Object) (bool, error) {
unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
if err != nil {

View File

@ -76,6 +76,40 @@ func TestAddWaitForReady(t *testing.T) {
}
}
func TestAddWaitForDelete(t *testing.T) {
for i, tc := range prepareDeleteTestCases("test-service") {
fakeWatchAPI := NewFakeWatch(tc.events)
waitForEvent := NewWaitForEvent(
"blub",
func(name string, timeout time.Duration) (watch.Interface, error) {
return fakeWatchAPI, nil
},
func(evt *watch.Event) bool { return evt.Type == watch.Deleted })
fakeWatchAPI.Start()
err, _ := waitForEvent.Wait("foobar", tc.timeout, NoopMessageCallback())
close(fakeWatchAPI.eventChan)
if tc.errorText == "" && err != nil {
t.Errorf("%d: Error received %v", i, err)
continue
}
if tc.errorText != "" {
if err == nil {
t.Errorf("%d: No error but expected one", i)
} else {
assert.ErrorContains(t, err, tc.errorText)
}
}
if fakeWatchAPI.StopCalled != 1 {
t.Errorf("%d: Exactly one 'stop' should be called, but got %d", i, fakeWatchAPI.StopCalled)
}
}
}
// Test cases which consists of a series of events to send and the expected behaviour.
func prepareTestCases(name string) []waitForReadyTestCase {
return []waitForReadyTestCase{
@ -87,6 +121,13 @@ func prepareTestCases(name string) []waitForReadyTestCase {
}
}
func prepareDeleteTestCases(name string) []waitForReadyTestCase {
return []waitForReadyTestCase{
tc(deNormal, name, time.Second, ""),
tc(peTimeout, name, 10*time.Second, "timeout"),
}
}
func errorTest(name string) waitForReadyTestCase {
events := []watch.Event{
{watch.Added, CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", "msg1")},
@ -150,3 +191,12 @@ func peReadyFalseWithinErrorWindow(name string) ([]watch.Event, int) {
{watch.Modified, CreateTestServiceWithConditions(name, corev1.ConditionTrue, corev1.ConditionTrue, "Route ready", "")},
}, len(messages)
}
func deNormal(name string) ([]watch.Event, int) {
messages := pMessages(2)
return []watch.Event{
{watch.Added, CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", messages[0])},
{watch.Modified, CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionTrue, "", messages[1])},
{watch.Deleted, CreateTestServiceWithConditions(name, corev1.ConditionTrue, corev1.ConditionTrue, "", "")},
}, len(messages)
}