diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 7d44b307..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2019 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: 2.1 - -executors: - builder: - docker: - - image: circleci/golang:1.16.2 - - image: registry:2 - -jobs: - check: - executor: builder - resource_class: medium+ - environment: - # Run garbage collection more aggresively to avoid getting OOMed during the lint phase. - GOGC: "20" - # Circle CI executor has many cores (> 30) but throttles CPU and RAM. If we don't limit - # this to the number of allocated cores, the job is likely to get OOMed and killed. - GOMAXPROCS: "3" - steps: - - checkout - - run: make check - - test: - executor: builder - resource_class: medium+ - steps: - - checkout - - run: make init - # prefetch implicit version needed by pkg/binary/envoy/controlplane/istio_test.go until #136. This avoids: - # Unable to start Envoy process: fork/exec /home/circleci/.getenvoy/builds/standard/1.11.0/linux_glibc/bin/envoy: text file busy - - run: go run cmd/getenvoy/main.go fetch standard:1.11.0 - - run: make test.ci GO_TEST_EXTRA_OPTS="-timeout 60s" - - run: - name: "Measure test coverage (for now, on a subset of tests)" - command: make coverage GO_COVERAGE_EXTRA_OPTS="-p 1" - - store_artifacts: - path: build/coverage - destination: /coverage - -workflows: - version: 2 - commit: - jobs: - - check - - test diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 428d5d25..3c14da78 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -17,6 +17,55 @@ on: workflow_dispatch: jobs: + test: + name: "Run unit tests" + runs-on: ${{ matrix.runner.os }} + timeout-minutes: 90 # instead of 360 by default + strategy: + matrix: + runner: + # - os: macos-latest ## revisit once all tests are off ginkgo + - os: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v2 + + - name: "Install Go" + uses: actions/setup-go@v2 + with: + go-version: '1.16.2' + + - name: "Install 'Docker for Mac' (Latest)" + uses: docker-practice/actions-setup-docker@v1 # needed while TestGetEnvoyExtensionPush uses a real registry + if: runner.os == 'macOS' + with: + docker_buildx: false # Install is flakey. When it, we can install it via docker/setup-buildx-action@v1 + timeout-minutes: 20 # fail fast if MacOS install takes too long + + - name: "Init on first use" + run: make init + + - name: "Verify clean check-in" + run: make check + + - name: "Run unit tests" + # prefetch implicit version needed by pkg/binary/envoy/controlplane/istio_test.go until #136. This avoids: + # Unable to start Envoy process: fork/exec /home/circleci/.getenvoy/builds/standard/1.11.0/linux_glibc/bin/envoy: text file busy + run: | + go run cmd/getenvoy/main.go fetch standard:1.11.0 + make test + + - name: "Generate test coverage report" + if: runner.os == 'Linux' # no need to do this per operating system + run: make coverage + + - name: "Upload test coverage report" + uses: actions/upload-artifact@v2 + if: runner.os == 'Linux' # no need to do this per operating system + with: + name: coverage + path: build/coverage + bin: name: "Build the `getenvoy` binary for use in e2e tests" runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 6200ea4d..d1d86989 100644 --- a/Makefile +++ b/Makefile @@ -47,11 +47,9 @@ GO_TEST_EXTRA_OPTS ?= # TODO(yskopets): include all packages into test run once blocking issues have been resolved, including # * https://github.com/tetratelabs/getenvoy/issues/87 `go test -race` fails -# * https://github.com/tetratelabs/getenvoy/issues/88 `go test ./...` fails on Mac -# * https://github.com/tetratelabs/getenvoy/issues/89 `go test github.com/tetratelabs/getenvoy/pkg/binary/envoy/controlplane` removes `/tmp` dir COVERAGE_PKG_LIST ?= $(shell go list ./pkg/... | grep -v -e github.com/tetratelabs/getenvoy/pkg/binary/envoy/controlplane -e github.com/tetratelabs/getenvoy/pkg/binary/envoy/debug) GO_COVERAGE_OPTS ?= -covermode=atomic -coverpkg=./... -GO_COVERAGE_EXTRA_OPTS ?= +GO_COVERAGE_EXTRA_OPTS ?= -p 1 E2E_PKG_LIST ?= ./test/e2e # Set the default timeout >10m as particularly rust e2e tests are slow https://golang.org/cmd/go/#hdr-Testing_flags @@ -100,10 +98,6 @@ test: generate docker-compose up -d go test $(GO_TEST_OPTS) $(GO_TEST_EXTRA_OPTS) $(TEST_PKG_LIST) -.PHONY: test.ci -test.ci: generate - go test $(GO_TEST_OPTS) $(GO_TEST_EXTRA_OPTS) $(TEST_PKG_LIST) - .PHONY: e2e e2e: $(call GETENVOY_OUT_PATH,$(GOOS),$(GOARCH)) docker-compose up -d diff --git a/go.mod b/go.mod index 8fa594c0..cf1c2c0a 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,6 @@ require ( github.com/tetratelabs/getenvoy-package v0.0.0-20190730071641-da31aed4333e github.com/tetratelabs/log v0.0.0-20190710134534-eb04d1e84fb8 github.com/tetratelabs/multierror v1.1.0 - gotest.tools v2.2.0+incompatible istio.io/api v0.0.0-20200227213531-891bf31f3c32 istio.io/istio v0.0.0-20200304114959-c3c353285578 rsc.io/letsencrypt v0.0.3 // indirect diff --git a/pkg/binary/envoy/controlplane/istio.tmpl_test.go b/pkg/binary/envoy/controlplane/istio.tmpl_test.go index 60d1fba5..86774731 100644 --- a/pkg/binary/envoy/controlplane/istio.tmpl_test.go +++ b/pkg/binary/envoy/controlplane/istio.tmpl_test.go @@ -24,55 +24,43 @@ import ( "testing" "github.com/mholt/archiver" - "gotest.tools/assert" + "github.com/stretchr/testify/require" ) -func Test_VersionedIstioTemplate(t *testing.T) { - t.Run(fmt.Sprintf("checking Istio bootstrap matches version %s", IstioVersion), func(t *testing.T) { - got := retrieveIstioBootstrap(t) - assert.Equal(t, istioBootStrapTemplate, string(got)) - }) -} - -func retrieveIstioBootstrap(t *testing.T) []byte { +// TestIstioBootStrapTemplateEqualsReleaseJson ensures istioBootStrapTemplate is exactly the same as what would have been +// downloaded from the istio release for version IstioVersion. +func TestIstioBootStrapTemplateEqualsReleaseJson(t *testing.T) { // Retrieve Istio tarball os doesn't matter we only car about the bootstrap JSON - url := fmt.Sprintf("https://github.com/istio/istio/releases/download/%v/istio-%v-linux.tar.gz", IstioVersion, IstioVersion) + url := fmt.Sprintf("https://github.com/istio/istio/releases/download/%s/istio-%s-linux.tar.gz", IstioVersion, IstioVersion) resp, err := http.Get(url) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err, "error getting tarball for istio version %s", IstioVersion) + defer resp.Body.Close() //nolint - if resp.StatusCode != http.StatusOK { - t.Errorf("received %v status code", resp.StatusCode) - } - dst := os.TempDir() + require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected HTTP status from %s", url) + + dst, err := ioutil.TempDir("", "") + require.NoError(t, err, `ioutil.TempDir("", "") erred`) defer os.RemoveAll(dst) + tarball := filepath.Join(dst, "istio.tar.gz") f, err := os.OpenFile(tarball, os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err, "Couldn't open file %s", tarball) defer f.Close() //nolint + _, err = io.Copy(f, resp.Body) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err, "Couldn't download %s into %s", url, tarball) // Walk the tarball until we find the bootstrap + bootstrapName := "envoy_bootstrap_v2.json" var bytes []byte - if walkErr := archiver.Walk(tarball, func(f archiver.File) error { - if f.Name() == "envoy_bootstrap_v2.json" { + err = archiver.Walk(tarball, func(f archiver.File) error { + if f.Name() == bootstrapName { bytes, err = ioutil.ReadAll(f) - if err != nil { - return err - } + require.NoError(t, err, "error reading %s in %s", bootstrapName, url) } return nil - }); walkErr != nil { - t.Fatal(err) - } - if bytes == nil { - t.Fatal("no boostrap found") - } - return bytes + }) + + require.NotNil(t, bytes, "couldn't find %s in %s", bootstrapName, url) + require.Equal(t, istioBootStrapTemplate, string(bytes), "istioBootStrapTemplate isn't the same as the istio %s distribution", IstioVersion) } diff --git a/pkg/binary/envoy/controlplane/istio_test.go b/pkg/binary/envoy/controlplane/istio_test.go index 5205d624..c134926c 100644 --- a/pkg/binary/envoy/controlplane/istio_test.go +++ b/pkg/binary/envoy/controlplane/istio_test.go @@ -16,14 +16,13 @@ package controlplane import ( "context" - "fmt" "io/ioutil" "os" "path/filepath" "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "istio.io/istio/pilot/pkg/bootstrap" "istio.io/istio/tests/util" @@ -32,42 +31,44 @@ import ( "github.com/tetratelabs/getenvoy/pkg/binary/envoytest" ) -func TestMain(m *testing.M) { - if err := envoytest.Fetch(); err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(m.Run()) -} +func TestConnectsToMockPilotAsAGateway(t *testing.T) { + err := envoytest.Fetch() + require.NoError(t, err, "error running envoytest.Fetch()") + _, teardown := setupMockPilot() + defer teardown() -// NOTE: This test will fail on macOS due to an issue with Envoy, the same issue as debug logging -func Test_IstioGateway(t *testing.T) { - t.Run("connects to mock Pilot as a gateway", func(t *testing.T) { - _, teardown := setupMockPilot() - defer teardown() - cfg := envoy.NewConfig( - func(c *envoy.Config) { - c.Mode = envoy.ParseMode("loadbalancer") - c.XDSAddress = util.MockPilotGrpcAddr - c.IPAddresses = []string{"1.1.1.1"} - }, - ) - runtime, _ := envoy.NewRuntime( - func(r *envoy.Runtime) { r.Config = cfg }, - debug.EnableEnvoyAdminDataCollection, - Istio, - ) - defer os.RemoveAll(runtime.DebugStore() + ".tar.gz") - defer os.RemoveAll(runtime.DebugStore()) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - assert.NoError(t, envoytest.Run(ctx, runtime, "")) - time.Sleep(time.Millisecond * 500) // Pilot config propagation - assert.NoError(t, envoytest.Kill(ctx, runtime)) - gotListeners, _ := ioutil.ReadFile(filepath.Join(runtime.DebugStore(), "listeners.txt")) - assert.Contains(t, string(gotListeners), "0.0.0.0_8443::0.0.0.0:8443") - assert.Contains(t, string(gotListeners), "0.0.0.0_8080::0.0.0.0:8080") - }) + cfg := envoy.NewConfig( + func(c *envoy.Config) { + c.Mode = envoy.ParseMode("loadbalancer") + c.XDSAddress = util.MockPilotGrpcAddr + c.IPAddresses = []string{"1.1.1.1"} + }, + ) + + runtime, err := envoy.NewRuntime( + func(r *envoy.Runtime) { r.Config = cfg }, + debug.EnableEnvoyAdminDataCollection, + Istio, + ) + require.NoError(t, err, "error creating envoy runtime") + defer os.RemoveAll(runtime.DebugStore()) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err = envoytest.Run(ctx, runtime, "") + require.NoError(t, err, "error running envoy") + + time.Sleep(time.Millisecond * 500) // Pilot config propagation + + err = envoytest.Kill(ctx, runtime) + require.NoError(t, err, "error killing envoy") + + gotListeners, err := ioutil.ReadFile(filepath.Join(runtime.DebugStore(), "listeners.txt")) + require.NoError(t, err, "error killing envoy listeners") + + require.Contains(t, string(gotListeners), "0.0.0.0_8443::0.0.0.0:8443") + require.Contains(t, string(gotListeners), "0.0.0.0_8080::0.0.0.0:8080") } func setupMockPilot() (*bootstrap.Server, util.TearDownFunc) { @@ -76,6 +77,10 @@ func setupMockPilot() (*bootstrap.Server, util.TearDownFunc) { args.Config.FileDir = "testdata" args.Plugins = bootstrap.DefaultPlugins args.Mesh.MixerAddress = "" + // In a normal macOS setup, you cannot write to /dev/stdout, which is the default path here. + // While not Docker-specific, there are related notes here https://github.com/moby/moby/issues/31243 + // Since this test doesn't read access logs anyway, the easier workaround is to disable access logging. + args.MeshConfig.AccessLogFile = "" args.Service.Registries = []string{} }) } diff --git a/pkg/binary/envoy/debug/lsof.go b/pkg/binary/envoy/debug/lsof.go index d69c9511..4d337688 100644 --- a/pkg/binary/envoy/debug/lsof.go +++ b/pkg/binary/envoy/debug/lsof.go @@ -45,7 +45,8 @@ type OpenFileStat struct { Name string `json:"name"` // name of the mount point and file system on which the file resides } -// EnableOpenFilesDataCollection is a preset option that registers collection of statistics of files opened by envoy instance(s) +// EnableOpenFilesDataCollection is a preset option that registers collection of statistics of files opened by envoy +// instance(s). This is unsupported on macOS/Darwin because it does not support process.OpenFiles func EnableOpenFilesDataCollection(r *envoy.Runtime) { if err := os.Mkdir(filepath.Join(r.DebugStore(), "lsof"), os.ModePerm); err != nil { log.Errorf("error in creating a directory to write open file data of envoy to: %v", err) @@ -80,7 +81,7 @@ func retrieveOpenFilesData(r binary.Runner) error { openFiles, err := envoyProcess.OpenFiles() if err != nil { - return fmt.Errorf("error in getting open file statistics: %v", err) + return fmt.Errorf("error in getting open file statistics: %w", err) } for _, stat := range openFiles { diff --git a/pkg/binary/envoy/debug/lsof_test.go b/pkg/binary/envoy/debug/lsof_test.go index 51ac4958..f3e56c14 100644 --- a/pkg/binary/envoy/debug/lsof_test.go +++ b/pkg/binary/envoy/debug/lsof_test.go @@ -19,7 +19,7 @@ import ( "io/ioutil" "os" "path/filepath" - "strings" + "runtime" "testing" "github.com/stretchr/testify/require" @@ -28,33 +28,29 @@ import ( "github.com/tetratelabs/getenvoy/pkg/binary/envoytest" ) -// TODO: this test fails on darwin due to process.Process.OpenFiles being unsupported. See #88 func TestGetOpenFileStats(t *testing.T) { - t.Run("creates non-empty files", func(t *testing.T) { - r, err := envoy.NewRuntime(EnableOpenFilesDataCollection) - defer os.RemoveAll(r.DebugStore()) - require.NoError(t, err, "error getting envoy runtime") - - err = envoytest.RunKill(r, filepath.Join("testdata", "null.yaml"), 0) - require.NoError(t, err, "error from envoytest.RunKill") - - files := [...]string{"lsof/lsof.json"} - - for _, file := range files { - path := filepath.Join(r.DebugStore(), file) - f, err := os.Stat(path) - require.NoError(t, err, "error stating %v", path) - require.NotEmpty(t, f.Size(), "file %v was empty", path) - - if strings.HasSuffix(file, ".json") { - raw, err := ioutil.ReadFile(path) - require.NoError(t, err, "error reading file %v", path) - - var is []interface{} - err = json.Unmarshal(raw, &is) - require.NoError(t, err, "error to unmarshal json string, %v: \"%v\"", err, raw) - require.NotEmpty(t, len(is), "unmarshaled content is empty, expected to be a non-empty array: \"%v\"", raw) - } - } - }) + r, err := envoy.NewRuntime(EnableOpenFilesDataCollection) + require.NoError(t, err, "error getting envoy runtime") + defer os.RemoveAll(r.DebugStore()) + + err = envoytest.RunKill(r, filepath.Join("testdata", "null.yaml"), 0) + require.NoError(t, err, "error from envoytest.RunKill") + + file := "lsof/lsof.json" + path := filepath.Join(r.DebugStore(), file) + f, err := os.Stat(path) + require.NoError(t, err, "error stating %v", path) + + if runtime.GOOS == `darwin` { // process.OpenFiles in unsupported, so this feature won't work + require.Empty(t, f.Size(), "file %v was not empty", path) + } else { + require.NotEmpty(t, f.Size(), "file %v was empty", path) + raw, err := ioutil.ReadFile(path) + require.NoError(t, err, "error reading file %v", path) + + var is []interface{} + err = json.Unmarshal(raw, &is) + require.NoError(t, err, "error to unmarshal json string, %v: \"%v\"", err, raw) + require.NotEmpty(t, len(is), "unmarshalled content is empty, expected to be a non-empty array: \"%v\"", raw) + } } diff --git a/pkg/binary/envoytest/util.go b/pkg/binary/envoytest/util.go index 60fdf2d7..b1501c51 100644 --- a/pkg/binary/envoytest/util.go +++ b/pkg/binary/envoytest/util.go @@ -52,7 +52,7 @@ func Fetch() error { // It is blocking and will only return once ready (nil) or context timeout is exceeded (error) func Run(ctx context.Context, r binary.Runner, bootstrap string) error { key, _ := manifest.NewKey(Reference) - args := []string{} + var args []string if bootstrap != "" { args = append(args, "-c", bootstrap) }