diff --git a/installutils/helminstall/internal/client_test.go b/installutils/helminstall/internal/client_test.go index 37b2fd0..db82340 100644 --- a/installutils/helminstall/internal/client_test.go +++ b/installutils/helminstall/internal/client_test.go @@ -2,7 +2,7 @@ package internal_test import ( "bytes" - "io/ioutil" + "io" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" @@ -109,7 +109,7 @@ var _ = Describe("helm install client", func() { It("should download Helm chart", func() { chartUri := "chartUri.tgz" chartFileContents := "test chart file" - chartFile := ioutil.NopCloser(bytes.NewBufferString(chartFileContents)) + chartFile := io.NopCloser(bytes.NewBufferString(chartFileContents)) expectedChart := &chart.Chart{} mockResourceFetcher. EXPECT(). diff --git a/manifesttestutils/util.go b/manifesttestutils/util.go index 3789951..77e05ad 100644 --- a/manifesttestutils/util.go +++ b/manifesttestutils/util.go @@ -2,7 +2,7 @@ package manifesttestutils import ( "fmt" - "io/ioutil" + "os" extv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -306,7 +306,7 @@ func (t *testManifest) mustFindObject(kind, namespace, name string) runtime.Obje } func mustReadManifest(relativePathToManifest string) string { - bytes, err := ioutil.ReadFile(relativePathToManifest) + bytes, err := os.ReadFile(relativePathToManifest) Expect(err).NotTo(HaveOccurred()) return string(bytes) } diff --git a/testutils/helper/curl.go b/testutils/helper/curl.go index bd1beab..7e98796 100644 --- a/testutils/helper/curl.go +++ b/testutils/helper/curl.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "strings" "time" @@ -65,6 +64,10 @@ func (t *testContainer) CurlEventuallyShouldOutput(opts CurlOpts, substr string, tick := time.Tick(currentTimeout / 8) gomega.EventuallyWithOffset(ginkgoOffset+1, func() string { + if !t.CanCurl() { + return "" + } + var res string bufChan, done, err := t.CurlAsyncChan(opts) @@ -84,7 +87,7 @@ func (t *testContainer) CurlEventuallyShouldOutput(opts CurlOpts, substr string, buf = r } } - byt, err := ioutil.ReadAll(buf) + byt, err := io.ReadAll(buf) if err != nil { res = err.Error() } else { @@ -103,6 +106,9 @@ func (t *testContainer) CurlEventuallyShouldRespond(opts CurlOpts, substr string tick := time.Tick(currentTimeout / 8) gomega.EventuallyWithOffset(ginkgoOffset+1, func() string { + if !t.CanCurl() { + return "" + } res, err := t.Curl(opts) if err != nil { res = err.Error() @@ -184,16 +190,28 @@ func (t *testContainer) buildCurlArgs(opts CurlOpts) []string { } func (t *testContainer) Curl(opts CurlOpts) (string, error) { + if !t.CanCurl() { + return "", fmt.Errorf("testContainer from image %s:%s cannot curl", t.containerImageName, t.imageTag) + } + args := t.buildCurlArgs(opts) return t.Exec(args...) } func (t *testContainer) CurlAsync(opts CurlOpts) (io.Reader, chan struct{}, error) { + if !t.CanCurl() { + return nil, nil, fmt.Errorf("testContainer from image %s:%s cannot curl", t.containerImageName, t.imageTag) + } + args := t.buildCurlArgs(opts) return t.TestRunnerAsync(args...) } func (t *testContainer) CurlAsyncChan(opts CurlOpts) (<-chan io.Reader, chan struct{}, error) { + if !t.CanCurl() { + return nil, nil, fmt.Errorf("testContainer from image %s:%s cannot curl", t.containerImageName, t.imageTag) + } + args := t.buildCurlArgs(opts) return t.TestRunnerChan(&bytes.Buffer{}, args...) } diff --git a/testutils/helper/http_echo.go b/testutils/helper/http_echo.go index beca149..6f2f6af 100644 --- a/testutils/helper/http_echo.go +++ b/testutils/helper/http_echo.go @@ -1,31 +1,13 @@ package helper -import ( - "time" -) - -type echoPod struct { - *testContainer -} - -func (t *echoPod) Deploy(timeout time.Duration) error { - return t.deploy(timeout) -} - const ( defaultHttpEchoImage = "kennship/http-echo@sha256:144322e8e96be2be6675dcf6e3ee15697c5d052d14d240e8914871a2a83990af" HttpEchoName = "http-echo" HttpEchoPort = 3000 ) -func NewEchoHttp(namespace string) (*echoPod, error) { - container, err := newTestContainer(namespace, defaultHttpEchoImage, HttpEchoName, HttpEchoPort) - if err != nil { - return nil, err - } - return &echoPod{ - testContainer: container, - }, nil +func NewEchoHttp(namespace string) (TestContainer, error) { + return newTestContainer(namespace, defaultHttpEchoImage, HttpEchoName, HttpEchoPort) } const ( @@ -34,12 +16,6 @@ const ( TcpEchoPort = 1025 ) -func NewEchoTcp(namespace string) (*echoPod, error) { - container, err := newTestContainer(namespace, defaultTcpEchoImage, TcpEchoName, TcpEchoPort) - if err != nil { - return nil, err - } - return &echoPod{ - testContainer: container, - }, nil +func NewEchoTcp(namespace string) (TestContainer, error) { + return newTestContainer(namespace, defaultTcpEchoImage, TcpEchoName, TcpEchoPort) } diff --git a/testutils/helper/test_container.go b/testutils/helper/test_container.go index dac9e72..bd8a0ce 100644 --- a/testutils/helper/test_container.go +++ b/testutils/helper/test_container.go @@ -2,7 +2,9 @@ package helper import ( "context" + "fmt" "io" + "strings" "time" "github.com/pkg/errors" @@ -16,16 +18,33 @@ import ( "k8s.io/client-go/kubernetes" ) -type TestRunner interface { +var _ TestRunner = &testRunner{} +var _ TestContainer = &testRunner{} +var _ TestContainer = &testContainer{} + +// A TestContainer is a general-purpose abstraction over a container in which we might +// execute cURL or other, arbitrary commands via kubectl. +type TestContainer interface { Deploy(timeout time.Duration) error Terminate() error - Exec(command ...string) (string, error) - TestRunnerAsync(args ...string) (io.Reader, chan struct{}, error) + CanCurl() bool // Checks the response of the request CurlEventuallyShouldRespond(opts CurlOpts, substr string, ginkgoOffset int, timeout ...time.Duration) - // CHecks all of the output of the curl command + // Checks all of the output of the curl command CurlEventuallyShouldOutput(opts CurlOpts, substr string, ginkgoOffset int, timeout ...time.Duration) Curl(opts CurlOpts) (string, error) + Exec(command ...string) (string, error) + ExecAsync(args ...string) (io.Reader, chan struct{}, error) +} + +// A TestRunner is an extension of a TestContainer which is typically run with the defaultTestRunnerImage +// and which has a service associated with it, and can run https. +type TestRunner interface { + TestContainer + DeployTLS(timeout time.Duration, crt, key []byte) error + DeleteService() error + TerminateAndDeleteService() error + TestRunnerAsync(args ...string) (io.Reader, chan struct{}, error) } func newTestContainer(namespace, imageTag, echoName string, port int32) (*testContainer, error) { @@ -60,6 +79,10 @@ type testContainer struct { port int32 } +func (t *testContainer) Deploy(timeout time.Duration) error { + return t.deploy(timeout) +} + // Deploys the http echo to the kubernetes cluster the kubeconfig is pointing to and waits for the given time for the // http-echo pod to be running. func (t *testContainer) deploy(timeout time.Duration) error { @@ -122,7 +145,23 @@ func (t *testContainer) Terminate() error { return errors.Wrapf(err, "deleting %s pod", t.echoName) } return nil +} +func (t *testContainer) DeleteService() error { + if err := testutils.Kubectl("delete", "service", "-n", t.namespace, t.echoName, "--grace-period=0"); err != nil { + return errors.Wrapf(err, "deleting %s service", t.echoName) + } + return nil +} + +func (t *testContainer) TerminateAndDeleteService() error { + if err := t.Terminate(); err != nil { + return err + } + if err := t.DeleteService(); err != nil { + return err + } + return nil } // testContainer executes a command inside the testContainer container @@ -131,6 +170,17 @@ func (t *testContainer) Exec(command ...string) (string, error) { return testutils.KubectlOut(args...) } +// Cp copies files into the testContainer container +func (t *testContainer) Cp(files map[string]string) error { + for k, v := range files { + if err := testutils.Kubectl("cp", k, fmt.Sprintf("%s/%s:%s", t.namespace, t.echoName, v)); err != nil { + return err + } + } + return nil +} + +// TestRunnerAsync is deprecated; please use ExecAsync. // TestContainerAsync executes a command inside the testContainer container // returning a buffer that can be read from as it executes func (t *testContainer) TestRunnerAsync(args ...string) (io.Reader, chan struct{}, error) { @@ -138,7 +188,21 @@ func (t *testContainer) TestRunnerAsync(args ...string) (io.Reader, chan struct{ return testutils.KubectlOutAsync(args...) } +// ExecAsync executes a command inside the testContainer container +// returning a buffer that can be read from as it executes +func (t *testContainer) ExecAsync(args ...string) (io.Reader, chan struct{}, error) { + args = append([]string{"exec", "-i", t.echoName, "-n", t.namespace, "--"}, args...) + return testutils.KubectlOutAsync(args...) +} + func (t *testContainer) TestRunnerChan(r io.Reader, args ...string) (<-chan io.Reader, chan struct{}, error) { args = append([]string{"exec", "-i", t.echoName, "-n", t.namespace, "--"}, args...) return testutils.KubectlOutChan(r, args...) } + +func (t *testContainer) CanCurl() bool { + if out, err := t.Exec("curl", "--version"); err != nil || !strings.HasPrefix(out, "curl") { + return false + } + return true +} diff --git a/testutils/helper/test_container_test.go b/testutils/helper/test_container_test.go index 2ec21b5..c06418e 100644 --- a/testutils/helper/test_container_test.go +++ b/testutils/helper/test_container_test.go @@ -42,7 +42,7 @@ var _ = Describe("test container tests", func() { }) Context("test runner", func() { var ( - testRunner *testRunner + testRunner TestRunner ) BeforeEach(func() { var err error @@ -71,15 +71,15 @@ var _ = Describe("test container tests", func() { }) }) - Context("http ehco", func() { + Context("http echo", func() { var ( - httpEcho *echoPod + httpEcho TestContainer ) BeforeEach(func() { var err error httpEcho, err = NewEchoHttp(namespace) Expect(err).NotTo(HaveOccurred()) - err = httpEcho.deploy(time.Minute) + err = httpEcho.(*testContainer).deploy(time.Minute) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { @@ -105,13 +105,13 @@ var _ = Describe("test container tests", func() { Context("tcp ehco", func() { var ( - tcpEcho *echoPod + tcpEcho TestContainer ) BeforeEach(func() { var err error tcpEcho, err = NewEchoTcp(namespace) Expect(err).NotTo(HaveOccurred()) - err = tcpEcho.deploy(time.Minute) + err = tcpEcho.(*testContainer).deploy(time.Minute) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { diff --git a/testutils/helper/testrunner.go b/testutils/helper/testrunner.go index 350e911..61e2eff 100644 --- a/testutils/helper/testrunner.go +++ b/testutils/helper/testrunner.go @@ -2,8 +2,10 @@ package helper import ( "fmt" + "os" "time" + "github.com/pkg/errors" "github.com/solo-io/go-utils/log" ) @@ -45,7 +47,7 @@ const ( ` ) -func NewTestRunner(namespace string) (*testRunner, error) { +func NewTestRunner(namespace string) (TestRunner, error) { testContainer, err := newTestContainer(namespace, defaultTestRunnerImage, TestrunnerName, TestRunnerPort) if err != nil { return nil, err @@ -79,3 +81,57 @@ func (t *testRunner) Deploy(timeout time.Duration) error { }() return nil } + +// DeployTLS deletes the running server pod and its service, then redeploys using a server +// which serves HTTPS with the cert and key provided. +func (t *testRunner) DeployTLS(timeout time.Duration, crt, key []byte) error { + if err := t.TerminateAndDeleteService(); err != nil { + return errors.Wrap(err, "terminating pod and deleting service") + } + if err := t.testContainer.Deploy(timeout); err != nil { + return errors.Wrap(err, "deploying pod") + } + + certFname := "/tmp/testrunner_tls/cert.pem" + keyFname := "/tmp/testrunner_tls/key.pem" + scriptFname := "/tmp/testrunner_tls/server.py" + os.MkdirAll("/tmp/testrunner_tls", os.ModePerm) + defer os.RemoveAll("/tmp/testrunner_tls") + + if err := os.WriteFile(certFname, crt, os.ModePerm); err != nil { + return errors.Wrap(err, "writing cert") + } + if err := os.WriteFile(keyFname, key, os.ModePerm); err != nil { + return errors.Wrap(err, "writing key") + } + if err := os.WriteFile(scriptFname, []byte(fmt.Sprintf(` +import BaseHTTPServer, SimpleHTTPServer +import ssl + +httpd = BaseHTTPServer.HTTPServer(('0.0.0.0', %d), SimpleHTTPServer.SimpleHTTPRequestHandler) +httpd.socket = ssl.wrap_socket (httpd.socket, keyfile='/tmp/key.pem', certfile='/tmp/cert.pem', server_side=True) +httpd.serve_forever() +`, TestRunnerPort)), os.ModePerm); err != nil { + return errors.Wrap(err, "writing server") + } + if err := t.Cp(map[string]string{ + certFname: "/tmp/cert.pem", + keyFname: "/tmp/key.pem", + scriptFname: "/tmp/server.py", + }); err != nil { + return errors.Wrap(err, "kubectl cp") + } + + go func() { + start := time.Now() + log.Debugf("starting https server listening on port %v", TestRunnerPort) + // This command starts an https SimpleHttpServer and blocks until the server terminates + if _, err := t.Exec("python", "/tmp/server.py"); err != nil { + // if an error happened after 5 seconds, it's probably not an error.. just the pod terminating. + if time.Now().Sub(start).Seconds() < 5.0 { + log.Warnf("failed to start HTTPS Server in Test Runner: %v", err) + } + } + }() + return nil +} diff --git a/testutils/kube/curl.go b/testutils/kube/curl.go index 3048a3e..e431df5 100644 --- a/testutils/kube/curl.go +++ b/testutils/kube/curl.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "github.com/solo-io/go-utils/testutils" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -176,7 +175,7 @@ func executeNoFail(ctx context.Context, logger io.Writer, kubeContext string, ar case <-ctx.Done(): return "", nil case reader := <-readerChan: - data, err := ioutil.ReadAll(reader) + data, err := io.ReadAll(reader) if err != nil { return "", err }