diff --git a/.gitignore b/.gitignore index f0c64ac72..57f2db2a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _output/ _build/ bin/ +result diff --git a/Makefile b/Makefile index 92b795510..014e0867e 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ fmt: .PHONY: build build: - GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="$(LDFLAGS)" -o $(OUTPUT_DIR)/$(OUTPUT_BIN) ./cmd/colima + GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -ldflags="$(LDFLAGS)" -o $(OUTPUT_DIR)/$(OUTPUT_BIN) ./cmd/colima cd $(OUTPUT_DIR) && openssl sha256 -r -out $(OUTPUT_BIN).sha256sum $(OUTPUT_BIN) .PHONY: test @@ -59,3 +59,9 @@ install: .PHONY: lint lint: ## Assumes that golangci-lint is installed and in the path. To install: https://golangci-lint.run/usage/install/ golangci-lint --timeout 3m run + +.PHONY: nix-derivation-shell +nix-derivation-shell: + $(eval DERIVATION=$(shell nix-build)) + echo $(DERIVATION) | grep ^/nix + nix-shell -p $(DERIVATION) diff --git a/cmd/daemon/daemon.go b/cmd/daemon/daemon.go index 66ad0be25..6255099d8 100644 --- a/cmd/daemon/daemon.go +++ b/cmd/daemon/daemon.go @@ -13,6 +13,7 @@ import ( "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/daemon/process" + "github.com/abiosoft/colima/util/fsutil" godaemon "github.com/sevlyar/go-daemon" "github.com/sirupsen/logrus" ) @@ -22,7 +23,7 @@ var dir = process.Dir // daemonize creates the daemon and returns if this is a child process func daemonize() (ctx *godaemon.Context, child bool, err error) { dir := dir() - if err := os.MkdirAll(dir, 0755); err != nil { + if err := fsutil.MkdirAll(dir, 0755); err != nil { return nil, false, fmt.Errorf("cannot make dir: %w", err) } diff --git a/cmd/nerdctl.go b/cmd/nerdctl.go index f3542ba00..9dc8c81bb 100644 --- a/cmd/nerdctl.go +++ b/cmd/nerdctl.go @@ -13,6 +13,8 @@ import ( "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/container/containerd" + "github.com/abiosoft/colima/util/fsutil" + "github.com/abiosoft/colima/util/osutil" "github.com/spf13/cobra" ) @@ -96,7 +98,7 @@ var nerdctlLinkFunc = func() *cobra.Command { ColimaApp string Profile string }{ - ColimaApp: os.Args[0], + ColimaApp: osutil.Executable(), Profile: config.CurrentProfile().ShortName, } var buf bytes.Buffer @@ -112,7 +114,7 @@ var nerdctlLinkFunc = func() *cobra.Command { return fmt.Errorf("error backing up existing file: %w", err) } } - if err := os.MkdirAll("/usr/local/bin", 0755); err != nil { + if err := fsutil.MkdirAll("/usr/local/bin", 0755); err != nil { return nil } return os.WriteFile(nerdctlCmdArgs.path, buf.Bytes(), 0755) diff --git a/config/dirs.go b/config/dirs.go index 97a8d26f9..a85c20e7b 100644 --- a/config/dirs.go +++ b/config/dirs.go @@ -6,6 +6,9 @@ import ( "path/filepath" "sync" + "github.com/abiosoft/colima/util/fsutil" + "github.com/abiosoft/colima/util/osutil" + "github.com/abiosoft/colima/util/shautil" "github.com/sirupsen/logrus" ) @@ -28,7 +31,7 @@ func (r *requiredDir) Dir() string { } r.once.Do(func() { - if err := os.MkdirAll(dir, 0755); err != nil { + if err := fsutil.MkdirAll(dir, 0755); err != nil { logrus.Fatal(fmt.Errorf("cannot make required directory: %w", err)) } }) @@ -73,7 +76,9 @@ var ( if err != nil { return "", err } - return filepath.Join(dir, ".colima", "_wrapper"), nil + // generate unique directory for the current binary + uniqueDir := shautil.SHA1(osutil.Executable()) + return filepath.Join(dir, ".colima", "_wrapper", uniqueDir.String()), nil }, } ) diff --git a/daemon/daemon.go b/daemon/daemon.go index d429a93c6..95c2ace2a 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -3,7 +3,6 @@ package daemon import ( "context" "fmt" - "os" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" @@ -11,6 +10,8 @@ import ( "github.com/abiosoft/colima/daemon/process/gvproxy" "github.com/abiosoft/colima/daemon/process/vmnet" "github.com/abiosoft/colima/environment" + "github.com/abiosoft/colima/util/fsutil" + "github.com/abiosoft/colima/util/osutil" ) // Manager handles running background processes. @@ -55,14 +56,14 @@ func (l processManager) Dependencies(ctx context.Context) (deps process.Dependen func (l processManager) init() error { // dependencies for network - if err := os.MkdirAll(process.Dir(), 0755); err != nil { + if err := fsutil.MkdirAll(process.Dir(), 0755); err != nil { return fmt.Errorf("error preparing vmnet: %w", err) } return nil } func (l processManager) Running(ctx context.Context) (s Status, err error) { - err = l.host.RunQuiet(os.Args[0], "daemon", "status", config.CurrentProfile().ShortName) + err = l.host.RunQuiet(osutil.Executable(), "daemon", "status", config.CurrentProfile().ShortName) if err != nil { return } @@ -86,7 +87,7 @@ func (l processManager) Start(ctx context.Context) error { return fmt.Errorf("error preparing network directory: %w", err) } - args := []string{os.Args[0], "daemon", "start", config.CurrentProfile().ShortName} + args := []string{osutil.Executable(), "daemon", "start", config.CurrentProfile().ShortName} opts := optsFromCtx(ctx) if opts.Vmnet { args = append(args, "--vmnet") @@ -108,7 +109,7 @@ func (l processManager) Stop(ctx context.Context) error { if s, err := l.Running(ctx); err != nil || !s.Running { return nil } - return l.host.RunQuiet(os.Args[0], "daemon", "stop", config.CurrentProfile().ShortName) + return l.host.RunQuiet(osutil.Executable(), "daemon", "stop", config.CurrentProfile().ShortName) } func optsFromCtx(ctx context.Context) struct { diff --git a/daemon/process/gvproxy/deps.go b/daemon/process/gvproxy/deps.go index d3fbce9f0..762fba0f5 100644 --- a/daemon/process/gvproxy/deps.go +++ b/daemon/process/gvproxy/deps.go @@ -10,6 +10,7 @@ import ( "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/environment" + "github.com/abiosoft/colima/util/fsutil" ) var _ process.Dependency = qemuBinsSymlinks{} @@ -34,7 +35,7 @@ func (q qemuBinsSymlinks) Installed() bool { func (q qemuBinsSymlinks) Install(host environment.HostActions) error { dir := q.dir() - if err := os.MkdirAll(dir, 0755); err != nil { + if err := fsutil.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("error preparing qemu wrapper bin directory: %w", err) } this, err := os.Executable() @@ -69,7 +70,7 @@ func (q qemuShareDirSymlink) Installed() bool { func (q qemuShareDirSymlink) Install(host environment.HostActions) error { dir := q.dir() parent := filepath.Dir(dir) - if err := os.MkdirAll(parent, 0755); err != nil { + if err := fsutil.MkdirAll(parent, 0755); err != nil { return fmt.Errorf("error preparing qemu wrapper shared directory: %w", err) } diff --git a/daemon/process/gvproxy/gvproxy.go b/daemon/process/gvproxy/gvproxy.go index 0f3586694..ee7abd6ec 100644 --- a/daemon/process/gvproxy/gvproxy.go +++ b/daemon/process/gvproxy/gvproxy.go @@ -13,7 +13,7 @@ import ( "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/daemon/process" - "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/shautil" "github.com/containers/gvisor-tap-vsock/pkg/transport" "github.com/containers/gvisor-tap-vsock/pkg/types" "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" @@ -88,9 +88,9 @@ func MacAddress() string { // there is not much concern about the precision of the uniqueness. // this can be revisited if macAddress == nil { - sum := util.SHA256Hash(process.Dir()) + sum := shautil.SHA256(process.Dir()) macAddress = append(macAddress, baseHWAddr...) - macAddress = append(macAddress, sum[0:3]...) + macAddress = append(macAddress, sum.Bytes()[0:3]...) } return macAddress.String() } diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..f9dc84cab --- /dev/null +++ b/default.nix @@ -0,0 +1,34 @@ +let + pkgs = import { }; +in +let + # override Lima to remove wrapper for qemu + # https://github.com/NixOS/nixpkgs/blob/f2537a505d45c31fe5d9c27ea9829b6f4c4e6ac5/pkgs/applications/virtualization/lima/default.nix#L35 + lima = pkgs.lima.overrideAttrs (old: { + installPhase = '' + runHook preInstall + mkdir -p $out + cp -r _output/* $out + runHook postInstall + ''; + }); +in +pkgs.buildGo118Module rec { + name = "colima"; + pname = "colima"; + src = ./.; + nativeBuildInputs = with pkgs; [ installShellFiles makeWrapper git coreutils ]; + vendorSha256 = "sha256-jDzDwK7qA9lKP8CfkKzfooPDrHuHI4OpiLXmX9vOpOg="; + preConfigure = '' + ldflags="-X github.com/abiosoft/colima/config.appVersion=$(git describe --tags --always) + -X github.com/abiosoft/colima/config.revision=$(git rev-parse HEAD)" + ''; + postInstall = '' + wrapProgram $out/bin/colima \ + --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.qemu lima ]} + installShellCompletion --cmd colima \ + --bash <($out/bin/colima completion bash) \ + --fish <($out/bin/colima completion fish) \ + --zsh <($out/bin/colima completion zsh) + ''; +} diff --git a/environment/vm/lima/lima.go b/environment/vm/lima/lima.go index e29233cb9..8f05b0bdb 100644 --- a/environment/vm/lima/lima.go +++ b/environment/vm/lima/lima.go @@ -20,17 +20,21 @@ import ( "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/osutil" "github.com/abiosoft/colima/util/yamlutil" "github.com/sirupsen/logrus" ) // New creates a new virtual machine. func New(host environment.HostActions) environment.VM { + // environment variables for the subprocesses var envs []string envLimaInstance := limaInstanceEnvVar + "=" + config.CurrentProfile().ID envSubprocess := config.SubprocessProfileEnvVar + "=" + config.CurrentProfile().ShortName - envs = append(envs, envLimaInstance, envSubprocess) + envBinary := osutil.EnvColimaBinary + "=" + osutil.Executable() + envs = append(envs, envLimaInstance, envSubprocess, envBinary) + // modify the PATH for qemu wrapper binDir := filepath.Join(config.WrapperDir(), "bin") envs = append(envs, "PATH="+util.AppendToPath(os.Getenv("PATH"), binDir)) diff --git a/environment/vm/lima/yaml_test.go b/environment/vm/lima/yaml_test.go index 16d63dc65..e194f6b37 100644 --- a/environment/vm/lima/yaml_test.go +++ b/environment/vm/lima/yaml_test.go @@ -8,6 +8,7 @@ import ( "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/fsutil" ) func Test_checkOverlappingMounts(t *testing.T) { @@ -43,6 +44,7 @@ func Test_checkOverlappingMounts(t *testing.T) { } func Test_config_Mounts(t *testing.T) { + fsutil.FS = fsutil.FakeFS tests := []struct { mounts []string isDefault bool diff --git a/shell.nix b/shell.nix index dbd40bc2d..8e981e99c 100644 --- a/shell.nix +++ b/shell.nix @@ -6,8 +6,9 @@ pkgs.mkShell { go_1_18 git coreutils + lima ]; shellHook = '' - export CGO_ENABLED=0 + echo Nix Shell with $(go version) ''; } diff --git a/util/downloader/download.go b/util/downloader/download.go index bcc5dfdbe..8884034a9 100644 --- a/util/downloader/download.go +++ b/util/downloader/download.go @@ -7,7 +7,7 @@ import ( "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" - "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/shautil" "github.com/abiosoft/colima/util/terminal" ) @@ -37,7 +37,7 @@ type downloader struct { } func (d downloader) cacheFileName(url string) string { - return filepath.Join(config.CacheDir(), "caches", util.SHA256Hash(url).String()) + return filepath.Join(config.CacheDir(), "caches", shautil.SHA256(url).String()) } func (d downloader) cacheDownloadingFileName(url string) string { diff --git a/util/fsutil/fs.go b/util/fsutil/fs.go new file mode 100644 index 000000000..5c8a2d9fc --- /dev/null +++ b/util/fsutil/fs.go @@ -0,0 +1,50 @@ +package fsutil + +import ( + "io/fs" + "os" + "testing/fstest" +) + +// FS is the host filesystem implementation. +var FS FileSystem = DefaultFS{} + +// MkdirAll calls FS.MakedirAll +func MkdirAll(path string, perm os.FileMode) error { return FS.MkdirAll(path, perm) } + +// Open calls FS.Open +func Open(name string) (fs.File, error) { return FS.Open(name) } + +// FS is abstraction for filesystem. +type FileSystem interface { + MkdirAll(path string, perm os.FileMode) error + fs.FS +} + +var _ FileSystem = DefaultFS{} +var _ FileSystem = fakeFS{} + +// DefaultFS is the default OS implementation of FileSystem. +type DefaultFS struct{} + +// Open implements FS +func (DefaultFS) Open(name string) (fs.File, error) { return os.Open(name) } + +// MkdirAll implements FS +func (DefaultFS) MkdirAll(path string, perm fs.FileMode) error { return os.MkdirAll(path, perm) } + +// FakeFS is a mock FS. The following can be done in a test before usage. +// osutil.FS = osutil.FakeFS +var FakeFS FileSystem = fakeFS{} + +type fakeFS struct{} + +// Open implements FileSystem +func (fakeFS) Open(name string) (fs.File, error) { + return fstest.MapFS{name: &fstest.MapFile{ + Data: []byte("fake file - " + name), + }}.Open(name) +} + +// MkdirAll implements FileSystem +func (fakeFS) MkdirAll(path string, perm fs.FileMode) error { return nil } diff --git a/util/osutil/os.go b/util/osutil/os.go new file mode 100644 index 000000000..31efb4336 --- /dev/null +++ b/util/osutil/os.go @@ -0,0 +1,48 @@ +package osutil + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +const EnvColimaBinary = "COLIMA_BINARY" + +// Executable returns the path name for the executable that started +// the current process. +func Executable() string { + e, err := func(s string) (string, error) { + // prioritize env var in case this is a nested process + if e := os.Getenv(EnvColimaBinary); e != "" { + return e, nil + } + + if filepath.IsAbs(s) { + return s, nil + } + + e, err := exec.LookPath(s) + if err != nil { + return "", fmt.Errorf("error looking up '%s' in PATH: %w", s, err) + } + + abs, err := filepath.Abs(e) + if err != nil { + return "", fmt.Errorf("error computing absolute path of '%s': %w", e, err) + } + + return abs, nil + }(os.Args[0]) + + if err != nil { + // this should never happen, thereby it is safe to do + logrus.Warnln(fmt.Errorf("cannot detect current running executable: %w", err)) + logrus.Warnln("falling back to first CLI argument") + return os.Args[0] + } + + return e +} diff --git a/util/shautil/sha.go b/util/shautil/sha.go new file mode 100644 index 000000000..a69995f76 --- /dev/null +++ b/util/shautil/sha.go @@ -0,0 +1,33 @@ +package shautil + +import ( + "crypto/sha1" + "crypto/sha256" + "fmt" +) + +// SHA is a sha computation +type SHA interface { + String() string + Bytes() []byte +} + +type s1 [20]byte + +func (s s1) String() string { return fmt.Sprintf("%x", s[:]) } +func (s s1) Bytes() []byte { return s[:] } + +type s256 [32]byte + +func (s s256) String() string { return fmt.Sprintf("%x", s[:]) } +func (s s256) Bytes() []byte { return s[:] } + +// SHA256Hash computes a sha256sum of a string. +func SHA256(s string) SHA { + return s256(sha256.Sum256([]byte(s))) +} + +// SHA256Hash computes a sha256sum of a string. +func SHA1(s string) SHA { + return s1(sha1.Sum([]byte(s))) +} diff --git a/util/util.go b/util/util.go index 9948bba86..efe6aa059 100644 --- a/util/util.go +++ b/util/util.go @@ -1,9 +1,7 @@ package util import ( - "crypto/sha256" "fmt" - "log" "net" "os" "runtime" @@ -18,20 +16,11 @@ func HomeDir() string { home, err := os.UserHomeDir() if err != nil { // this should never happen - log.Fatal(fmt.Errorf("error retrieving home directory: %w", err)) + logrus.Fatal(fmt.Errorf("error retrieving home directory: %w", err)) } return home } -type SHA256 [32]byte - -func (s SHA256) String() string { return fmt.Sprintf("%x", s[:]) } - -// SHA256Hash computes a sha256sum of a string. -func SHA256Hash(s string) SHA256 { - return sha256.Sum256([]byte(s)) -} - // MacOS returns if the current OS is macOS. func MacOS() bool { return runtime.GOOS == "darwin" @@ -64,11 +53,11 @@ func RemoveFromPath(path, dir string) string { func RandomAvailablePort() int { listener, err := net.Listen("tcp", ":0") if err != nil { - log.Fatal(fmt.Errorf("error picking an available port: %w", err)) + logrus.Fatal(fmt.Errorf("error picking an available port: %w", err)) } if err := listener.Close(); err != nil { - log.Fatal(fmt.Errorf("error closing temporary port listener: %w", err)) + logrus.Fatal(fmt.Errorf("error closing temporary port listener: %w", err)) } return listener.Addr().(*net.TCPAddr).Port