mirror of
https://github.com/cirruslabs/tart-guest-agent.git
synced 2026-10-21 11:15:30 +02:00
Automatic disk resizing for macOS VMs (#1)
* Automatic disk resizing for macOS VMs * CI: install Golang via Homebrew * diskresizer_test.go: fix fixture values for "diskSizeUnused"
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
load("github.com/cirrus-modules/golang@main", "lint_task")
|
||||
|
||||
def main(ctx):
|
||||
return [lint_task()]
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
task:
|
||||
name: Test (Linux)
|
||||
alias: Tests
|
||||
container:
|
||||
image: golang:latest
|
||||
|
||||
test_script: go test -v ./...
|
||||
|
||||
task:
|
||||
name: Test (macOS)
|
||||
alias: Tests
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
|
||||
prepare_script: brew install go
|
||||
test_script: go test -v ./...
|
||||
|
||||
task:
|
||||
name: Release (Dry Run)
|
||||
only_if: $CIRRUS_TAG == ''
|
||||
container:
|
||||
image: golang:latest
|
||||
memory: 8GB
|
||||
depends_on:
|
||||
- Lint
|
||||
- Tests
|
||||
install_script:
|
||||
- echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list
|
||||
- apt-get update
|
||||
- apt-get -y install goreleaser-pro
|
||||
release_script: goreleaser build --snapshot
|
||||
goreleaser_artifacts:
|
||||
path: "dist/**"
|
||||
|
||||
task:
|
||||
name: Release
|
||||
only_if: $CIRRUS_TAG != ''
|
||||
container:
|
||||
image: golang:latest
|
||||
memory: 8GB
|
||||
depends_on:
|
||||
- Lint
|
||||
- Tests
|
||||
env:
|
||||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||
FURY_TOKEN: ENCRYPTED[!97fe4497d9aca60a3d64904883b81e21f19706c6aedda625c97f62f67ec46b8efa74c55699956158bbf0a23726e7d9f6!]
|
||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||
install_script:
|
||||
- echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list
|
||||
- apt-get update
|
||||
- apt-get -y install goreleaser-pro
|
||||
release_script: goreleaser
|
||||
@@ -0,0 +1,67 @@
|
||||
version: 2
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
# Even in Rust you can get away with partial matching,
|
||||
# so make sure that the linter respects the programmer's
|
||||
# intent expressed in the form of "default" case.
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
gosec:
|
||||
excludes:
|
||||
- G115
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
|
||||
disable:
|
||||
# We don't have high-performance requirements at this moment, so sacrificing
|
||||
# the code readability for marginal performance gains is not worth it.
|
||||
- prealloc
|
||||
|
||||
# New linters that require a lot of codebase churn and noise, but perhaps we can enable them in the future.
|
||||
- nlreturn
|
||||
- wrapcheck
|
||||
- errorlint
|
||||
|
||||
# Unfortunately, we use globals due to how spf13/cobra works.
|
||||
- gochecknoglobals
|
||||
|
||||
# Style linters that are total nuts.
|
||||
- wsl
|
||||
- funlen
|
||||
|
||||
# Enough parallelism for now.
|
||||
- paralleltest
|
||||
|
||||
# Ill-based assumptions about identifiers like fmt.Println without taking context into account.
|
||||
- forbidigo
|
||||
|
||||
# Advantages of using t.Helper() are too small to waste developer's cognitive stamina on it.
|
||||
- thelper
|
||||
|
||||
# Too restrictive defaults, plus there's already a gocyclo linter in place.
|
||||
- cyclop
|
||||
|
||||
# Gives false positives for textbook examples[1][2]
|
||||
# [1]: https://github.com/charithe/durationcheck/issues/7
|
||||
# [2]: https://golang.org/pkg/time/ (see "To convert an integer number of units to a Duration, multiply:")
|
||||
- durationcheck
|
||||
|
||||
# No way to disable the "exported" check for the whole project[1]
|
||||
# [1]: https://github.com/mgechev/revive/issues/244#issuecomment-560512162
|
||||
- revive
|
||||
|
||||
# Needs package whitelists
|
||||
- depguard
|
||||
|
||||
# Not all errors need to be checked
|
||||
- errcheck
|
||||
|
||||
issues:
|
||||
# Don't hide multiple issues that belong to one class since GitHub annotations can handle them all nicely.
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
project_name: tart-guest-agent
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- main: cmd/main.go
|
||||
ldflags: >
|
||||
-X github.com/cirruslabs/tart-guest-agent/internal/version.Version={{.Version}}
|
||||
-X github.com/cirruslabs/tart-guest-agent/internal/version.Commit={{.ShortCommit}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- id: binary
|
||||
format: binary
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
- id: regular
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
|
||||
nfpms:
|
||||
- package_name: tart-guest-agent
|
||||
vendor: Cirrus Labs, Inc.
|
||||
homepage: https://github.com/cirruslabs/tart-guest-agent
|
||||
maintainer: support@cirruslabs.org
|
||||
description: Guest agent for Tart VMs
|
||||
section: misc
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
|
||||
furies:
|
||||
- account: cirruslabs
|
||||
@@ -1,3 +1,7 @@
|
||||
# Guest agent for Tart VMs
|
||||
|
||||
A guest agent for Tart VMS is a lightweight background service that runs inside the virtual machine and enables enhanced communication between the host and guest and other useful features, such as automatic disk resizing.
|
||||
|
||||
Currently implemented features:
|
||||
|
||||
* Automatic disk resizing for macOS VMs with recovery partition removed
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/command"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/logginglevel"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if !mainImpl() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func mainImpl() bool {
|
||||
// Set up a signal-interruptible context
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
// Initialize logger
|
||||
loggerConfig := zap.NewProductionConfig()
|
||||
loggerConfig.Level = logginglevel.Level
|
||||
logger, err := loggerConfig.Build()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
|
||||
return false
|
||||
}
|
||||
defer func() {
|
||||
_ = logger.Sync()
|
||||
}()
|
||||
|
||||
// Replace zap.L() and zap.S() to avoid
|
||||
// propagating the *zap.Logger by hand
|
||||
zap.ReplaceGlobals(logger)
|
||||
|
||||
if err := command.NewRootCommand().ExecuteContext(ctx); err != nil {
|
||||
logger.Sugar().Error(err)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module github.com/cirruslabs/tart-guest-agent
|
||||
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sys v0.32.0
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
@@ -0,0 +1,64 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/diskresizer"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/logginglevel"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/tart"
|
||||
"github.com/cirruslabs/tart-guest-agent/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var debug bool
|
||||
|
||||
func NewRootCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "tart-guest-agent",
|
||||
Short: "Guest agent for Tart VMs",
|
||||
Version: version.FullVersion,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
if debug {
|
||||
logginglevel.Level.SetLevel(zapcore.DebugLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: run,
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string) error {
|
||||
// Terminate to prevent corruption on systems with disk layouts other than Tart's
|
||||
communicationPoint, ok := tart.LocateCommunicationPoint()
|
||||
if !ok {
|
||||
return unix.Kill(os.Getppid(), syscall.SIGTERM)
|
||||
}
|
||||
|
||||
zap.S().Infof("successfully located host communication point %s, proceeding...",
|
||||
communicationPoint)
|
||||
|
||||
// Perform disk resizing
|
||||
if err := diskresizer.Resize(); err != nil {
|
||||
if errors.Is(err, diskresizer.ErrUnsupported) || errors.Is(err, diskresizer.ErrAlreadyResized) {
|
||||
zap.S().Infof("skipping disk resizing: %v", err)
|
||||
} else {
|
||||
zap.S().Warnf("failed to resize disk: %v", err)
|
||||
}
|
||||
} else {
|
||||
zap.S().Infof("sucessfully resized the disk")
|
||||
}
|
||||
|
||||
// Wait indefinitely
|
||||
<-cmd.Context().Done()
|
||||
|
||||
return cmd.Context().Err()
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package diskresizer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/samber/lo"
|
||||
"howett.net/plist"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DiskUtilOutput struct {
|
||||
AllDisksAndPartitions []DiskWithPartitions `plist:"AllDisksAndPartitions"`
|
||||
}
|
||||
|
||||
type DiskWithPartitions struct {
|
||||
DeviceIdentifier string `plist:"DeviceIdentifier"`
|
||||
Size int64 `plist:"Size"`
|
||||
Partitions []Partition `plist:"Partitions"`
|
||||
}
|
||||
|
||||
type Partition struct {
|
||||
DeviceIdentifier string `plist:"DeviceIdentifier"`
|
||||
Size int64 `plist:"Size"`
|
||||
Content string `plist:"Content"`
|
||||
}
|
||||
|
||||
const expectedPartitionContent = "Apple_APFS"
|
||||
|
||||
var (
|
||||
ErrUnsupported = errors.New("disk resizing is only supported on macOS")
|
||||
ErrAlreadyResized = errors.New("disk already seems to be resized")
|
||||
)
|
||||
|
||||
func Resize() error {
|
||||
// Only macOS is currently supported because Linux distributions
|
||||
// already have this functionality
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ErrUnsupported
|
||||
}
|
||||
|
||||
// Obtain a list of physical disks with their partitions
|
||||
cmd := exec.Command("diskutil", "list", "-plist", "physical")
|
||||
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
cmd.Stderr = stderrBuf
|
||||
|
||||
diskutilOutput, err := cmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list physical disks: %w: %s", err,
|
||||
firstNonEmptyLine(stderrBuf.String()))
|
||||
}
|
||||
|
||||
// Figure out which disk and partition to resize,
|
||||
// aborts on ambiguity
|
||||
diskName, partitionName, diskSizeUnused, err := diskAndPartitionNameToResize(diskutilOutput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to locate a physical disk to resize: %w", err)
|
||||
}
|
||||
|
||||
if diskSizeUnused <= 4096*10 {
|
||||
return ErrAlreadyResized
|
||||
}
|
||||
|
||||
// Repair the disk whose partition we're going to resize, just in case
|
||||
cmd = exec.Command("diskutil", "repairDisk", diskName)
|
||||
|
||||
cmd.Stdin = bytes.NewReader([]byte("yes"))
|
||||
|
||||
stderrBuf = &bytes.Buffer{}
|
||||
cmd.Stderr = stderrBuf
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to repair disk %s: %w: %s", diskName, err,
|
||||
firstNonEmptyLine(stderrBuf.String()))
|
||||
}
|
||||
|
||||
// Finally, resize the partition
|
||||
cmd = exec.Command("diskutil", "apfs", "resizeContainer", partitionName, "0")
|
||||
|
||||
stderrBuf = &bytes.Buffer{}
|
||||
cmd.Stderr = stderrBuf
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to resize partition %s: %w: %s", partitionName, err,
|
||||
firstNonEmptyLine(stderrBuf.String()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// diskAndPartitionNameToResize parses "diskutil list -plist" output,
|
||||
// makes sure there's only one disk on the system and returns its
|
||||
// name and the name of a single APFS partition or errors otherwise.
|
||||
func diskAndPartitionNameToResize(input []byte) (string, string, int64, error) {
|
||||
var diskUtilOutput DiskUtilOutput
|
||||
|
||||
_, err := plist.Unmarshal(input, &diskUtilOutput)
|
||||
if err != nil {
|
||||
return "", "", 0, fmt.Errorf("failed to parse \"diskutil list -plist\" output: %w", err)
|
||||
}
|
||||
|
||||
type Candidate struct {
|
||||
DiskName string
|
||||
DiskSizeUnused int64
|
||||
PartitionName string
|
||||
}
|
||||
|
||||
var candidates []Candidate
|
||||
|
||||
for _, diskWithPartitions := range diskUtilOutput.AllDisksAndPartitions {
|
||||
unusedSize := diskWithPartitions.Size - lo.SumBy(diskWithPartitions.Partitions, func(partition Partition) int64 {
|
||||
return partition.Size
|
||||
})
|
||||
|
||||
for _, partition := range diskWithPartitions.Partitions {
|
||||
if partition.Content != expectedPartitionContent {
|
||||
continue
|
||||
}
|
||||
|
||||
candidates = append(candidates, Candidate{
|
||||
DiskName: diskWithPartitions.DeviceIdentifier,
|
||||
DiskSizeUnused: unusedSize,
|
||||
PartitionName: partition.DeviceIdentifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return "", "", 0, fmt.Errorf("found no disks on which the partition's \"Content\" "+
|
||||
"is %q, make sure that the macOS is installed", expectedPartitionContent)
|
||||
}
|
||||
|
||||
if len(candidates) > 1 {
|
||||
return "", "", 0, fmt.Errorf("found more than one disk on which the partition's \"Content\" "+
|
||||
"is %q, please only mount a single disk that contains APFS partitions otherwise it's hard "+
|
||||
"to tell on which disk the macOS is installed", expectedPartitionContent)
|
||||
}
|
||||
|
||||
return candidates[0].DiskName, candidates[0].PartitionName,
|
||||
candidates[0].DiskSizeUnused, nil
|
||||
}
|
||||
|
||||
func firstNonEmptyLine(outputs ...string) string {
|
||||
for _, output := range outputs {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if line != "" {
|
||||
return line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package diskresizer
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSingleDisk(t *testing.T) {
|
||||
plistBytes := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AllDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
<string>disk0s1</string>
|
||||
<string>disk0s2</string>
|
||||
<string>disk0s3</string>
|
||||
</array>
|
||||
<key>AllDisksAndPartitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>GUID_partition_scheme</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0</string>
|
||||
<key>OSInternal</key>
|
||||
<false/>
|
||||
<key>Partitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS_ISC</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s1</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>024D2AE5-891F-4244-81EC-182B88D1AA0B</string>
|
||||
<key>Size</key>
|
||||
<integer>524288000</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s2</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>430F1409-D91A-47F4-8418-0876B14AA807</string>
|
||||
<key>Size</key>
|
||||
<integer>494384795648</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>Size</key>
|
||||
<integer>500277792768</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>VolumesFromDisks</key>
|
||||
<array/>
|
||||
<key>WholeDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
diskName, partitionName, diskSizeUnused, err := diskAndPartitionNameToResize([]byte(plistBytes))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "disk0", diskName)
|
||||
require.Equal(t, "disk0s2", partitionName)
|
||||
require.EqualValues(t, 5368709120, diskSizeUnused)
|
||||
}
|
||||
|
||||
func TestSingleDiskWithRecoveryPartition(t *testing.T) {
|
||||
plistBytes := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AllDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
<string>disk0s1</string>
|
||||
<string>disk0s2</string>
|
||||
<string>disk0s3</string>
|
||||
</array>
|
||||
<key>AllDisksAndPartitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>GUID_partition_scheme</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0</string>
|
||||
<key>OSInternal</key>
|
||||
<false/>
|
||||
<key>Partitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS_ISC</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s1</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>22A4F5CC-CB5B-4EF1-B4F5-EBE9F79C0EE6</string>
|
||||
<key>Size</key>
|
||||
<integer>524288000</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s2</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>E59D951F-E0B0-42B5-BE26-DF3611AD673C</string>
|
||||
<key>Size</key>
|
||||
<integer>44107001856</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS_Recovery</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s3</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>7684137C-F165-45C4-948B-681012742DAC</string>
|
||||
<key>Size</key>
|
||||
<integer>5368672256</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>Size</key>
|
||||
<integer>50000000000</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>VolumesFromDisks</key>
|
||||
<array/>
|
||||
<key>WholeDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
diskName, partitionName, diskSizeUnused, err := diskAndPartitionNameToResize([]byte(plistBytes))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "disk0", diskName)
|
||||
require.Equal(t, "disk0s2", partitionName)
|
||||
require.EqualValues(t, 37888, diskSizeUnused)
|
||||
}
|
||||
|
||||
func TestMultipleDisks(t *testing.T) {
|
||||
plistBytes := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AllDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
<string>disk0s1</string>
|
||||
<string>disk0s2</string>
|
||||
<string>disk2</string>
|
||||
</array>
|
||||
<key>AllDisksAndPartitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>GUID_partition_scheme</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0</string>
|
||||
<key>OSInternal</key>
|
||||
<false/>
|
||||
<key>Partitions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS_ISC</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s1</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>D2B79297-879E-4461-8DA2-EEA50EA7319A</string>
|
||||
<key>Size</key>
|
||||
<integer>524288000</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string>Apple_APFS</string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk0s2</string>
|
||||
<key>DiskUUID</key>
|
||||
<string>D5BA624D-182F-40D0-8248-D08508A8D1B3</string>
|
||||
<key>Size</key>
|
||||
<integer>89475674112</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>Size</key>
|
||||
<integer>90000000000</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Content</key>
|
||||
<string></string>
|
||||
<key>DeviceIdentifier</key>
|
||||
<string>disk2</string>
|
||||
<key>OSInternal</key>
|
||||
<false/>
|
||||
<key>Size</key>
|
||||
<integer>107164426240</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>VolumesFromDisks</key>
|
||||
<array/>
|
||||
<key>WholeDisks</key>
|
||||
<array>
|
||||
<string>disk0</string>
|
||||
<string>disk2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
diskName, partitionName, diskSizeUnused, err := diskAndPartitionNameToResize([]byte(plistBytes))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "disk0", diskName)
|
||||
require.Equal(t, "disk0s2", partitionName)
|
||||
require.EqualValues(t, 37888, diskSizeUnused)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package logginglevel
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var Level = zap.NewAtomicLevel()
|
||||
@@ -0,0 +1,24 @@
|
||||
package tart
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const devDirectoryPath = "/dev"
|
||||
|
||||
func LocateCommunicationPoint() (string, bool) {
|
||||
dirEntries, err := os.ReadDir(devDirectoryPath)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for _, entry := range dirEntries {
|
||||
if strings.HasPrefix(entry.Name(), "cu.tart-version-") {
|
||||
return filepath.Join(devDirectoryPath, entry.Name()), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goversion "github.com/hashicorp/go-version"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "unknown"
|
||||
Commit = "unknown"
|
||||
FullVersion = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
if Version == "unknown" {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
// We parse the version here for two reasons:
|
||||
// * to weed out the "(devel)" version and fallback to "unknown" instead
|
||||
// (see https://github.com/golang/go/issues/29228 for details on when this might happen)
|
||||
// * to remove the "v" prefix from the BuildInfo's version (e.g. "v0.7.0") and thus be consistent
|
||||
// with the binary builds, where the version string would be "0.7.0" instead
|
||||
semver, err := goversion.NewSemver(info.Main.Version)
|
||||
if err == nil {
|
||||
Version = semver.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FullVersion = fmt.Sprintf("%s-%s", Version, Commit)
|
||||
}
|
||||
Reference in New Issue
Block a user