Files
odo/pkg/task/retry_test.go
Armel Soro 74516a9f58 Add support for command/args fields in container components (#5768)
* Introduce new 'pkg/remotecmd' package

This package allows to execute commands in remote packages
and exposes an interface for managing processes associated
to given Devfile commands.

* Rely on 'pkg/libdevfile' as much as possible for Devfile command execution

This requires passing a handler at the odo side,
which in turns uses the 'pkg/remotecmd' package to
run commands in remote containers.

* Switch to running without Supervisord as PID 1 in containers

To do this, the idea is to start the container component:
1- using the command/args defined in the Devfile
2- using whatever was defined in the container image
   if there is no command/args defined in the Devfile

Then, once the container is started, we would execute the Devfile
commands directly in the container component, just like a simple
'kubectl exec' command would do.
Since this is a long-running command (and potentially never ending),
we would need to run it in the background, i.e. in a side goroutine.

Point 2) above requires implementing a temporary hack (as discussed in [1]),
without us having to wait for [2] to be merged on the Devfile side.
This temporary hack overrides the container entrypoint with "tail -f /dev/null"
if the component defines no command or args (in which case we should have
used whatever is defined in the image, per the specification).

[1] https://github.com/redhat-developer/odo/pull/5768#issuecomment-1147190409
[2] https://github.com/devfile/registry/pull/102

* Rename K8s adapter struct 'client' field into 'kubeClient', as suggested in review

* Rename sync adapter struct 'client' fields to better distinguish between them

* Make sure messages displayed to users running 'odo dev' are the same

* Update temporary hack log message

Co-authored-by: Philippe Martin <contact@elol.fr>

* Make sure to handle process output line by line, for performance purposes

* Handle remote process output and errors in the Devfile command handler

The implementation in kubeexec.go should remain
as generic as possible

* Keep retrying remote process status until timeout, rather than just waiting for 1 sec

Now that the command is run via a goroutine,
there might be some situations where we were checking
the status just before the goroutine had a chance to start.

* Handle remote process output and errors in the Devfile command handler

The implementation in kubeexec.go should remain
as generic as possible

* Update kubeexec StopProcessForCommand implementation such that it relies on /proc to kill the parent children processes

* Ignore missing children file in getProcessChildren

* Unit-test methods in kubexec.go

* Fix missing logs when build command does not pass when running 'odo dev'

Also add integration test case

* Fix spinner status when commands passed to exec_handler do not pass

* Make sure to check process status right after stopping it

The process just stopped might take longer to exit (it might have caught
the signal and is performing additional cleanup)

* Keep retrying remote process status until timeout, rather than just waiting for 1 sec

Now that the command is run via a goroutine,
there might be some situations where we were checking
the status just before the goroutine had a chance to start.

* Fix potential deadlock when reading output from remotecmd#ExecuteCommandAndGetOutput

Rely on the same logic in ExecuteCommand

* Add more unit tests

* Remove block that used to check debug port from env info

As commented out in [1], we don't store anymore the debug port value in the ENV file.

[1] https://github.com/redhat-developer/odo/pull/5768#discussion_r893163382

* Rename 'getCommandFromFlag' into 'getCommandByName', as suggested in review

* Make remotecmd package more generic

This package no longer depends on Devfile-related packages.

* Fix comments in libdevfile.go

* Move errorIfTimeout struct field as parameter of RetryWithSchedule

This boolean is tied to the given retry schedule,
so it makes sense for it to be passed with the schedule.

* Expose a single ExecuteCommand function that returns both stdout and stderr

Co-authored-by: Philippe Martin <contact@elol.fr>
2022-06-17 10:51:02 +00:00

138 lines
3.8 KiB
Go

package task
import (
"errors"
"testing"
"time"
)
func TestRetryable_RetryWithSchedule(t *testing.T) {
var empty struct{}
for _, tt := range []struct {
name string
runner func(nbInvocations int) (exitCondition bool, result interface{}, err error)
errorIfTimeout bool
schedule []time.Duration
wantErr bool
wantInvocations int
}{
{
name: "no schedule with runner returning no error",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, nil
},
wantInvocations: 1,
},
{
name: "no schedule with runner returning an error",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, errors.New("some error")
},
wantErr: true,
wantInvocations: 1,
},
{
name: "schedule with runner returning no error and exit condition never matched",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, nil
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
},
wantInvocations: 3,
},
{
name: "schedule with runner returning an error and exit condition never matched",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, errors.New("some error")
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
},
wantInvocations: 3,
wantErr: true,
},
{
name: "schedule with runner returning no error and exit condition never matched and error if timeout set to true",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, nil
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
},
wantInvocations: 3,
errorIfTimeout: true,
wantErr: true,
},
{
name: "schedule with runner returning an error and exit condition never matched and error if timeout set to true",
runner: func(_ int) (exitCondition bool, result interface{}, err error) {
return false, empty, errors.New("some error")
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
},
wantInvocations: 3,
errorIfTimeout: true,
wantErr: true,
},
{
name: "schedule with runner return no error and matching exit condition after 2nd invocation",
runner: func(n int) (exitCondition bool, result interface{}, err error) {
if n == 2 {
return true, empty, nil
}
return false, empty, nil
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
},
wantInvocations: 2,
},
{
name: "schedule with runner return an error and matching exit condition after 2nd invocation",
runner: func(n int) (exitCondition bool, result interface{}, err error) {
err = errors.New("some error")
if n == 2 {
return true, empty, err
}
return false, empty, err
},
schedule: []time.Duration{
10 * time.Millisecond,
30 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
},
wantInvocations: 2,
wantErr: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
var nbRunnerInvocations int
_, err := NewRetryable(tt.name, func() (exitCondition bool, result interface{}, err error) {
nbRunnerInvocations++
return tt.runner(nbRunnerInvocations)
}).RetryWithSchedule(tt.schedule, tt.errorIfTimeout)
if tt.wantErr != (err != nil) {
t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
}
if tt.wantInvocations != nbRunnerInvocations {
t.Errorf("expected %d, got %d", tt.wantInvocations, nbRunnerInvocations)
}
})
}
}