Repository: tetratelabs/getenvoy
Pull request #216 (2021-05-13 09:03:52)

Drops compile dep on archiver/v3
This removes the dependency on "archiver/v3" from the binary because
* It has no support for streaming https://github.com/mholt/archiver/pull/199, so we had to buffer to disk first
* Including "archiver/v3" makes it easy to accidentally bloat the binary (we did this by 25% before)
* The result is simpler dependencies

Before:
```bash
$ du -sk build/bin/*/*/getenvoy
8020	build/bin/darwin/amd64
7528	build/bin/linux/amd64
```

```bash
$ du -sk build/bin/*/*/getenvoy
7728	build/bin/darwin/amd64
7264	build/bin/linux/amd64
```

codefromthecrypt: ok I retouched this with an updated description
codefromthecrypt: I need to verify this manually prior to merge because GH actions doesn't actually download anything due to caching... `1.17.1/darwin is already downloaded`
codefromthecrypt: We have used xz compression except for the first release (which was gz), so I had to add a dependency for that.
codefromthecrypt: ok all good manually and also in CI
codefromthecrypt: Added a commit to split e2e from the top-level go.mod as it now makes a difference
codefromthecrypt: I rewrote the code and now it increases coverage albeit slightly (85.8% -> 85.9%)
codefromthecrypt: it doesn't seem like our implementation of progress bar likes to be tapped into a read stream (rather the write side). I'll look into it prior to merge.
codefromthecrypt: will pull out the general log and test fixture refactoring into a separate PR as it is unrelated to the archive dep, though needed for me to figure out what was going on.
codefromthecrypt: will rebase after merging https://github.com/tetratelabs/getenvoy/pull/220
codefromthecrypt: oddly coverage is exactly the same 85.6%
codefromthecrypt: #220 removed a lot of the bloat, so I updated the description to be more precise with what's going on. The last commit includes cleanups requested before and some new ones as I noticed progress bar behaves differently depending on the HTTP response (ex if content length is available or not)
codefromthecrypt: aggregate statement coverage is exactly the same before and after this: 86.9%

