Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v0.15.2

- Upgraded the metallb implementation to match the implementation from
0.31.

## v0.15.1

- Added missing Kuma addon CLI entry.
Expand Down
14 changes: 12 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/stretchr/testify v1.8.2
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
golang.org/x/text v0.3.7 // indirect
Expand All @@ -34,7 +34,12 @@ require (

require github.com/docker/go-connections v0.4.0 // indirect

require sigs.k8s.io/controller-runtime v0.12.2
require (
github.com/avast/retry-go/v4 v4.3.4
sigs.k8s.io/controller-runtime v0.12.2
sigs.k8s.io/kustomize/api v0.10.1
sigs.k8s.io/kustomize/kyaml v0.13.0
)

require (
cloud.google.com/go/compute v1.7.0 // indirect
Expand All @@ -47,6 +52,7 @@ require (
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
Expand All @@ -58,6 +64,7 @@ require (
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand All @@ -70,6 +77,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand All @@ -85,7 +93,9 @@ require (
github.com/tidwall/gjson v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
go.opencensus.io v0.23.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
Expand Down
42 changes: 41 additions & 1 deletion go.sum

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions internal/retry/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package retry

import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
"time"

"github.com/avast/retry-go/v4"
"github.com/sirupsen/logrus"
)

const (
retryCount = 10
retryWait = 3 * time.Second
)

type Doer interface {
Do(ctx context.Context) error
DoWithErrorHandling(ctx context.Context, errorFunc ErrorFunc) error
}

type ErrorFunc func(error, *bytes.Buffer, *bytes.Buffer) error

type CommandDoer struct {
stdin io.Reader
stdout io.Writer
stderr io.Writer
cmd string
args []string
}

func Command(cmd string, args ...string) CommandDoer {
return CommandDoer{
cmd: cmd,
args: args,
}
}

func (c CommandDoer) WithStdin(r io.Reader) CommandDoer {
c.stdin = r
return c
}

func (c CommandDoer) WithStdout(w io.Writer) CommandDoer {
c.stdout = w
return c
}

func (c CommandDoer) WithStderr(w io.Writer) CommandDoer {
c.stderr = w
return c
}

func (c CommandDoer) Do(ctx context.Context) error {
return retry.Do(func() error {
cmd, stdout, stderr := c.createCmd(ctx)
err := cmd.Run()
if err != nil {
return fmt.Errorf("command %q with args %v failed STDOUT=(%s) STDERR=(%s): %w",
c.cmd, c.args, stdout.String(), stderr.String(), err,
)
}
return nil
},
c.createOpts(ctx)...,
)
}

// DoWithErrorHandling executes the command and runs errorFunc passing a resulting err, stdout and stderr to be handled
// by the caller. The errorFunc is going to be called only when the resulting err != nil.
func (c CommandDoer) DoWithErrorHandling(ctx context.Context, errorFunc ErrorFunc) error {
return retry.Do(func() error {
cmd, stdout, stderr := c.createCmd(ctx)
err := cmd.Run()
if err != nil {
return errorFunc(err, stdout, stderr)
}
return nil
},
c.createOpts(ctx)...,
)
}

func (c CommandDoer) createCmd(ctx context.Context) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer) {
stdout := new(bytes.Buffer)
if c.stdout == nil {
c.stdout = stdout
} else {
c.stdout = io.MultiWriter(c.stdout, stdout)
}

stderr := new(bytes.Buffer)
if c.stderr == nil {
c.stderr = stderr
} else {
c.stderr = io.MultiWriter(c.stderr, stderr)
}

cmd := exec.CommandContext(ctx, c.cmd, c.args...) //nolint:gosec
cmd.Stdin = c.stdin
cmd.Stdout = c.stdout
cmd.Stderr = c.stderr

return cmd, stdout, stderr
}

func (c CommandDoer) createOpts(ctx context.Context) []retry.Option {
return []retry.Option{
retry.Context(ctx),
retry.Delay(retryWait),
retry.Attempts(retryCount),
retry.DelayType(retry.FixedDelay),
retry.OnRetry(func(_ uint, err error) {
if err != nil {
logrus.WithError(err).
WithField("args", c.args).
Errorf("failed running %s", c.cmd)
}
}),
}
}
72 changes: 72 additions & 0 deletions internal/retry/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package retry_test

import (
"bytes"
"context"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/kong/kubernetes-testing-framework/internal/retry"
)

func TestDoWithErrorHandling(t *testing.T) {
t.Run("succeeded command won't call the errorFunc", func(t *testing.T) {
cmd := retry.Command("echo", "test")

itShouldntGetCalled := func(err error, _ *bytes.Buffer, _ *bytes.Buffer) error {
t.Error("this function shouldn't be called because there was no error running command")
return err
}
err := cmd.DoWithErrorHandling(context.Background(), itShouldntGetCalled)
require.NoError(t, err)
})

t.Run("failing command will call the errorFunc", func(t *testing.T) {
cmd := retry.Command("unknown-command")

wasCalled := false
itShouldBeCalled := func(err error, _ *bytes.Buffer, _ *bytes.Buffer) error {
wasCalled = true
return err
}

// Wait just a second to not make tests run too long. It's enough to know the errorFunc was called at least once.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := cmd.DoWithErrorHandling(ctx, itShouldBeCalled)
require.Error(t, err)
require.True(t, wasCalled, "expected errorFunc to be called because the command has failed")
})
}

func TestDo(t *testing.T) {
t.Run("passing stdout works", func(t *testing.T) {
stdout := &bytes.Buffer{}
cmd := retry.Command("echo", "hello").WithStdout(stdout)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

err := cmd.Do(ctx)
require.NoError(t, err)
require.Equal(t, "hello\n", stdout.String())
})

t.Run("passing stdin works", func(t *testing.T) {
stdin := bytes.NewBufferString("hello")
cmd := retry.Command("cat").WithStdin(stdin)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

err := cmd.Do(ctx)
require.NoError(t, err)
})

// Testing stderr might not be reliable because it's not guaranteed that
// the command will fail in the time we allow it to run.
// Alternative would be to wait long enough so that we're it ran but that
// would unnecessarily make the tests longer.
}
Loading