diff --git a/.cirrus.star b/.cirrus.star new file mode 100644 index 0000000..0a13153 --- /dev/null +++ b/.cirrus.star @@ -0,0 +1,4 @@ +load("github.com/cirrus-modules/golang@main", "lint_task") + +def main(ctx): + return [lint_task()] diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000..31b9195 --- /dev/null +++ b/.cirrus.yml @@ -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 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d69190a --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..cd083e0 --- /dev/null +++ b/.goreleaser.yml @@ -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 diff --git a/README.md b/README.md index 1781fbd..f278e6b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..3314bf2 --- /dev/null +++ b/cmd/main.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eb8c040 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f2ffc7f --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/command/root.go b/internal/command/root.go new file mode 100644 index 0000000..c534bb2 --- /dev/null +++ b/internal/command/root.go @@ -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() +} diff --git a/internal/diskresizer/diskresizer.go b/internal/diskresizer/diskresizer.go new file mode 100644 index 0000000..a70d946 --- /dev/null +++ b/internal/diskresizer/diskresizer.go @@ -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 "" +} diff --git a/internal/diskresizer/diskresizer_test.go b/internal/diskresizer/diskresizer_test.go new file mode 100644 index 0000000..0cdbcb1 --- /dev/null +++ b/internal/diskresizer/diskresizer_test.go @@ -0,0 +1,222 @@ +package diskresizer + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestSingleDisk(t *testing.T) { + plistBytes := ` + + + + AllDisks + + disk0 + disk0s1 + disk0s2 + disk0s3 + + AllDisksAndPartitions + + + Content + GUID_partition_scheme + DeviceIdentifier + disk0 + OSInternal + + Partitions + + + Content + Apple_APFS_ISC + DeviceIdentifier + disk0s1 + DiskUUID + 024D2AE5-891F-4244-81EC-182B88D1AA0B + Size + 524288000 + + + Content + Apple_APFS + DeviceIdentifier + disk0s2 + DiskUUID + 430F1409-D91A-47F4-8418-0876B14AA807 + Size + 494384795648 + + + Size + 500277792768 + + + VolumesFromDisks + + WholeDisks + + disk0 + + + +` + + 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 := ` + + + + AllDisks + + disk0 + disk0s1 + disk0s2 + disk0s3 + + AllDisksAndPartitions + + + Content + GUID_partition_scheme + DeviceIdentifier + disk0 + OSInternal + + Partitions + + + Content + Apple_APFS_ISC + DeviceIdentifier + disk0s1 + DiskUUID + 22A4F5CC-CB5B-4EF1-B4F5-EBE9F79C0EE6 + Size + 524288000 + + + Content + Apple_APFS + DeviceIdentifier + disk0s2 + DiskUUID + E59D951F-E0B0-42B5-BE26-DF3611AD673C + Size + 44107001856 + + + Content + Apple_APFS_Recovery + DeviceIdentifier + disk0s3 + DiskUUID + 7684137C-F165-45C4-948B-681012742DAC + Size + 5368672256 + + + Size + 50000000000 + + + VolumesFromDisks + + WholeDisks + + disk0 + + + +` + + 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 := ` + + + + AllDisks + + disk0 + disk0s1 + disk0s2 + disk2 + + AllDisksAndPartitions + + + Content + GUID_partition_scheme + DeviceIdentifier + disk0 + OSInternal + + Partitions + + + Content + Apple_APFS_ISC + DeviceIdentifier + disk0s1 + DiskUUID + D2B79297-879E-4461-8DA2-EEA50EA7319A + Size + 524288000 + + + Content + Apple_APFS + DeviceIdentifier + disk0s2 + DiskUUID + D5BA624D-182F-40D0-8248-D08508A8D1B3 + Size + 89475674112 + + + Size + 90000000000 + + + Content + + DeviceIdentifier + disk2 + OSInternal + + Size + 107164426240 + + + VolumesFromDisks + + WholeDisks + + disk0 + disk2 + + + +` + + 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) +} diff --git a/internal/logginglevel/logginglevel.go b/internal/logginglevel/logginglevel.go new file mode 100644 index 0000000..709f93e --- /dev/null +++ b/internal/logginglevel/logginglevel.go @@ -0,0 +1,6 @@ +package logginglevel + +import "go.uber.org/zap" + +//nolint:gochecknoglobals +var Level = zap.NewAtomicLevel() diff --git a/internal/tart/tart.go b/internal/tart/tart.go new file mode 100644 index 0000000..bf231c5 --- /dev/null +++ b/internal/tart/tart.go @@ -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 +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..e576cab --- /dev/null +++ b/internal/version/version.go @@ -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) +}