Patch:
diff --git a/go.mod b/go.mod
index 7a7403c..d6df720 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,6 @@ require (
 	github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
 	github.com/mattn/go-runewidth v0.0.12 // indirect
-	github.com/mholt/archiver/v3 v3.5.0
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/schollz/progressbar/v3 v3.8.0
 	github.com/shirou/gopsutil/v3 v3.21.4
diff --git a/go.sum b/go.sum
index 16f955d..4446ce3 100644
--- a/go.sum
+++ b/go.sum
@@ -19,8 +19,6 @@ github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjl
 github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
-github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -42,9 +40,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
-github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
-github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -66,8 +61,6 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -113,12 +106,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
-github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
-github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -134,8 +121,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
 github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
-github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
@@ -150,13 +135,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
-github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pierrec/lz4/v4 v4.0.3 h1:vNQKSVZNYUEAvRY9FaUXAF1XPbSOHJtDTiP41kzDz2E=
-github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -215,12 +196,8 @@ github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1g
 github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
 github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
-github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
-github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
diff --git a/internal/tar/tar.go b/internal/tar/tar.go
index d0eaa4f..d82ed0f 100644
--- a/internal/tar/tar.go
+++ b/internal/tar/tar.go
@@ -12,82 +12,153 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package tar avoids third-party dependencies (ex archiver/v3) and are special-cased to the needs of getenvoy.
 package tar
 
 import (
+	"archive/tar"
+	"bufio"
 	"compress/gzip"
-	"errors"
-	"fmt"
 	"io"
-	"io/ioutil"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"strings"
 
-	"github.com/mholt/archiver/v3"
 	"github.com/ulikunitz/xz"
 )
 
-// NewDecompressor returns a compression function based on the "src" filename.
-// NOTE: As of May 2021, all Envoy binaries except the first are tar.xz
-func NewDecompressor(src string, r io.Reader) (io.ReadCloser, error) {
-	if strings.HasSuffix(src, "tar.xz") {
-		zr, err := xz.NewReader(r)
-		if err != nil {
-			return nil, fmt.Errorf("not a valid xz stream %s: %w", src, err)
-		}
-		return io.NopCloser(zr), nil
+// Untar unarchives the compressed "src" which is either a "tar.xz" or "tar.gz" stream.
+// This strips the base directory inside the "src" archive. Ex on "/foo/bar", "dst" will have "bar/**"
+//
+// This is used to decompress Envoy distributions in the "downloadLocationURL" field of "manifest.json".
+// To keep the binary size small, only supports compression formats used in practice. As of May 2021, all
+// "downloadLocationURL" values were "tar.xz", except the first (1.11.0), which was "tar.gz".
+func Untar(dst string, src io.Reader) error { // dst, src order like io.Copy
+	if e := os.MkdirAll(dst, 0750); e != nil {
+		return e
 	}
-	zr, err := gzip.NewReader(r)
-	if err != nil {
-		return nil, fmt.Errorf("not a valid gz stream %s: %w", src, err)
+
+	zSrc, e := newDecompressor(src)
+	if e != nil {
+		return e
+	}
+	defer zSrc.Close() //nolint
+
+	tr := tar.NewReader(zSrc)
+	for {
+		header, err := tr.Next()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return err
+		}
+
+		srcPath := filepath.Clean(header.Name)
+		slash := strings.Index(srcPath, "/")
+		if slash == -1 { // strip leading path
+			continue
+		}
+		srcPath = srcPath[slash+1:]
+
+		dstPath := filepath.Join(dst, srcPath)
+		info := header.FileInfo()
+		if info.IsDir() {
+			if e := os.MkdirAll(dstPath, info.Mode()); e != nil {
+				return e
+			}
+			continue
+		}
+
+		if e := extractFile(dstPath, tr, info.Mode()); e != nil {
+			return e
+		}
 	}
-	return zr, nil
+	return nil
 }
 
-// Untar unarchives, stripping the base directory inside the "src" archive. Ex on "/foo/bar", "dst" will have "bar/**"
-func Untar(dst string, src io.Reader) error { // dst, src order like io.Copy
-	// No support for streaming https://github.com/mholt/archiver/pull/199
-	tmpDir, err := ioutil.TempDir("", "getenvoy-")
-	if err != nil {
-		return err
+// newDecompressor returns an "xz" or "gzip" decompression function based on bytes in the stream.
+func newDecompressor(r io.Reader) (io.ReadCloser, error) {
+	br := bufio.NewReader(r)
+	h, err := br.Peek(xz.HeaderLen)
+	if err != nil { // This is only used to decompress Envoy, so a valid stream will never be this short.
+		return nil, err
+	}
+	if xz.ValidHeader(h) {
+		xzr, e := xz.NewReader(br)
+		if e != nil {
+			return nil, err
+		}
+		return io.NopCloser(xzr), nil
 	}
-	defer os.RemoveAll(tmpDir) //nolint
+	return gzip.NewReader(br)
+}
 
-	archive := filepath.Join(tmpDir, "archive.tar")
-	tarball, err := os.OpenFile(archive, os.O_CREATE|os.O_WRONLY, 0600) //nolint:gosec
+func extractFile(dst string, src io.Reader, perm os.FileMode) error {
+	file, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) //nolint:gosec
 	if err != nil {
 		return err
 	}
-	defer tarball.Close() //nolint
-	if _, err = io.Copy(tarball, src); err != nil {
-		return err
-	}
-
-	tar := &archiver.Tar{MkdirAll: true, StripComponents: 1}
-	defer tar.Close() //nolint
-	return tar.Unarchive(archive, dst)
+	defer file.Close() //nolint
+	_, err = io.Copy(file, src)
+	return err
 }
 
-// Tar archives the source, rooted at the given directory.
-// Ex Given "src" includes "envoy-2/bin" and "build/bin". If "root" is "envoy-2", the archive includes "envoy-2/bin".
-func Tar(dst io.Writer, src, root string) error { // dst, src order like io.Copy
-	if root == "" {
-		return errors.New("root must not be empty")
-	}
+// TarGz tars and gzips "src", rooted at the last directory, into the file named "dst"
+// Ex If "src" includes "/tmp/envoy/bin" and "/tmp/build/bin". If "src" is "/tmp/envoy", "dst" includes "envoy/bin".
+//
+// This is used to compress the working directory of Envoy after it is stopped.
+func TarGz(dst, src string) error { //nolint dst, src order like io.Copy
+	srcFS := os.DirFS(filepath.Dir(src))
+	basePath := filepath.Base(src)
 
-	// No support for streaming https://github.com/mholt/archiver/pull/199
-	archive := filepath.Join(src, "archive.tar")
-	err := archiver.DefaultTar.Archive([]string{filepath.Join(src, root)}, archive)
+	// create the tar.gz file
+	file, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) //nolint:gosec
 	if err != nil {
 		return err
 	}
-	defer os.Remove(archive) //nolint
+	defer file.Close() //nolint
+	gzw := gzip.NewWriter(file)
+	defer gzw.Close() //nolint
+	tw := tar.NewWriter(gzw)
+	defer tw.Close() //nolint
+
+	// Recurse through the path including all files and directories
+	return fs.WalkDir(srcFS, basePath, func(path string, d os.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		info, err := d.Info()
+		if err != nil {
+			return err
+		}
+
+		// Make a header for the file or directory based on the current file
+		header, err := tar.FileInfoHeader(info, info.Name())
+		if err != nil {
+			return err
+		}
+
+		// Ensure the destination file starts at the intended path
+		header.Name = path
+		if err := tw.WriteHeader(header); err != nil {
+			return err
+		}
+
+		if info.IsDir() {
+			return nil // nothing to write
+		}
+		return copy(tw, srcFS, path)
+	})
+}
 
-	archiveFile, err := os.Open(archive) //nolint:gosec
+// Copy the contents of the file into the tar without buffering
+func copy(dst io.Writer, src fs.FS, path string) error { // dst, src order like io.Copy
+	f, err := src.Open(path) //nolint:gosec
 	if err != nil {
 		return err
 	}
-	_, err = io.Copy(dst, archiveFile)
+	defer f.Close() //nolint
+	_, err = io.Copy(dst, f)
 	return err
 }
diff --git a/pkg/binary/envoy/fetch.go b/pkg/binary/envoy/fetch.go
index 5e955b7..2694652 100644
--- a/pkg/binary/envoy/fetch.go
+++ b/pkg/binary/envoy/fetch.go
@@ -84,9 +84,9 @@ func verifyEnvoy(platformPath string) (string, error) {
 
 func untarEnvoy(dst, url string, out io.Writer) error { // dst, src order like io.Copy
 	// #nosec -> url can be anywhere by design
-	resp, err := transport.Get(url)
-	if err != nil {
-		return err
+	resp, e := transport.Get(url)
+	if e != nil {
+		return e
 	}
 	defer resp.Body.Close() //nolint
 
@@ -94,22 +94,24 @@ func untarEnvoy(dst, url string, out io.Writer) error { // dst, src order like i
 		return fmt.Errorf("received %v status code from %s", resp.StatusCode, url)
 	}
 
-	// Ensure there's a progress bar while extraction is taking place
-	bar := progressbar.NewOptions64(
-		resp.ContentLength,
-		progressbar.OptionSetWriter(out),
-		progressbar.OptionOnCompletion(func() {
-			fmt.Fprint(out, "\n")
-		}),
-	)
-	defer bar.Close() //nolint
-	src := progressbar.NewReader(resp.Body, bar)
+	// Ensure there's a progress while extraction is taking place
+	src := progressReader(out, resp.Body, resp.ContentLength)
+	defer src.Close() //nolint
 
-	zSrc, err := tar.NewDecompressor(url, &src)
-	if err != nil {
-		return err
+	if e = tar.Untar(dst, src); e != nil {
+		return fmt.Errorf("error untarring %s: %w", url, e)
 	}
-	defer zSrc.Close() //nolint
+	return nil
+}
 
-	return tar.Untar(dst, zSrc)
+// progressReader will show a spinner or progress bar, depending on if max == -1
+func progressReader(dst io.Writer, src io.Reader, max int64) io.ReadCloser {
+	b := progressbar.NewOptions64(max,
+		progressbar.OptionSetWriter(dst),
+		progressbar.OptionOnCompletion(func() {
+			fmt.Fprint(dst, "\n")
+		}),
+	)
+	br := progressbar.NewReader(src, b)
+	return &br
 }
diff --git a/pkg/binary/envoy/termination.go b/pkg/binary/envoy/termination.go
index 842d98f..08204f0 100644
--- a/pkg/binary/envoy/termination.go
+++ b/pkg/binary/envoy/termination.go
@@ -15,7 +15,6 @@
 package envoy
 
 import (
-	"compress/gzip"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -58,15 +57,7 @@ func (r *Runtime) handlePostTermination() error {
 	baseName := filepath.Base(r.GetWorkingDir())            // 1620955405964267000
 	targzName := filepath.Join(dirName, baseName+".tar.gz") // ~/.getenvoy/debug/1620955405964267000.tar.gz
 
-	targz, err := os.Create(targzName)
-	if err != nil {
-		return err
-	}
-	defer targz.Close() //nolint
-	zw := gzip.NewWriter(targz)
-	defer zw.Close() //nolint
-
-	if err = tar.Tar(zw, dirName, baseName); err != nil {
+	if err := tar.TarGz(targzName, r.GetWorkingDir()); err != nil {
 		return fmt.Errorf("unable to archive run directory %v: %w", r.GetWorkingDir(), err)
 	}
 	return os.RemoveAll(r.GetWorkingDir())


Test patch:
diff --git a/internal/tar/tar_test.go b/internal/tar/tar_test.go
index 8b86c10..6ede0bc 100644
--- a/internal/tar/tar_test.go
+++ b/internal/tar/tar_test.go
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package tar_test
+package tar
 
 import (
+	"bytes"
+	"fmt"
 	"io"
 	"os"
 	"path"
@@ -24,39 +26,46 @@ import (
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/tetratelabs/getenvoy/internal/tar"
 	"github.com/tetratelabs/getenvoy/pkg/test/morerequire"
 )
 
 func TestNewDecompressor_Validates(t *testing.T) {
-	tests := []struct{ name, src, path, expectedErr string }{
+	tests := []struct {
+		name        string
+		junk        []byte
+		expectedErr string
+	}{
 		{
-			name:        "not a gz",
-			src:         "empty.tar.gz",
-			path:        "testdata/empty.tar.xz",
-			expectedErr: "not a valid gz stream empty.tar.gz: gzip: invalid header",
+			name:        "empty",
+			junk:        []byte{},
+			expectedErr: "EOF",
 		},
 		{
-			name:        "not an xz",
-			src:         "empty.tar.xz",
-			path:        "testdata/empty.tar.gz",
-			expectedErr: "not a valid xz stream empty.tar.xz: xz: invalid header magic bytes",
+			name:        "short and invalid",
+			junk:        []byte{1, 2, 3, 4},
+			expectedErr: "EOF",
+		},
+		{
+			name:        "longer than xz header and invalid",
+			junk:        []byte("mary had a little lamb"),
+			expectedErr: "gzip: invalid header",
 		},
 	}
 
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			f, e := os.Open(tt.path)
-			require.NoError(t, e)
-			defer f.Close()
-
-			_, e = tar.NewDecompressor(tt.src, f)
-			require.EqualError(t, e, tt.expectedErr)
+			_, e := newDecompressor(bytes.NewReader(tc.junk))
+			require.EqualError(t, e, tc.expectedErr)
 		})
 	}
 }
 
+// TestNewDecompressor shows we can handle all compressed variants of Envoy, even accidentally empty files.
+//
+// As of May 2021, here are example values:
+// * "getenvoy-envoy-1.17.1.p0.gd6a4496-1p74.gbb8060d-darwin-release-x86_64.tar.xz"
+// * "getenvoy-1.11.0-bf169f9-af8a2e7-darwin-release-x86_64.tar.gz"
 func TestNewDecompressor(t *testing.T) {
 	for _, p := range []string{
 		"testdata/empty.tar.xz",
@@ -73,7 +82,7 @@ func TestNewDecompressor(t *testing.T) {
 			want, e := os.ReadFile(strings.TrimSuffix(p, path.Ext(p)))
 			require.NoError(t, e)
 
-			d, e := tar.NewDecompressor(p, f)
+			d, e := newDecompressor(f)
 			require.NoError(t, e)
 			defer d.Close()
 
@@ -85,51 +94,75 @@ func TestNewDecompressor(t *testing.T) {
 	}
 }
 
+// For simplicity, TestUntar only tests "xz" format. TestNewDecompressor already shows it handles "gz"
 func TestUntar(t *testing.T) {
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-	defer removeTempDir()
-
-	f, e := os.Open(filepath.Join("testdata", "test.tar"))
-	require.NoError(t, e)
-	defer f.Close()
+	for _, tt := range []struct {
+		dstExists bool
+		emptyTar  bool
+	}{{true, true}, {true, false}, {false, true}, {false, false}} {
+		tt := tt
+		t.Run(fmt.Sprintf("%+v", tt), func(t *testing.T) {
+			tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
+			defer removeTempDir()
+
+			dst := tempDir
+			if !tt.dstExists {
+				dst = filepath.Join(tempDir, "new")
+			}
+
+			srcFile := filepath.Join("testdata", "test.tar.xz")
+			if tt.emptyTar {
+				srcFile = filepath.Join("testdata", "empty.tar.xz")
+			}
+			f, e := os.Open(srcFile)
+			require.NoError(t, e)
+			defer f.Close()
 
-	e = tar.Untar(tempDir, f)
-	require.NoError(t, e)
+			e = Untar(dst, f)
+			require.NoError(t, e)
 
-	requireTestFiles(t, tempDir)
+			if tt.emptyTar {
+				requireEmptyDirectory(t, dst)
+			} else {
+				requireTestFiles(t, dst)
+			}
+		})
+	}
 }
 
 // requireTestFiles ensures the given directory includes the testdata/foo directory
-func requireTestFiles(t *testing.T, tempDir string) {
+func requireTestFiles(t *testing.T, dst string) {
 	for _, path := range []string{"bar.sh", "bar/baz.txt"} {
 		want, e := os.Stat(filepath.Join("testdata", "foo", path))
 		require.NoError(t, e)
-		have, e := os.Stat(filepath.Join(tempDir, path))
+		have, e := os.Stat(filepath.Join(dst, path))
 		require.NoError(t, e)
 		require.Equal(t, want.Mode(), have.Mode())
 	}
 }
 
-func TestTar(t *testing.T) {
+// requireTestFiles ensures the given directory is empty
+func requireEmptyDirectory(t *testing.T, dst string) {
+	d, e := os.ReadDir(dst)
+	require.NoError(t, e)
+	require.Empty(t, d, e)
+}
+
+func TestTarGZ(t *testing.T) {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
-	f, e := os.Open(requireTarTestData(t, tempDir))
+	src := filepath.Join("testdata", "foo")
+	dst := filepath.Join(tempDir, "test.tar.gz")
+	e := TarGz(dst, src)
 	require.NoError(t, e)
 
-	dst := filepath.Join(tempDir, "out")
-	e = tar.Untar(dst, f)
+	f, e := os.Open(dst)
 	require.NoError(t, e)
+	defer f.Close() //nolint
 
-	requireTestFiles(t, dst)
-}
-
-func requireTarTestData(t *testing.T, tempDir string) string {
-	f, e := os.Create(filepath.Join(tempDir, "test.tar"))
+	e = Untar(tempDir, f)
 	require.NoError(t, e)
-	defer f.Close()
 
-	e = tar.Tar(f, "testdata", "foo")
-	require.NoError(t, e)
-	return f.Name()
+	requireTestFiles(t, tempDir)
 }
diff --git a/pkg/binary/envoy/fetch_test.go b/pkg/binary/envoy/fetch_test.go
index a408fe6..861cf18 100644
--- a/pkg/binary/envoy/fetch_test.go
+++ b/pkg/binary/envoy/fetch_test.go
@@ -16,7 +16,6 @@ package envoy
 
 import (
 	"bytes"
-	"compress/gzip"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -27,9 +26,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"github.com/ulikunitz/xz"
 
-	"github.com/tetratelabs/getenvoy/internal/tar"
 	"github.com/tetratelabs/getenvoy/pkg/globals"
 	manifesttest "github.com/tetratelabs/getenvoy/pkg/test/manifest"
 	"github.com/tetratelabs/getenvoy/pkg/test/morerequire"
@@ -57,7 +54,7 @@ func TestUntarEnvoyError(t *testing.T) {
 	}))
 	defer server.Close()
 
-	url := server.URL + "/file.tar.xz"
+	url := server.URL + "/file.tar.gz"
 	t.Run("error on incorrect URL", func(t *testing.T) {
 		e := untarEnvoy(dst, url, io.Discard)
 		require.EqualError(t, e, fmt.Sprintf(`received 404 status code from %s`, url))
@@ -68,7 +65,7 @@ func TestUntarEnvoyError(t *testing.T) {
 	}
 	t.Run("error on empty", func(t *testing.T) {
 		e := untarEnvoy(dst, url, io.Discard)
-		require.EqualError(t, e, fmt.Sprintf(`not a valid xz stream %s: unexpected EOF`, url))
+		require.EqualError(t, e, fmt.Sprintf(`error untarring %s: EOF`, url))
 	})
 
 	realHandler = func(w http.ResponseWriter, _ *http.Request) {
@@ -77,61 +74,20 @@ func TestUntarEnvoyError(t *testing.T) {
 	}
 	t.Run("error on not a tar", func(t *testing.T) {
 		e := untarEnvoy(dst, url, io.Discard)
-		require.EqualError(t, e, fmt.Sprintf(`not a valid xz stream %s: xz: invalid header magic bytes`, url))
+		require.EqualError(t, e, fmt.Sprintf(`error untarring %s: gzip: invalid header`, url))
 	})
 }
 
+// TestUntarEnvoy doesn't test compression formats because that logic is in tar.Tar
 func TestUntarEnvoy(t *testing.T) {
-	tests := []struct {
-		extension string
-		path      string
-	}{
-		{
-			extension: "tar.xz", // As of May 2021 all releases were compressed using xz
-			path:      "getenvoy-envoy-1.17.1.p0.gd6a4496-1p74.gbb8060d-darwin-release-x86_64",
-		},
-		{
-			extension: "tar.gz", // The very first release of envoy was compressed with gz
-			path:      "getenvoy-1.11.0-bf169f9-af8a2e7-darwin-release-x86_64",
-		},
-	}
+	o, cleanup := setupTest(t)
+	defer cleanup()
 
-	for _, test := range tests {
-		test := test // pin! see https://github.com/kyoh86/scopelint for why
-		t.Run(test.extension, func(t *testing.T) {
-			dstDir, removeDstDir := morerequire.RequireNewTempDir(t)
-			defer removeDstDir()
-
-			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
-				w.WriteHeader(200)
-				var zw io.WriteCloser
-				if test.extension == "tar.xz" {
-					zw, _ = xz.NewWriter(w)
-				} else {
-					zw = gzip.NewWriter(w)
-				}
-				defer zw.Close() //nolint
-
-				tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
-				defer removeTempDir()
-				binDir := filepath.Join(tempDir, test.path, "bin")
-				require.NoError(t, os.MkdirAll(binDir, 0700))
-				require.NoError(t, ioutil.WriteFile(filepath.Join(binDir, "envoy"), []byte("fake"), 0700))
-				require.NoError(t, tar.Tar(zw, tempDir, test.path))
-			}))
-			defer server.Close()
-
-			url := fmt.Sprintf(`%s/tetrate/getenvoy/%s.%s`, server.URL, test.path, test.extension)
-
-			dst := filepath.Join(dstDir, "dst")
-
-			out := new(bytes.Buffer)
-			e := untarEnvoy(dst, url, out)
-			require.NoError(t, e)
-			require.Contains(t, out.String(), `100% |████████████████████████████████████████|`)
-			require.FileExists(t, filepath.Join(dst, binEnvoy))
-		})
-	}
+	out := new(bytes.Buffer)
+	e := untarEnvoy(o.tempDir, o.envoyURL, out)
+	require.NoError(t, e)
+	require.FileExists(t, filepath.Join(o.tempDir, binEnvoy))
+	require.Contains(t, out.String(), `100% |████████████████████████████████████████|`)
 }
 
 func TestFetchIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
@@ -144,6 +100,28 @@ func TestFetchIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
 	require.Empty(t, o.Out.(*bytes.Buffer))
 }
 
+// progressReader emits a progress bar, when the length is known
+func TestProgressReader(t *testing.T) {
+	var out bytes.Buffer
+	b := []byte{1, 2, 3, 4}
+	br := progressReader(&out, bytes.NewReader(b), int64(len(b)))
+	_, e := io.ReadAll(br)
+
+	require.NoError(t, e)
+	require.Contains(t, out.String(), "100% |████████████████████████████████████████|")
+}
+
+// progressReader emits a spinner with download rate when it doesn't know the length
+func TestProgressReader_UnknownLength(t *testing.T) {
+	var out bytes.Buffer
+	b := []byte{1, 2, 3, 4}
+	br := progressReader(&out, bytes.NewReader(b), -1)
+	_, e := io.ReadAll(br)
+
+	require.NoError(t, e)
+	require.NotContains(t, out.String(), "100% |████████████████████████████████████████|")
+}
+
 func TestFetchIfNeeded(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
@@ -151,11 +129,11 @@ func TestFetchIfNeeded(t *testing.T) {
 
 	envoyPath, e := FetchIfNeeded(&o.GlobalOpts, o.reference)
 	require.NoError(t, e)
-	require.Contains(t, out.String(), o.envoyURL)
-	require.Contains(t, out.String(), "100% |████████████████████████████████████████|")
-
 	require.Equal(t, o.EnvoyPath, envoyPath)
 	require.FileExists(t, envoyPath)
+
+	require.Contains(t, out.String(), o.envoyURL)
+	require.Contains(t, out.String(), "100% |████████████████████████████████████████|")
 }
 
 func TestFetchIfNeeded_AlreadyExists(t *testing.T) {
@@ -210,14 +188,14 @@ func TestVerifyEnvoy(t *testing.T) {
 
 type manifestTest struct {
 	globals.GlobalOpts
-	reference, envoyURL string
+	tempDir, reference, envoyURL string
 }
 
 func setupTest(t *testing.T) (*manifestTest, func()) {
 	var tearDown []func()
 
-	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
-	tearDown = append(tearDown, removeHomeDir)
+	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
+	tearDown = append(tearDown, removeTempDir)
 
 	ref := fmt.Sprintf("standard:%s/%s", version, platform)
 	m, err := manifesttest.NewSimpleManifest(ref)
@@ -226,14 +204,15 @@ func setupTest(t *testing.T) (*manifestTest, func()) {
 	tearDown = append(tearDown, manifestServer.Close)
 
 	return &manifestTest{
+			tempDir:   tempDir,
 			reference: ref,
-			envoyURL:  fmt.Sprintf("%s/builds/%s/%s.tar.xz", manifestServer.URL, version, platform),
+			envoyURL:  fmt.Sprintf("%s/builds/%s/%s.tar.gz", manifestServer.URL, version, platform),
 			GlobalOpts: globals.GlobalOpts{
-				HomeDir:     homeDir,
+				HomeDir:     tempDir,
 				ManifestURL: manifestServer.URL + "/manifest.json",
 				Out:         new(bytes.Buffer),
 				RunOpts: globals.RunOpts{
-					EnvoyPath: filepath.Join(homeDir, "builds", "standard", version, platform, "bin", "envoy"),
+					EnvoyPath: filepath.Join(tempDir, "builds", "standard", version, platform, "bin", "envoy"),
 				},
 			},
 		}, func() {
diff --git a/pkg/test/manifest/manifest.go b/pkg/test/manifest/manifest.go
index 3535506..3748150 100644
--- a/pkg/test/manifest/manifest.go
+++ b/pkg/test/manifest/manifest.go
@@ -15,8 +15,6 @@
 package manifest
 
 import (
-	"fmt"
-
 	"github.com/tetratelabs/getenvoy/pkg/manifest"
 )
 
@@ -53,7 +51,7 @@ func NewSimpleManifest(reference string) (*manifest.Manifest, error) {
 	for _, platform := range platforms {
 		version.Builds[platform] = &manifest.Build{
 			Platform:            platform,
-			DownloadLocationURL: fmt.Sprintf("%s.tar.xz", ref.String()),
+			DownloadLocationURL: ref.String(),
 		}
 	}
 	return m, nil
diff --git a/pkg/test/manifest/server.go b/pkg/test/manifest/server.go
index 1d61149..d46519c 100644
--- a/pkg/test/manifest/server.go
+++ b/pkg/test/manifest/server.go
@@ -26,14 +26,15 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"github.com/ulikunitz/xz"
 
 	"github.com/tetratelabs/getenvoy/internal/tar"
 	"github.com/tetratelabs/getenvoy/pkg/manifest"
 	"github.com/tetratelabs/getenvoy/pkg/test/morerequire"
 )
 
-const archiveFormat = ".tar.xz"
+// Even though currently binaries are compressed with "xz" more than "gz", using "gz" in tests allows us to re-use
+// tar.TarGz instead of complicating internal utilities or adding dependencies only for tests.
+const archiveFormat = ".tar.gz"
 const buildsPath = "/builds/"
 
 // RequireManifestTestServer serves "/manifest.json", which contains download links a compressed archive of artifactDir
@@ -63,8 +64,10 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		ref, err := manifest.ParseReference(reference)
 		require.NoError(s.t, err, "could not parse reference from uri %q", reference)
 		require.Regexpf(s.t, `^1\.[1-9][0-9]\.[0-9]+$`, ref.Version, "unsupported version in uri %q", reference)
+
 		w.WriteHeader(http.StatusOK)
-		s.writeFakeEnvoyTarXz(w, ref.Version)
+		_, e := w.Write(requireFakeEnvoyTarGz(s.t, ref.Version))
+		require.NoError(s.t, e)
 	default:
 		w.WriteHeader(http.StatusNotFound)
 	}
@@ -87,10 +90,13 @@ func (s *server) rewriteDownloadLocations(baseURL string, m manifest.Manifest) {
 	s.manifest = &m
 }
 
-func (s *server) writeFakeEnvoyTarXz(w io.Writer, version string) {
-	tempDir, removeTempDir := morerequire.RequireNewTempDir(s.t)
+func requireFakeEnvoyTarGz(t *testing.T, version string) []byte {
+	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
-	require.NoError(s.t, os.MkdirAll(filepath.Join(tempDir, version, "bin"), 0700)) //nolint:gosec
+
+	// construct the platform directory based on the input version
+	platformDir := filepath.Join(tempDir, version)
+	require.NoError(t, os.MkdirAll(filepath.Join(platformDir, "bin"), 0700)) //nolint:gosec
 	// go:embed doesn't allow us to retain execute bit, so it is simpler to inline this.
 	fakeEnvoy := []byte(`#!/bin/sh
 set -ue
@@ -100,11 +106,18 @@ echo envoy bin: $0
 echo envoy args: $@
 echo >&2 envoy stderr
 `)
-	require.NoError(s.t, os.WriteFile(filepath.Join(tempDir, version, "bin", "envoy"), fakeEnvoy, 0700)) //nolint:gosec
+	require.NoError(t, os.WriteFile(filepath.Join(platformDir, "bin", "envoy"), fakeEnvoy, 0700)) //nolint:gosec
 
-	zw, err := xz.NewWriter(w)
-	require.NoError(s.t, err)
-	defer zw.Close() //nolint
-	err = tar.Tar(zw, tempDir, version)
-	require.NoError(s.t, err)
+	// tar.gz the platform dir
+	tempGz := filepath.Join(tempDir, "envoy.tar.gz")
+	e := tar.TarGz(tempGz, platformDir)
+	require.NoError(t, e)
+
+	// Read the tar.gz into a byte array. This allows the mock server to set content length correctly
+	f, e := os.Open(tempGz) //nolint:gosec
+	require.NoError(t, e)
+	defer f.Close() // nolint
+	b, e := io.ReadAll(f)
+	require.NoError(t, e)
+	return b
 }
diff --git a/test/e2e/getenvoy_run_test.go b/test/e2e/getenvoy_run_test.go
index 87100fc..a0118ea 100644
--- a/test/e2e/getenvoy_run_test.go
+++ b/test/e2e/getenvoy_run_test.go
@@ -101,11 +101,7 @@ func verifyDebugDump(t *testing.T, workingDir string, c interface{}) {
 
 	src, err := os.Open(debugArchive)
 	require.NoError(t, err, "error opening %s after stopping [%v]", debugArchive, c)
-	zSrc, err := tar.NewDecompressor(debugArchive, src)
-	require.NoError(t, err, "error getting decompressor for %s after stopping [%v]", debugArchive, c)
-	defer zSrc.Close() //nolint
-
-	e := tar.Untar(workingDir, zSrc)
+	e := tar.Untar(workingDir, src)
 	require.NoError(t, e, "error restoring %s from %s after stopping [%v]", workingDir, debugArchive, c)
 
 	// ensure the minimum contents exist