From 14f6985fe34e64b3190b0d64e60945075723702f Mon Sep 17 00:00:00 2001 From: Malte Poll Date: Thu, 19 May 2022 17:12:03 +0200 Subject: [PATCH] Implement binary file installer & extractor Signed-off-by: Malte Poll --- coordinator/kubernetes/k8sapi/install.go | 214 ++++++ coordinator/kubernetes/k8sapi/install_test.go | 632 ++++++++++++++++++ go.mod | 6 +- go.sum | 4 + go.work.sum | 1 + 5 files changed, 856 insertions(+), 1 deletion(-) create mode 100644 coordinator/kubernetes/k8sapi/install.go create mode 100644 coordinator/kubernetes/k8sapi/install_test.go diff --git a/coordinator/kubernetes/k8sapi/install.go b/coordinator/kubernetes/k8sapi/install.go new file mode 100644 index 000000000..cdca804a5 --- /dev/null +++ b/coordinator/kubernetes/k8sapi/install.go @@ -0,0 +1,214 @@ +package k8sapi + +import ( + "archive/tar" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path" + + "github.com/spf13/afero" + "golang.org/x/text/transform" +) + +// osInstaller installs binary components of supported kubernetes versions. +type osInstaller struct { + fs *afero.Afero + hClient httpClient +} + +// newOSInstaller creates a new osInstaller. +func newOSInstaller() *osInstaller { + return &osInstaller{ + fs: &afero.Afero{Fs: afero.NewOsFs()}, + hClient: &http.Client{}, + } +} + +// Install downloads a resource from a URL, applies any given text transformations and extracts the resulting file if required. +// The resulting file(s) are copied to all destinations. +func (i *osInstaller) Install( + ctx context.Context, sourceURL string, destinations []string, perm fs.FileMode, + extract bool, transforms ...transform.Transformer, +) error { + tempPath, err := i.downloadToTempDir(ctx, sourceURL, transforms...) + if err != nil { + return err + } + defer func() { + _ = i.fs.Remove(tempPath) + }() + for _, destination := range destinations { + var err error + if extract { + err = i.extractArchive(tempPath, destination, perm) + } else { + err = i.copy(tempPath, destination, perm) + } + if err != nil { + return fmt.Errorf("installing from %q failed: copying to destination %q failed: %w", sourceURL, destination, err) + } + } + return nil +} + +// extractArchive extracts tar gz archives to a prefixed destination. +func (i *osInstaller) extractArchive(archivePath, prefix string, perm fs.FileMode) error { + archiveFile, err := i.fs.Open(archivePath) + if err != nil { + return fmt.Errorf("unable to open archive file: %w", err) + } + defer archiveFile.Close() + gzReader, err := gzip.NewReader(archiveFile) + if err != nil { + return fmt.Errorf("unable to read archive file as gzip: %w", err) + } + defer gzReader.Close() + if err := i.fs.MkdirAll(prefix, fs.ModePerm); err != nil { + return fmt.Errorf("unable to create prefix folder: %w", err) + } + tarReader := tar.NewReader(gzReader) + + for { + header, err := tarReader.Next() + if err == io.EOF { + return nil + } + if err != nil { + return fmt.Errorf("unable to parse tar header: %w", err) + } + if err := verifyTarPath(header.Name); err != nil { + return fmt.Errorf("invalid tar path %q: %w", header.Name, err) + } + switch header.Typeflag { + case tar.TypeDir: + if len(header.Name) == 0 { + return errors.New("cannot create dir for empty path") + } + if err := i.fs.Mkdir(path.Join(prefix, header.Name), fs.FileMode(header.Mode)&perm); err != nil && !errors.Is(err, os.ErrExist) { + return fmt.Errorf("unable to create folder: %w", err) + } + case tar.TypeReg: + if len(header.Name) == 0 { + return errors.New("cannot create file for empty path") + } + out, err := i.fs.OpenFile(path.Join(prefix, header.Name), os.O_WRONLY|os.O_CREATE, fs.FileMode(header.Mode)) + if err != nil { + return fmt.Errorf("unable to create file for writing: %w", err) + } + defer out.Close() + if _, err := io.Copy(out, tarReader); err != nil { + return fmt.Errorf("unable to write extracted file contents: %w", err) + } + case tar.TypeSymlink: + if err := verifyTarPath(header.Linkname); err != nil { + return fmt.Errorf("invalid tar path %q: %w", header.Linkname, err) + } + if len(header.Name) == 0 { + return errors.New("cannot symlink file for empty oldname") + } + if len(header.Linkname) == 0 { + return errors.New("cannot symlink file for empty newname") + } + if symlinker, ok := i.fs.Fs.(afero.Symlinker); ok { + if err := symlinker.SymlinkIfPossible(path.Join(prefix, header.Name), path.Join(prefix, header.Linkname)); err != nil { + return fmt.Errorf("creating symlink failed: %w", err) + } + } else { + return errors.New("fs does not support symlinks") + } + default: + return fmt.Errorf("unsupported tar record: %v", header.Typeflag) + } + } +} + +// downloadToTempDir downloads a file to a temporary location, applying transform on-the-fly. +func (i *osInstaller) downloadToTempDir(ctx context.Context, url string, transforms ...transform.Transformer) (string, error) { + out, err := afero.TempFile(i.fs, "", "") + if err != nil { + return "", fmt.Errorf("unable to create destination temp file: %w", err) + } + defer out.Close() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("request to download %q failed: %w", url, err) + } + resp, err := i.hClient.Do(req) + if err != nil { + return "", fmt.Errorf("request to download %q failed: %w", url, err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("request to download %q failed with status code: %v", url, resp.Status) + } + defer resp.Body.Close() + + transformReader := transform.NewReader(resp.Body, transform.Chain(transforms...)) + + if _, err = io.Copy(out, transformReader); err != nil { + return "", fmt.Errorf("downloading %q failed: %w", url, err) + } + return out.Name(), nil +} + +// copy copies a file from oldname to newname. +func (i *osInstaller) copy(oldname, newname string, perm fs.FileMode) (err error) { + old, openOldErr := i.fs.OpenFile(oldname, os.O_RDONLY, fs.ModePerm) + if openOldErr != nil { + return fmt.Errorf("unable to copy %q to %q: cannot open source file for reading: %w", oldname, newname, openOldErr) + } + defer func() { _ = old.Close() }() + // create destination path if not exists + if err := i.fs.MkdirAll(path.Dir(newname), fs.ModePerm); err != nil { + return fmt.Errorf("unable to copy %q to %q: unable to create destination folder: %w", oldname, newname, err) + } + new, openNewErr := i.fs.OpenFile(newname, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perm) + if openNewErr != nil { + return fmt.Errorf("unable to copy %q to %q: cannot open destination file for writing: %w", oldname, newname, openNewErr) + } + defer func() { + _ = new.Close() + if err != nil { + _ = i.fs.Remove(newname) + } + }() + if _, err := io.Copy(new, old); err != nil { + return fmt.Errorf("unable to copy %q to %q: copying file contents failed: %w", oldname, newname, err) + } + + return nil +} + +// verifyTarPath checks if a tar path is valid (must not contain ".." as path element). +func verifyTarPath(pat string) error { + n := len(pat) + r := 0 + for r < n { + switch { + case os.IsPathSeparator(pat[r]): + // empty path element + r++ + case pat[r] == '.' && (r+1 == n || os.IsPathSeparator(pat[r+1])): + // . element + r++ + case pat[r] == '.' && pat[r+1] == '.' && (r+2 == n || os.IsPathSeparator(pat[r+2])): + // .. element + return errors.New("path contains \"..\"") + default: + // skip to next path element + for r < n && !os.IsPathSeparator(pat[r]) { + r++ + } + } + } + return nil +} + +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} diff --git a/coordinator/kubernetes/k8sapi/install_test.go b/coordinator/kubernetes/k8sapi/install_test.go new file mode 100644 index 000000000..495f70ad8 --- /dev/null +++ b/coordinator/kubernetes/k8sapi/install_test.go @@ -0,0 +1,632 @@ +package k8sapi + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "context" + "io" + "io/fs" + "net" + "net/http" + "net/http/httptest" + "path" + "testing" + + "github.com/hashicorp/go-multierror" + "github.com/icholy/replace" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/transform" + "google.golang.org/grpc/test/bufconn" +) + +func TestInstall(t *testing.T) { + testCases := map[string]struct { + server httpBufconnServer + destination string + extract bool + transforms []transform.Transformer + readonly bool + wantErr bool + wantFiles map[string][]byte + }{ + "download works": { + server: newHTTPBufconnServerWithBody([]byte("file-contents")), + destination: "/destination", + wantFiles: map[string][]byte{"/destination": []byte("file-contents")}, + }, + "download with extract works": { + server: newHTTPBufconnServerWithBody(createTarGz([]byte("file-contents"), "/destination")), + destination: "/prefix", + extract: true, + wantFiles: map[string][]byte{"/prefix/destination": []byte("file-contents")}, + }, + "download with transform works": { + server: newHTTPBufconnServerWithBody([]byte("/usr/bin/kubelet")), + destination: "/destination", + transforms: []transform.Transformer{ + replace.String("/usr/bin", "/run/state/bin"), + }, + wantFiles: map[string][]byte{"/destination": []byte("/run/state/bin/kubelet")}, + }, + "download fails": { + server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }), + destination: "/destination", + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + defer tc.server.Close() + + hClient := http.Client{ + Transport: &http.Transport{ + DialContext: tc.server.DialContext, + Dial: tc.server.Dial, + DialTLSContext: tc.server.DialContext, + DialTLS: tc.server.Dial, + }, + } + + inst := osInstaller{ + fs: &afero.Afero{Fs: afero.NewMemMapFs()}, + hClient: &hClient, + } + err := inst.Install(context.Background(), "http://server/path", []string{tc.destination}, fs.ModePerm, tc.extract, tc.transforms...) + if tc.wantErr { + assert.Error(err) + return + } + + require.NoError(err) + for path, wantContents := range tc.wantFiles { + contents, err := inst.fs.ReadFile(path) + assert.NoError(err) + assert.Equal(wantContents, contents) + } + }) + } +} + +func TestExtractArchive(t *testing.T) { + tarGzTestFile := createTarGz([]byte("file-contents"), "/destination") + tarGzTestWithFolder := createTarGzWithFolder([]byte("file-contents"), "/folder/destination", nil) + + testCases := map[string]struct { + source string + destination string + contents []byte + readonly bool + wantErr bool + wantFiles map[string][]byte + }{ + "extract works": { + source: "in.tar.gz", + destination: "/prefix", + contents: tarGzTestFile, + wantFiles: map[string][]byte{ + "/prefix/destination": []byte("file-contents"), + }, + }, + "extract with folder works": { + source: "in.tar.gz", + destination: "/prefix", + contents: tarGzTestWithFolder, + wantFiles: map[string][]byte{ + "/prefix/folder/destination": []byte("file-contents"), + }, + }, + "source missing": { + source: "in.tar.gz", + destination: "/prefix", + wantErr: true, + }, + "non-gzip file contents": { + source: "in.tar.gz", + contents: []byte("invalid bytes"), + destination: "/prefix", + wantErr: true, + }, + "non-tar file contents": { + source: "in.tar.gz", + contents: createGz([]byte("file-contents")), + destination: "/prefix", + wantErr: true, + }, + "mkdir prefix dir fails on RO fs": { + source: "in.tar.gz", + contents: tarGzTestFile, + destination: "/prefix", + readonly: true, + wantErr: true, + }, + "mkdir tar dir fails on RO fs": { + source: "in.tar.gz", + contents: tarGzTestWithFolder, + destination: "/", + readonly: true, + wantErr: true, + }, + "writing tar file fails on RO fs": { + source: "in.tar.gz", + contents: tarGzTestFile, + destination: "/", + readonly: true, + wantErr: true, + }, + "symlink can be detected (but is unsupported on memmapfs)": { + source: "in.tar.gz", + contents: createTarGzWithSymlink("source", "dest"), + destination: "/prefix", + wantErr: true, + }, + "unsupported tar header type is detected": { + source: "in.tar.gz", + contents: createTarGzWithFifo("/destination"), + destination: "/prefix", + wantErr: true, + }, + "path traversal is detected": { + source: "in.tar.gz", + contents: createTarGz([]byte{}, "../destination"), + wantErr: true, + }, + "path traversal in symlink is detected": { + source: "in.tar.gz", + contents: createTarGzWithSymlink("/source", "../destination"), + wantErr: true, + }, + "empty file name is detected": { + source: "in.tar.gz", + contents: createTarGz([]byte{}, ""), + wantErr: true, + }, + "empty folder name is detected": { + source: "in.tar.gz", + contents: createTarGzWithFolder([]byte{}, "source", stringPtr("")), + wantErr: true, + }, + "empty symlink source is detected": { + source: "in.tar.gz", + contents: createTarGzWithSymlink("", "/target"), + wantErr: true, + }, + "empty symlink target is detected": { + source: "in.tar.gz", + contents: createTarGzWithSymlink("/source", ""), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + afs := afero.NewMemMapFs() + if len(tc.source) > 0 && len(tc.contents) > 0 { + require.NoError(afero.WriteFile(afs, tc.source, tc.contents, fs.ModePerm)) + } + + if tc.readonly { + afs = afero.NewReadOnlyFs(afs) + } + + inst := osInstaller{ + fs: &afero.Afero{Fs: afs}, + } + err := inst.extractArchive(tc.source, tc.destination, fs.ModePerm) + if tc.wantErr { + assert.Error(err) + return + } + + require.NoError(err) + for path, wantContents := range tc.wantFiles { + contents, err := inst.fs.ReadFile(path) + assert.NoError(err) + assert.Equal(wantContents, contents) + } + }) + } +} + +func TestDownloadToTempDir(t *testing.T) { + testCases := map[string]struct { + server httpBufconnServer + transforms []transform.Transformer + readonly bool + wantErr bool + wantFile []byte + }{ + "download works": { + server: newHTTPBufconnServerWithBody([]byte("file-contents")), + wantFile: []byte("file-contents"), + }, + "download with transform works": { + server: newHTTPBufconnServerWithBody([]byte("/usr/bin/kubelet")), + transforms: []transform.Transformer{ + replace.String("/usr/bin", "/run/state/bin"), + }, + wantFile: []byte("/run/state/bin/kubelet"), + }, + "download fails": { + server: newHTTPBufconnServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }), + wantErr: true, + }, + "creating temp file fails on RO fs": { + server: newHTTPBufconnServerWithBody([]byte("file-contents")), + readonly: true, + wantErr: true, + }, + "content length mismatch": { + server: newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Content-Length", "1337") + writer.WriteHeader(200) + }), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + defer tc.server.Close() + + hClient := http.Client{ + Transport: &http.Transport{ + DialContext: tc.server.DialContext, + Dial: tc.server.Dial, + DialTLSContext: tc.server.DialContext, + DialTLS: tc.server.Dial, + }, + } + + afs := afero.NewMemMapFs() + if tc.readonly { + afs = afero.NewReadOnlyFs(afs) + } + inst := osInstaller{ + fs: &afero.Afero{Fs: afs}, + hClient: &hClient, + } + path, err := inst.downloadToTempDir(context.Background(), "http://server/path", tc.transforms...) + if tc.wantErr { + assert.Error(err) + return + } + + require.NoError(err) + contents, err := inst.fs.ReadFile(path) + assert.NoError(err) + assert.Equal(tc.wantFile, contents) + }) + } +} + +func TestCopy(t *testing.T) { + contents := []byte("file-contents") + existingFile := "/source" + testCases := map[string]struct { + oldname string + newname string + perm fs.FileMode + readonly bool + wantErr bool + }{ + "copy works": { + oldname: existingFile, + newname: "/destination", + perm: fs.ModePerm, + }, + "oldname does not exist": { + oldname: "missing", + newname: "/destination", + wantErr: true, + }, + "copy on readonly fs fails": { + oldname: existingFile, + newname: "/destination", + perm: fs.ModePerm, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + afs := afero.NewMemMapFs() + require.NoError(afero.WriteFile(afs, existingFile, contents, fs.ModePerm)) + + if tc.readonly { + afs = afero.NewReadOnlyFs(afs) + } + + inst := osInstaller{fs: &afero.Afero{Fs: afs}} + err := inst.copy(tc.oldname, tc.newname, tc.perm) + if tc.wantErr { + assert.Error(err) + return + } + + require.NoError(err) + + oldfile, err := afs.Open(tc.oldname) + assert.NoError(err) + newfile, err := afs.Open(tc.newname) + assert.NoError(err) + + oldContents, _ := io.ReadAll(oldfile) + newContents, _ := io.ReadAll(newfile) + assert.Equal(oldContents, newContents) + + newStat, _ := newfile.Stat() + assert.Equal(tc.perm, newStat.Mode()) + }) + } +} + +func TestVerifyTarPath(t *testing.T) { + testCases := map[string]struct { + path string + wantErr bool + }{ + "valid relative path": { + path: "a/b/c", + }, + "valid absolute path": { + path: "/a/b/c", + }, + "valid path with dot": { + path: "/a/b/.d", + }, + "valid path with dots": { + path: "/a/b/..d", + }, + "single dot in path is allowed": { + path: ".", + }, + "simple path traversal": { + path: "..", + wantErr: true, + }, + "simple path traversal 2": { + path: "../", + wantErr: true, + }, + "simple path traversal 3": { + path: "/..", + wantErr: true, + }, + "simple path traversal 4": { + path: "/../", + wantErr: true, + }, + "complex relative path traversal": { + path: "a/b/c/../../../../c/d/e", + wantErr: true, + }, + "complex absolute path traversal": { + path: "/a/b/c/../../../../c/d/e", + wantErr: true, + }, + "path traversal at the end": { + path: "a/..", + wantErr: true, + }, + "path traversal at the end with trailing /": { + path: "a/../", + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + err := verifyTarPath(tc.path) + if tc.wantErr { + assert.Error(err) + return + } + require.NoError(err) + + assert.Equal(tc.path, path.Clean(tc.path)) + }) + } +} + +type httpBufconnServer struct { + *httptest.Server + *bufconn.Listener +} + +func (s *httpBufconnServer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + return s.Listener.DialContext(ctx) +} + +func (s *httpBufconnServer) Dial(network, addr string) (net.Conn, error) { + return s.Listener.Dial() +} + +func (s *httpBufconnServer) Close() { + s.Server.Close() + s.Listener.Close() +} + +func newHTTPBufconnServer(handlerFunc http.HandlerFunc) httpBufconnServer { + server := httptest.NewUnstartedServer(handlerFunc) + listener := bufconn.Listen(1024) + server.Listener = listener + server.Start() + return httpBufconnServer{ + Server: server, + Listener: listener, + } +} + +func newHTTPBufconnServerWithBody(body []byte) httpBufconnServer { + return newHTTPBufconnServer(func(writer http.ResponseWriter, request *http.Request) { + if _, err := writer.Write(body); err != nil { + panic(err) + } + }) +} + +func createTarGz(contents []byte, path string) []byte { + tgzWriter := newTarGzWriter() + defer func() { _ = tgzWriter.Close() }() + + if err := tgzWriter.writeHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: path, + Size: int64(len(contents)), + Mode: int64(fs.ModePerm), + }); err != nil { + panic(err) + } + if _, err := tgzWriter.writeTar(contents); err != nil { + panic(err) + } + + return tgzWriter.Bytes() +} + +func createTarGzWithFolder(contents []byte, pat string, dirnameOverride *string) []byte { + tgzWriter := newTarGzWriter() + defer func() { _ = tgzWriter.Close() }() + + dir := path.Dir(pat) + if dirnameOverride != nil { + dir = *dirnameOverride + } + + if err := tgzWriter.writeHeader(&tar.Header{ + Typeflag: tar.TypeDir, + Name: dir, + Mode: int64(fs.ModePerm), + }); err != nil { + panic(err) + } + if err := tgzWriter.writeHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: pat, + Size: int64(len(contents)), + Mode: int64(fs.ModePerm), + }); err != nil { + panic(err) + } + if _, err := tgzWriter.writeTar(contents); err != nil { + panic(err) + } + + return tgzWriter.Bytes() +} + +func createTarGzWithSymlink(oldname, newname string) []byte { + tgzWriter := newTarGzWriter() + defer func() { _ = tgzWriter.Close() }() + + if err := tgzWriter.writeHeader(&tar.Header{ + Typeflag: tar.TypeSymlink, + Name: oldname, + Linkname: newname, + Mode: int64(fs.ModePerm), + }); err != nil { + panic(err) + } + + return tgzWriter.Bytes() +} + +func createTarGzWithFifo(name string) []byte { + tgzWriter := newTarGzWriter() + defer func() { _ = tgzWriter.Close() }() + + if err := tgzWriter.writeHeader(&tar.Header{ + Typeflag: tar.TypeFifo, + Name: name, + Mode: int64(fs.ModePerm), + }); err != nil { + panic(err) + } + + return tgzWriter.Bytes() +} + +func createGz(contents []byte) []byte { + tgzWriter := newTarGzWriter() + defer func() { _ = tgzWriter.Close() }() + + if _, err := tgzWriter.writeGz(contents); err != nil { + panic(err) + } + + return tgzWriter.Bytes() +} + +type tarGzWriter struct { + buf *bytes.Buffer + bufWriter *bufio.Writer + gzWriter *gzip.Writer + tarWriter *tar.Writer +} + +func newTarGzWriter() *tarGzWriter { + var buf bytes.Buffer + bufWriter := bufio.NewWriter(&buf) + gzipWriter := gzip.NewWriter(bufWriter) + tarWriter := tar.NewWriter(gzipWriter) + + return &tarGzWriter{ + buf: &buf, + bufWriter: bufWriter, + gzWriter: gzipWriter, + tarWriter: tarWriter, + } +} + +func (w *tarGzWriter) writeHeader(hdr *tar.Header) error { + return w.tarWriter.WriteHeader(hdr) +} + +func (w *tarGzWriter) writeTar(b []byte) (int, error) { + return w.tarWriter.Write(b) +} + +func (w *tarGzWriter) writeGz(b []byte) (int, error) { + return w.gzWriter.Write(b) +} + +func (w *tarGzWriter) Bytes() []byte { + _ = w.tarWriter.Flush() + _ = w.gzWriter.Flush() + _ = w.gzWriter.Close() // required to ensure clean EOF in gz reader + _ = w.bufWriter.Flush() + return w.buf.Bytes() +} + +func (w *tarGzWriter) Close() (result error) { + if err := w.tarWriter.Close(); err != nil { + result = multierror.Append(result, err) + } + if err := w.gzWriter.Close(); err != nil { + result = multierror.Append(result, err) + } + return result +} + +func stringPtr(s string) *string { + return &s +} diff --git a/go.mod b/go.mod index 12b89462c..f176dbc36 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.2.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + github.com/hashicorp/go-multierror v1.1.1 github.com/kr/text v0.2.0 github.com/martinjungblut/go-cryptsetup v0.0.0-20220421194528-92e17766b2e7 github.com/schollz/progressbar/v3 v3.8.6 @@ -104,6 +105,8 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require github.com/hashicorp/errwrap v1.1.0 // indirect + require ( cloud.google.com/go v0.100.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect @@ -158,6 +161,7 @@ require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/icholy/replace v0.5.0 github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -201,7 +205,7 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.3.7 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.zx2c4.com/wireguard v0.0.0-20220202223031-3b95c81cc178 // indirect diff --git a/go.sum b/go.sum index d891e68ac..093833983 100644 --- a/go.sum +++ b/go.sum @@ -871,6 +871,7 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -881,6 +882,7 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= @@ -914,6 +916,8 @@ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icholy/replace v0.5.0 h1:Nx80zYQVlowdba+3Y6dvHDnmxaGtBrDlf2wYn9GyIXQ= +github.com/icholy/replace v0.5.0/go.mod h1:zzi8pxElj2t/5wHHHYmH45D+KxytX/t4w3ClY5nlK+g= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= diff --git a/go.work.sum b/go.work.sum index 150a80467..159cd6a9e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,5 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=