diff --git a/Gopkg.lock b/Gopkg.lock index 1016dd05..16ec85b9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -133,6 +133,12 @@ revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" version = "v1.0" +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66" + [[projects]] branch = "master" name = "github.com/nightlyone/lockfile" @@ -214,7 +220,7 @@ "internal/socks", "proxy" ] - revision = "1e491301e022f8f977054da4c2d852decd59571f" + revision = "db08ff08e8622530d9ed3a0e8ac279f6d4c02196" [[projects]] branch = "master" @@ -223,11 +229,11 @@ "unix", "windows" ] - revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb" + revision = "bff228c7b664c5fce602223a05fb708fd8654986" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "e6952a6950654188b2f9d5e3cb17fa28195513fc09b87d1145b3a1709f9aca43" + inputs-digest = "3772f28f3f9281dbe6c04a6fb717ab333dbd59045aefcb12bba9cae2eb8aed41" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index 40bd5602..fdde2771 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "//vendor/github.com/google/go-containerregistry/pkg/v1/daemon:go_default_library", "//vendor/github.com/google/go-containerregistry/pkg/v1/remote:go_default_library", "//vendor/github.com/google/go-containerregistry/pkg/v1/tarball:go_default_library", + "//vendor/github.com/mitchellh/go-homedir:go_default_library", "//vendor/github.com/sirupsen/logrus:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/spf13/pflag:go_default_library", diff --git a/cmd/analyze.go b/cmd/analyze.go index 3393794d..95a5e184 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -64,7 +64,7 @@ func analyzeImage(imageName string, analyzerArgs []string) error { return err } - if !save { + if noCache && !save { defer pkgutil.CleanupImage(image) } if err != nil { @@ -79,11 +79,11 @@ func analyzeImage(imageName string, analyzerArgs []string) error { return fmt.Errorf("Error performing image analysis: %s", err) } - output.PrintToStdErr("Retrieving analyses\n") + logrus.Info("retrieving analyses") outputResults(analyses) - if save { - logrus.Infof("Image was saved at %s", image.FSPath) + if noCache && save { + logrus.Infof("image was saved at %s", image.FSPath) } return nil diff --git a/cmd/diff.go b/cmd/diff.go index bac5b340..61eceab5 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -78,7 +78,7 @@ func diffImages(image1Arg, image2Arg string, diffArgs []string) error { var wg sync.WaitGroup wg.Add(2) - output.PrintToStdErr("Starting diff on images %s and %s, using differs: %s\n", image1Arg, image2Arg, diffArgs) + logrus.Infof("starting diff on images %s and %s, using differs: %s\n", image1Arg, image2Arg, diffArgs) imageMap := map[string]*pkgutil.Image{ image1Arg: {}, @@ -97,12 +97,12 @@ func diffImages(image1Arg, image2Arg string, diffArgs []string) error { } wg.Wait() - if !save { + if noCache && !save { defer pkgutil.CleanupImage(*imageMap[image1Arg]) defer pkgutil.CleanupImage(*imageMap[image2Arg]) } - output.PrintToStdErr("Computing diffs\n") + logrus.Info("computing diffs") req := differs.DiffRequest{ Image1: *imageMap[image1Arg], Image2: *imageMap[image2Arg], @@ -114,14 +114,14 @@ func diffImages(image1Arg, image2Arg string, diffArgs []string) error { outputResults(diffs) if filename != "" { - output.PrintToStdErr("Computing filename diffs\n") + logrus.Info("computing filename diffs") err := diffFile(imageMap[image1Arg], imageMap[image2Arg]) if err != nil { return err } } - if save { + if noCache && save { logrus.Infof("Images were saved at %s and %s", imageMap[image1Arg].FSPath, imageMap[image2Arg].FSPath) } diff --git a/cmd/root.go b/cmd/root.go index b00eb589..12c1cbaf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,8 +22,10 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "sort" "strings" + "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -35,14 +37,17 @@ import ( pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" "github.com/GoogleContainerTools/container-diff/util" "github.com/google/go-containerregistry/pkg/v1" + homedir "github.com/mitchellh/go-homedir" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" ) var json bool + var save bool var types diffTypes +var noCache bool var LogLevel string var format string @@ -125,14 +130,17 @@ func checkIfValidAnalyzer(_ []string) error { } func getImageForName(imageName string) (pkgutil.Image, error) { - logrus.Infof("getting image for name %s", imageName) + logrus.Infof("retrieving image: %s", imageName) var img v1.Image var err error if pkgutil.IsTar(imageName) { + start := time.Now() img, err = tarball.ImageFromPath(imageName, nil) if err != nil { return pkgutil.Image{}, err } + elapsed := time.Now().Sub(start) + logrus.Infof("retrieving image from tar took %f seconds", elapsed.Seconds()) } if strings.HasPrefix(imageName, DaemonPrefix) { @@ -144,10 +152,16 @@ func getImageForName(imageName string) (pkgutil.Image, error) { return pkgutil.Image{}, err } - img, err = daemon.Image(ref, &daemon.ReadOptions{}) + start := time.Now() + // TODO(nkubala): specify gzip.NoCompression here when functional options are supported + img, err = daemon.Image(ref, &daemon.ReadOptions{ + Buffer: true, + }) if err != nil { return pkgutil.Image{}, err } + elapsed := time.Now().Sub(start) + logrus.Infof("retrieving image from daemon took %f seconds", elapsed.Seconds()) } else { // either has remote prefix or has no prefix, in which case we force remote imageName = strings.Replace(imageName, RemotePrefix, "", -1) @@ -159,22 +173,27 @@ func getImageForName(imageName string) (pkgutil.Image, error) { if err != nil { return pkgutil.Image{}, err } + start := time.Now() img, err = remote.Image(ref, auth, http.DefaultTransport) if err != nil { return pkgutil.Image{}, err } + elapsed := time.Now().Sub(start) + logrus.Infof("retrieving remote image took %f seconds", elapsed.Seconds()) } - // TODO(nkubala): implement caching // create tempdir and extract fs into it var layers []pkgutil.Layer if includeLayers() { + start := time.Now() imgLayers, err := img.Layers() if err != nil { return pkgutil.Image{}, err } for _, layer := range imgLayers { - path, err := ioutil.TempDir("", strings.Replace(imageName, "/", "", -1)) + layerStart := time.Now() + digest, err := layer.Digest() + path, err := getExtractPathForName(digest.String()) if err != nil { return pkgutil.Image{ Layers: layers, @@ -188,12 +207,15 @@ func getImageForName(imageName string) (pkgutil.Image, error) { layers = append(layers, pkgutil.Layer{ FSPath: path, }) + elapsed := time.Now().Sub(layerStart) + logrus.Infof("time elapsed retrieving layer: %fs", elapsed.Seconds()) } + elapsed := time.Now().Sub(start) + logrus.Infof("time elapsed retrieving image layers: %fs", elapsed.Seconds()) } - path, err := ioutil.TempDir("", strings.Replace(imageName, "/", "", -1)) - if err != nil { - return pkgutil.Image{}, err - } + + path, err := getExtractPathForImage(imageName, img) + // extract fs into provided dir if err := pkgutil.GetFileSystemForImage(img, path, nil); err != nil { return pkgutil.Image{ FSPath: path, @@ -208,6 +230,44 @@ func getImageForName(imageName string) (pkgutil.Image, error) { }, nil } +func getExtractPathForImage(imageName string, image v1.Image) (string, error) { + start := time.Now() + digest, err := image.Digest() + if err != nil { + return "", err + } + elapsed := time.Now().Sub(start) + logrus.Infof("time elapsed retrieving image digest: %fs", elapsed.Seconds()) + return getExtractPathForName(pkgutil.RemoveTag(imageName) + "@" + digest.String()) +} + +func getExtractPathForName(name string) (string, error) { + var path string + var err error + if !noCache { + path, err = cacheDir(name) + if err != nil { + return "", err + } + // if cachedir doesn't exist, create it + if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + err = os.MkdirAll(path, 0700) + if err != nil { + return "", err + } + logrus.Infof("caching filesystem at %s", path) + } + } else { + // otherwise, create tempdir + logrus.Infof("skipping caching") + path, err = ioutil.TempDir("", strings.Replace(name, "/", "", -1)) + if err != nil { + return "", err + } + } + return path, nil +} + func includeLayers() bool { for _, t := range types { if t == "layer" { @@ -217,6 +277,16 @@ func includeLayers() bool { return false } +func cacheDir(imageName string) (string, error) { + dir, err := homedir.Dir() + if err != nil { + return "", err + } + rootDir := filepath.Join(dir, ".container-diff", "cache") + imageName = strings.Replace(imageName, string(os.PathSeparator), "", -1) + return filepath.Join(rootDir, filepath.Clean(imageName)), nil +} + func init() { RootCmd.PersistentFlags().StringVarP(&LogLevel, "verbosity", "v", "warning", "This flag controls the verbosity of container-diff.") RootCmd.PersistentFlags().StringVarP(&format, "format", "", "", "Format to output diff in.") @@ -254,4 +324,5 @@ func addSharedFlags(cmd *cobra.Command) { cmd.Flags().VarP(&types, "type", "t", "This flag sets the list of analyzer types to use. Set it repeatedly to use multiple analyzers.") cmd.Flags().BoolVarP(&save, "save", "s", false, "Set this flag to save rather than remove the final image filesystems on exit.") cmd.Flags().BoolVarP(&util.SortSize, "order", "o", false, "Set this flag to sort any file/package results by descending size. Otherwise, they will be sorted by name.") + cmd.Flags().BoolVarP(&noCache, "no-cache", "n", false, "Set this to force retrieval of image filesystem on each run.") } diff --git a/pkg/util/fs_utils.go b/pkg/util/fs_utils.go index b7c054e8..9ae105d4 100644 --- a/pkg/util/fs_utils.go +++ b/pkg/util/fs_utils.go @@ -18,6 +18,7 @@ package util import ( "bytes" + "io" "io/ioutil" "os" "path/filepath" @@ -191,3 +192,18 @@ func HasFilepathPrefix(path, prefix string) bool { } return true } + +// given a path to a directory, check if it has any contents +func DirIsEmpty(path string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err +} diff --git a/pkg/util/image_utils.go b/pkg/util/image_utils.go index b3889ec4..319a9f7f 100644 --- a/pkg/util/image_utils.go +++ b/pkg/util/image_utils.go @@ -33,6 +33,8 @@ import ( "github.com/sirupsen/logrus" ) +const tagRegexStr = ".*:([^/]+$)" + type Layer struct { FSPath string } @@ -83,7 +85,16 @@ func GetFileSystemForLayer(layer v1.Layer, root string, whitelist []string) erro } // unpack image filesystem to local disk +// if provided directory is not empty, do nothing func GetFileSystemForImage(image v1.Image, root string, whitelist []string) error { + empty, err := DirIsEmpty(root) + if err != nil { + return err + } + if !empty { + logrus.Infof("using cached filesystem in %s", root) + return nil + } if err := unpackTar(tar.NewReader(mutate.Extract(image)), root, whitelist); err != nil { return err } @@ -134,6 +145,17 @@ func copyToFile(outfile string, r io.Reader) error { // checks to see if an image string contains a tag. func HasTag(image string) bool { - tagRegex := regexp.MustCompile(".*:[^/]+$") + tagRegex := regexp.MustCompile(tagRegexStr) return tagRegex.MatchString(image) } + +// returns a raw image name with the tag removed +func RemoveTag(image string) string { + if !HasTag(image) { + return image + } + tagRegex := regexp.MustCompile(tagRegexStr) + parts := tagRegex.FindStringSubmatch(image) + tag := parts[len(parts)-1] + return image[0 : len(image)-len(tag)-1] +} diff --git a/pkg/util/tar_utils.go b/pkg/util/tar_utils.go index 427570c4..c1b23566 100644 --- a/pkg/util/tar_utils.go +++ b/pkg/util/tar_utils.go @@ -83,7 +83,7 @@ func unpackTar(tr *tar.Reader, path string, whitelist []string) error { // It's possible for a file to be included before the directory it's in is created. baseDir := filepath.Dir(target) if _, err := os.Stat(baseDir); os.IsNotExist(err) { - logrus.Debugf("baseDir %s for file %s does not exist. Creating.", baseDir, target) + logrus.Debugf("baseDir %s for file %s does not exist. Creating", baseDir, target) if err := os.MkdirAll(baseDir, 0755); err != nil { return err } @@ -91,8 +91,9 @@ func unpackTar(tr *tar.Reader, path string, whitelist []string) error { // It's possible we end up creating files that can't be overwritten based on their permissions. // Explicitly delete an existing file before continuing. if _, err := os.Stat(target); !os.IsNotExist(err) { - logrus.Debugf("Removing %s for overwrite.", target) + logrus.Debugf("Removing %s for overwrite", target) if err := os.Remove(target); err != nil { + logrus.Errorf("error removing file %s", target) return err } } @@ -117,7 +118,7 @@ func unpackTar(tr *tar.Reader, path string, whitelist []string) error { // It's possible we end up creating files that can't be overwritten based on their permissions. // Explicitly delete an existing file before continuing. if _, err := os.Stat(target); !os.IsNotExist(err) { - logrus.Debugf("Removing %s to create symlink.", target) + logrus.Debugf("Removing %s to create symlink", target) if err := os.RemoveAll(target); err != nil { logrus.Debugf("Unable to remove %s: %s", target, err) } @@ -139,7 +140,7 @@ func unpackTar(tr *tar.Reader, path string, whitelist []string) error { } for target, linkname := range hardlinks { - logrus.Info("Resolving hard links.") + logrus.Info("Resolving hard links") if _, err := os.Stat(linkname); !os.IsNotExist(err) { // If it exists, create the hard link if err := resolveHardlink(linkname, target); err != nil { diff --git a/tests/integration_test.go b/tests/integration_test.go index 7ca2dbe4..8d6b7ebb 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -106,7 +106,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: diffBase, imageB: diffModified, - differFlags: []string{"--type=file"}, + differFlags: []string{"--type=file", "--no-cache"}, expectedFile: "file_diff_expected.json", }, { @@ -114,7 +114,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: diffLayerBase, imageB: diffLayerModifed, - differFlags: []string{"--type=layer"}, + differFlags: []string{"--type=layer", "--no-cache"}, expectedFile: "file_layer_diff_expected.json", }, { @@ -122,7 +122,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: aptBase, imageB: aptModified, - differFlags: []string{"--type=apt"}, + differFlags: []string{"--type=apt", "--no-cache"}, expectedFile: "apt_diff_expected.json", }, // { @@ -138,7 +138,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: nodeBase, imageB: nodeModified, - differFlags: []string{"--type=node"}, + differFlags: []string{"--type=node", "--no-cache"}, expectedFile: "node_diff_order_expected.json", }, { @@ -146,7 +146,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: multiBase, imageB: multiModified, - differFlags: []string{"--type=node", "--type=pip", "--type=apt"}, + differFlags: []string{"--type=node", "--type=pip", "--type=apt", "--no-cache"}, expectedFile: "multi_diff_expected.json", }, { @@ -154,7 +154,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: multiBaseLocal, imageB: multiModifiedLocal, - differFlags: []string{"--type=node", "--type=pip", "--type=apt"}, + differFlags: []string{"--type=node", "--type=pip", "--type=apt", "--no-cache"}, expectedFile: "multi_diff_expected.json", }, { @@ -162,7 +162,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: diffBase, imageB: diffModified, - differFlags: []string{"--type=history"}, + differFlags: []string{"--type=history", "--no-cache"}, expectedFile: "hist_diff_expected.json", }, { @@ -170,7 +170,7 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: metadataBase, imageB: metadataModified, - differFlags: []string{"--type=metadata"}, + differFlags: []string{"--type=metadata", "--no-cache"}, expectedFile: "metadata_diff_expected.json", }, { @@ -178,14 +178,14 @@ func TestDiffAndAnalysis(t *testing.T) { subcommand: "diff", imageA: aptBase, imageB: aptModified, - differFlags: []string{"--type=apt", "-o"}, + differFlags: []string{"--type=apt", "-o", "--no-cache"}, expectedFile: "apt_sorted_diff_expected.json", }, { description: "apt analysis", subcommand: "analyze", imageA: aptModified, - differFlags: []string{"--type=apt"}, + differFlags: []string{"--type=apt", "--no-cache"}, expectedFile: "apt_analysis_expected.json", }, // { @@ -199,28 +199,28 @@ func TestDiffAndAnalysis(t *testing.T) { description: "file sorted analysis", subcommand: "analyze", imageA: diffModified, - differFlags: []string{"--type=file", "-o"}, + differFlags: []string{"--type=file", "-o", "--no-cache"}, expectedFile: "file_sorted_analysis_expected.json", }, { description: "file layer analysis", subcommand: "analyze", imageA: diffLayerBase, - differFlags: []string{"--type=layer"}, + differFlags: []string{"--type=layer", "--no-cache"}, expectedFile: "file_layer_analysis_expected.json", }, { description: "pip analysis", subcommand: "analyze", imageA: pipModified, - differFlags: []string{"--type=pip"}, + differFlags: []string{"--type=pip", "--no-cache"}, expectedFile: "pip_analysis_expected.json", }, { description: "node analysis", subcommand: "analyze", imageA: nodeModified, - differFlags: []string{"--type=node"}, + differFlags: []string{"--type=node", "--no-cache"}, expectedFile: "node_analysis_expected.json", }, } diff --git a/vendor/github.com/mitchellh/go-homedir/BUILD.bazel b/vendor/github.com/mitchellh/go-homedir/BUILD.bazel new file mode 100644 index 00000000..59049e5b --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["homedir.go"], + importpath = "github.com/mitchellh/go-homedir", + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["homedir_test.go"], + embed = [":go_default_library"], + importpath = "github.com/mitchellh/go-homedir", +) diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 00000000..f9c841a5 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md new file mode 100644 index 00000000..d70706d5 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/README.md @@ -0,0 +1,14 @@ +# go-homedir + +This is a Go library for detecting the user's home directory without +the use of cgo, so the library can be used in cross-compilation environments. + +Usage is incredibly simple, just call `homedir.Dir()` to get the home directory +for a user, and `homedir.Expand()` to expand the `~` in a path to the home +directory. + +**Why not just use `os/user`?** The built-in `os/user` package requires +cgo on Darwin systems. This means that any Go code that uses that package +cannot cross compile. But 99% of the time the use for `os/user` is just to +retrieve the home directory, which we can do for the current user without +cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go new file mode 100644 index 00000000..acbb605d --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir.go @@ -0,0 +1,155 @@ +package homedir + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" +) + +// DisableCache will disable caching of the home directory. Caching is enabled +// by default. +var DisableCache bool + +var homedirCache string +var cacheLock sync.RWMutex + +// Dir returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Dir() (string, error) { + if !DisableCache { + cacheLock.RLock() + cached := homedirCache + cacheLock.RUnlock() + if cached != "" { + return cached, nil + } + } + + cacheLock.Lock() + defer cacheLock.Unlock() + + var result string + var err error + if runtime.GOOS == "windows" { + result, err = dirWindows() + } else { + // Unix-like system, so just assume Unix + result, err = dirUnix() + } + + if err != nil { + return "", err + } + homedirCache = result + return result, nil +} + +// Expand expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~`, the path is +// returned as-is. +func Expand(path string) (string, error) { + if len(path) == 0 { + return path, nil + } + + if path[0] != '~' { + return path, nil + } + + if len(path) > 1 && path[1] != '/' && path[1] != '\\' { + return "", errors.New("cannot expand user-specific home dir") + } + + dir, err := Dir() + if err != nil { + return "", err + } + + return filepath.Join(dir, path[1:]), nil +} + +func dirUnix() (string, error) { + homeEnv := "HOME" + if runtime.GOOS == "plan9" { + // On plan9, env vars are lowercase. + homeEnv = "home" + } + + // First prefer the HOME environmental variable + if home := os.Getenv(homeEnv); home != "" { + return home, nil + } + + var stdout bytes.Buffer + + // If that fails, try OS specific commands + if runtime.GOOS == "darwin" { + cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) + cmd.Stdout = &stdout + if err := cmd.Run(); err == nil { + result := strings.TrimSpace(stdout.String()) + if result != "" { + return result, nil + } + } + } else { + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if err != exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd := exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func dirWindows() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + home = os.Getenv("USERPROFILE") + } + if home == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") + } + + return home, nil +} diff --git a/vendor/github.com/mitchellh/go-homedir/homedir_test.go b/vendor/github.com/mitchellh/go-homedir/homedir_test.go new file mode 100644 index 00000000..cd52127d --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir_test.go @@ -0,0 +1,129 @@ +package homedir + +import ( + "os" + "os/user" + "path/filepath" + "testing" +) + +func patchEnv(key, value string) func() { + bck := os.Getenv(key) + deferFunc := func() { + os.Setenv(key, bck) + } + + if value != "" { + os.Setenv(key, value) + } else { + os.Unsetenv(key) + } + + return deferFunc +} + +func BenchmarkDir(b *testing.B) { + // We do this for any "warmups" + for i := 0; i < 10; i++ { + Dir() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + Dir() + } +} + +func TestDir(t *testing.T) { + u, err := user.Current() + if err != nil { + t.Fatalf("err: %s", err) + } + + dir, err := Dir() + if err != nil { + t.Fatalf("err: %s", err) + } + + if u.HomeDir != dir { + t.Fatalf("%#v != %#v", u.HomeDir, dir) + } + + DisableCache = true + defer func() { DisableCache = false }() + defer patchEnv("HOME", "")() + dir, err = Dir() + if err != nil { + t.Fatalf("err: %s", err) + } + + if u.HomeDir != dir { + t.Fatalf("%#v != %#v", u.HomeDir, dir) + } +} + +func TestExpand(t *testing.T) { + u, err := user.Current() + if err != nil { + t.Fatalf("err: %s", err) + } + + cases := []struct { + Input string + Output string + Err bool + }{ + { + "/foo", + "/foo", + false, + }, + + { + "~/foo", + filepath.Join(u.HomeDir, "foo"), + false, + }, + + { + "", + "", + false, + }, + + { + "~", + u.HomeDir, + false, + }, + + { + "~foo/foo", + "", + true, + }, + } + + for _, tc := range cases { + actual, err := Expand(tc.Input) + if (err != nil) != tc.Err { + t.Fatalf("Input: %#v\n\nErr: %s", tc.Input, err) + } + + if actual != tc.Output { + t.Fatalf("Input: %#v\n\nOutput: %#v", tc.Input, actual) + } + } + + DisableCache = true + defer func() { DisableCache = false }() + defer patchEnv("HOME", "/custom/path/")() + expected := filepath.Join("/", "custom", "path", "foo/bar") + actual, err := Expand("~/foo/bar") + + if err != nil { + t.Errorf("No error is expected, got: %v", err) + } else if actual != expected { + t.Errorf("Expected: %v; actual: %v", expected, actual) + } +} diff --git a/vendor/golang.org/x/sys/unix/BUILD.bazel b/vendor/golang.org/x/sys/unix/BUILD.bazel index 3e937d2f..39c6be08 100644 --- a/vendor/golang.org/x/sys/unix/BUILD.bazel +++ b/vendor/golang.org/x/sys/unix/BUILD.bazel @@ -237,6 +237,7 @@ go_library( "asm_linux_386.s", "fcntl_linux_32bit.go", "syscall_linux_386.go", + "syscall_linux_gc_386.go", "zerrors_linux_386.go", "zptrace386_linux.go", "zsyscall_linux_386.go", diff --git a/vendor/golang.org/x/sys/unix/gccgo_c.c b/vendor/golang.org/x/sys/unix/gccgo_c.c index 24e96b11..46523ced 100644 --- a/vendor/golang.org/x/sys/unix/gccgo_c.c +++ b/vendor/golang.org/x/sys/unix/gccgo_c.c @@ -36,12 +36,3 @@ gccgoRealSyscallNoError(uintptr_t trap, uintptr_t a1, uintptr_t a2, uintptr_t a3 { return syscall(trap, a1, a2, a3, a4, a5, a6, a7, a8, a9); } - -// Define the use function in C so that it is not inlined. - -extern void use(void *) __asm__ (GOSYM_PREFIX GOPKGPATH ".use") __attribute__((noinline)); - -void -use(void *p __attribute__ ((unused))) -{ -} diff --git a/vendor/golang.org/x/sys/unix/syscall_linux_386.go b/vendor/golang.org/x/sys/unix/syscall_linux_386.go index bb8e4fbd..f17872f5 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux_386.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux_386.go @@ -10,7 +10,6 @@ package unix import ( - "syscall" "unsafe" ) @@ -157,10 +156,6 @@ func Setrlimit(resource int, rlim *Rlimit) (err error) { return setrlimit(resource, &rl) } -// Underlying system call writes to newoffset via pointer. -// Implemented in assembly to avoid allocation. -func seek(fd int, offset int64, whence int) (newoffset int64, err syscall.Errno) - func Seek(fd int, offset int64, whence int) (newoffset int64, err error) { newoffset, errno := seek(fd, offset, whence) if errno != 0 { @@ -206,9 +201,6 @@ const ( _SENDMMSG = 20 ) -func socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) -func rawsocketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) - func accept(s int, rsa *RawSockaddrAny, addrlen *_Socklen) (fd int, err error) { fd, e := socketcall(_ACCEPT, uintptr(s), uintptr(unsafe.Pointer(rsa)), uintptr(unsafe.Pointer(addrlen)), 0, 0, 0) if e != 0 { diff --git a/vendor/golang.org/x/sys/unix/syscall_linux_gc_386.go b/vendor/golang.org/x/sys/unix/syscall_linux_gc_386.go new file mode 100644 index 00000000..070bd389 --- /dev/null +++ b/vendor/golang.org/x/sys/unix/syscall_linux_gc_386.go @@ -0,0 +1,16 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux,!gccgo,386 + +package unix + +import "syscall" + +// Underlying system call writes to newoffset via pointer. +// Implemented in assembly to avoid allocation. +func seek(fd int, offset int64, whence int) (newoffset int64, err syscall.Errno) + +func socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) +func rawsocketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) diff --git a/vendor/golang.org/x/sys/unix/syscall_linux_gccgo_386.go b/vendor/golang.org/x/sys/unix/syscall_linux_gccgo_386.go new file mode 100644 index 00000000..308eb7ae --- /dev/null +++ b/vendor/golang.org/x/sys/unix/syscall_linux_gccgo_386.go @@ -0,0 +1,30 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux,gccgo,386 + +package unix + +import ( + "syscall" + "unsafe" +) + +func seek(fd int, offset int64, whence int) (int64, syscall.Errno) { + var newoffset int64 + offsetLow := uint32(offset & 0xffffffff) + offsetHigh := uint32((offset >> 32) & 0xffffffff) + _, _, err := Syscall6(SYS__LLSEEK, uintptr(fd), uintptr(offsetHigh), uintptr(offsetLow), uintptr(unsafe.Pointer(&newoffset)), uintptr(whence), 0) + return newoffset, err +} + +func socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (int, syscall.Errno) { + fd, _, err := Syscall(SYS_SOCKETCALL, uintptr(call), uintptr(unsafe.Pointer(&a0)), 0) + return int(fd), err +} + +func rawsocketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (int, syscall.Errno) { + fd, _, err := RawSyscall(SYS_SOCKETCALL, uintptr(call), uintptr(unsafe.Pointer(&a0)), 0) + return int(fd), err +} diff --git a/vendor/golang.org/x/sys/unix/syscall_linux_gccgo.go b/vendor/golang.org/x/sys/unix/syscall_linux_gccgo_arm.go similarity index 53% rename from vendor/golang.org/x/sys/unix/syscall_linux_gccgo.go rename to vendor/golang.org/x/sys/unix/syscall_linux_gccgo_arm.go index df9c1237..aa7fc9e1 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux_gccgo.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux_gccgo_arm.go @@ -2,9 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux -// +build gccgo -// +build 386 arm +// +build linux,gccgo,arm package unix @@ -13,9 +11,10 @@ import ( "unsafe" ) -func seek(fd int, offset int64, whence int) (newoffset int64, err syscall.Errno) { +func seek(fd int, offset int64, whence int) (int64, syscall.Errno) { + var newoffset int64 offsetLow := uint32(offset & 0xffffffff) offsetHigh := uint32((offset >> 32) & 0xffffffff) - _, _, err = Syscall6(SYS__LLSEEK, uintptr(fd), uintptr(offsetHigh), uintptr(offsetLow), uintptr(unsafe.Pointer(&newoffset)), uintptr(whence), 0) + _, _, err := Syscall6(SYS__LLSEEK, uintptr(fd), uintptr(offsetHigh), uintptr(offsetLow), uintptr(unsafe.Pointer(&newoffset)), uintptr(whence), 0) return newoffset, err } diff --git a/vendor/golang.org/x/sys/windows/security_windows.go b/vendor/golang.org/x/sys/windows/security_windows.go index f1ec5dc4..4f17a333 100644 --- a/vendor/golang.org/x/sys/windows/security_windows.go +++ b/vendor/golang.org/x/sys/windows/security_windows.go @@ -296,6 +296,7 @@ const ( TOKEN_ADJUST_PRIVILEGES TOKEN_ADJUST_GROUPS TOKEN_ADJUST_DEFAULT + TOKEN_ADJUST_SESSIONID TOKEN_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | @@ -305,7 +306,8 @@ const ( TOKEN_QUERY_SOURCE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | - TOKEN_ADJUST_DEFAULT + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY TOKEN_WRITE = STANDARD_RIGHTS_WRITE | TOKEN_ADJUST_PRIVILEGES | diff --git a/vendor/golang.org/x/sys/windows/syscall_windows_test.go b/vendor/golang.org/x/sys/windows/syscall_windows_test.go index 9c7133cc..0e27464e 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows_test.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows_test.go @@ -105,3 +105,9 @@ func ExampleLoadLibrary() { build := uint16(r >> 16) print("windows version ", major, ".", minor, " (Build ", build, ")\n") } + +func TestTOKEN_ALL_ACCESS(t *testing.T) { + if windows.TOKEN_ALL_ACCESS != 0xF01FF { + t.Errorf("TOKEN_ALL_ACCESS = %x, want 0xF01FF", windows.TOKEN_ALL_ACCESS) + } +}