Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions cmd/composectl/cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import (

type (
checkOptions struct {
UsageWatermark *uint
SrcStorePath *string
Locally *bool
Format string
CheckInstall bool
Quick bool
UsageWatermark *uint
ReservedStorage *string
SrcStorePath *string
Locally *bool
Format string
CheckInstall bool
Quick bool
}

CheckAppResult struct {
Expand Down Expand Up @@ -61,6 +62,8 @@ func init() {
opts := checkOptions{}
opts.UsageWatermark = checkCmd.Flags().UintP("storage-usage-watermark", "u", DefaultUsageWatermark,
fmt.Sprintf("The maximum allowed storage usage in percentage in range %d-%d", MinUsageWatermark, MaxUsageWatermark))
opts.ReservedStorage = checkCmd.Flags().String("reserved-storage", "",
"Absolute amount of free space to keep reserved, e.g. \"2GiB\" or \"500MB\"; takes precedence over --storage-usage-watermark")
opts.SrcStorePath = checkCmd.Flags().StringP("source-store-path", "l", "",
"A path to the source store root directory")
opts.Locally = checkCmd.Flags().BoolP("local", "", false,
Expand All @@ -77,8 +80,8 @@ func init() {
fmt.Fprintf(os.Stderr, "unsupported `--format` value: %s\n", opts.Format)
os.Exit(1)
}
checkWatermark(*opts.UsageWatermark)
checkAppsCmd(cmd, args, &opts)
watermark, watermarkInBytes := resolveWatermark(cmd, *opts.UsageWatermark, *opts.ReservedStorage)
checkAppsCmd(cmd, args, &opts, watermark, watermarkInBytes)
}

rootCmd.AddCommand(checkCmd)
Expand All @@ -91,7 +94,7 @@ func checkWatermark(watermark uint) {
}
}

func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions, watermark uint64, watermarkInBytes bool) {
var quietCheck bool
if opts.Format == "json" {
quietCheck = true
Expand All @@ -103,7 +106,7 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
opts.SrcStorePath = &config.StoreRoot
}
cr, ui, _, err := checkApps(cmd.Context(), args, blobProvider,
*opts.UsageWatermark, *opts.SrcStorePath, quietCheck, opts.Quick)
watermark, watermarkInBytes, *opts.SrcStorePath, quietCheck, opts.Quick)
DieNotNil(err, "failed to check apps status")

var ir InstallCheckResult
Expand Down Expand Up @@ -153,7 +156,8 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) {
func checkApps(ctx context.Context,
appRefs []string,
srcBlobProvider compose.BlobProvider,
usageWatermark uint,
watermark uint64,
watermarkInBytes bool,
srcStorePath string,
quiet bool,
quick bool) (*CheckAppResult, *compose.UsageInfo, []compose.App, error) {
Expand Down Expand Up @@ -205,7 +209,7 @@ func checkApps(ctx context.Context,
checkResult.TotalRuntimeSize += bi.RuntimeSize
}
ui, err := compose.GetUsageInfo(config.StoreRoot,
checkResult.TotalStoreSize+checkResult.TotalRuntimeSize, usageWatermark)
checkResult.TotalStoreSize+checkResult.TotalRuntimeSize, watermark, watermarkInBytes)
if err != nil {
return nil, nil, nil, err
}
Expand Down
54 changes: 45 additions & 9 deletions cmd/composectl/cmd/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/containerd/containerd/platforms"
"github.com/docker/go-units"
"github.com/foundriesio/composeapp/pkg/compose"
v1 "github.com/foundriesio/composeapp/pkg/compose/v1"
"github.com/moby/term"
Expand All @@ -15,11 +17,12 @@ import (

type (
pullOptions struct {
UsageWatermark uint
SrcStorePath string
PrintUsageStat bool
Quick bool
Workers uint
UsageWatermark uint
ReservedStorage string
SrcStorePath string
PrintUsageStat bool
Quick bool
Workers uint
}
)

Expand All @@ -44,15 +47,17 @@ func init() {

pullCmd.Flags().UintVarP(&opts.UsageWatermark, "storage-usage-watermark", "u", DefaultUsageWatermark,
fmt.Sprintf("The maximum allowed storage usage in percentage in range %d-%d", MinUsageWatermark, MaxUsageWatermark))
pullCmd.Flags().StringVar(&opts.ReservedStorage, "reserved-storage", "",
"Absolute amount of free space to keep reserved, e.g. \"2GiB\" or \"500MB\"; takes precedence over --storage-usage-watermark")
pullCmd.Flags().StringVarP(&opts.SrcStorePath, "source-store-path", "l", "", "A path to the source store root directory")
pullCmd.Flags().BoolVarP(&opts.PrintUsageStat, "print-usage-stat", "p", false, "A flag to enable/disable usage statistic output to stderr")
pullCmd.Flags().BoolVar(&opts.Quick, "quick", false, "Skip checking hash of app blobs; verify only their presence and size")
pullCmd.Flags().UintVarP(&opts.Workers, "workers", "w", DefaultWorkers,
fmt.Sprintf("Number of concurrent blob download workers in range %d-%d", MinWorkers, MaxWorkers))
pullCmd.Run = func(cmd *cobra.Command, args []string) {
checkWatermark(opts.UsageWatermark)
watermark, watermarkInBytes := resolveWatermark(cmd, opts.UsageWatermark, opts.ReservedStorage)
checkWorkers(opts.Workers)
pullApps(cmd, args, &opts)
pullApps(cmd, args, &opts, watermark, watermarkInBytes)
}

rootCmd.AddCommand(pullCmd)
Expand All @@ -65,7 +70,38 @@ func checkWorkers(workers uint) {
}
}

func pullApps(cmd *cobra.Command, args []string, opts *pullOptions) {
// resolveWatermark turns the storage-usage-watermark percentage and the optional
// reserved-storage size into the (watermark, inBytes) pair GetUsageInfo expects.
// reserved-storage takes precedence: when it is set the percentage watermark is
// ignored, with a warning if it was also explicitly provided.
func resolveWatermark(cmd *cobra.Command, usageWatermark uint, reservedStorage string) (watermark uint64, inBytes bool) {
if len(reservedStorage) == 0 {
checkWatermark(usageWatermark)
return uint64(usageWatermark), false
}
if cmd.Flags().Changed("storage-usage-watermark") {
fmt.Fprintln(os.Stderr,
"warning: both --storage-usage-watermark and --reserved-storage are set; ignoring --storage-usage-watermark")
}
reserved, err := parseReservedStorage(reservedStorage)
if err != nil || reserved <= 0 {
DieNotNilWithCode(fmt.Errorf("invalid `--reserved-storage` value: %q; expected a byte size such as \"2GiB\" or \"500MB\"",
reservedStorage), 1, "invalid argument")
}
return uint64(reserved), true
}

// parseReservedStorage accepts both binary (e.g. "2GiB", "500MiB") and decimal
// (e.g. "2GB", "500MB") byte-size suffixes. The presence of an "ib" suffix
// selects the binary parser; otherwise the decimal parser is used.
func parseReservedStorage(s string) (int64, error) {
if strings.Contains(strings.ToLower(s), "ib") {
return units.RAMInBytes(s)
}
return units.FromHumanSize(s)
}

func pullApps(cmd *cobra.Command, args []string, opts *pullOptions, watermark uint64, watermarkInBytes bool) {
if len(args) > 1 {
fmt.Printf("Pulling %d apps to %s\n", len(args), config.StoreRoot)
} else {
Expand All @@ -75,7 +111,7 @@ func pullApps(cmd *cobra.Command, args []string, opts *pullOptions) {
srcBlobProvider, cs, err := getAppStoreAndDstBlobProvider(opts.SrcStorePath, false)
DieNotNil(err)

cr, ui, apps, err := checkApps(cmd.Context(), args, srcBlobProvider, opts.UsageWatermark,
cr, ui, apps, err := checkApps(cmd.Context(), args, srcBlobProvider, watermark, watermarkInBytes,
opts.SrcStorePath, false, opts.Quick)
DieNotNil(err, "failed to check apps status")
if len(cr.MissingBlobs) > 0 {
Expand Down
25 changes: 17 additions & 8 deletions pkg/compose/statfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,29 @@ func (u *UsageInfo) Print() {
FormatBytesUint64(u.Reserved), u.ReservedP)
}

func GetUsageInfo(path string, required int64, watermark uint) (*UsageInfo, error) {
// GetUsageInfo reports filesystem usage for path against a watermark that bounds
// how much storage apps may consume. The watermark is interpreted as a percentage
// of total size when watermarkInBytes is false, or as an absolute amount of free
// space to keep reserved (in bytes) when watermarkInBytes is true.
func GetUsageInfo(path string, required int64, watermark uint64, watermarkInBytes bool) (*UsageInfo, error) {
fsStat, err := GetFsStat(path)
if err != nil {
return nil, err
}
ui := UsageInfo{
Path: path,
SizeB: uint64(fsStat.BlockSize) * fsStat.Blocks,
Free: fsStat.Bfree * uint64(fsStat.BlockSize),
FreeP: (float32(fsStat.Bfree) / float32(fsStat.Blocks)) * 100.0,
ReservedP: float32(100 - watermark),
Required: uint64(required),
Path: path,
SizeB: uint64(fsStat.BlockSize) * fsStat.Blocks,
Free: fsStat.Bfree * uint64(fsStat.BlockSize),
FreeP: (float32(fsStat.Bfree) / float32(fsStat.Blocks)) * 100.0,
Required: uint64(required),
}
if watermarkInBytes {
ui.Reserved = watermark
ui.ReservedP = (float32(ui.Reserved) / float32(ui.SizeB)) * 100.0
} else {
ui.Reserved = uint64((float64(100-watermark) / 100.0) * float64(ui.SizeB))
ui.ReservedP = float32(100 - watermark)
}
ui.Reserved = uint64((float64(100-watermark) / 100.0) * float64(ui.SizeB))
ui.RequiredP = (float32(ui.Required) / float32(ui.SizeB)) * 100.0
if ui.Free > ui.Reserved {
ui.Available = ui.Free - ui.Reserved
Expand Down
43 changes: 43 additions & 0 deletions pkg/compose/statfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package compose

import "testing"

func TestGetUsageInfoPercentAndBytes(t *testing.T) {
dir := t.TempDir()

// Percent mode: reserved is (100-watermark)% of total size.
const watermark = 90
pui, err := GetUsageInfo(dir, 0, watermark, false)
if err != nil {
t.Fatalf("percent GetUsageInfo failed: %v", err)
}
wantReserved := uint64((float64(100-watermark) / 100.0) * float64(pui.SizeB))
if pui.Reserved != wantReserved {
t.Fatalf("percent reserved: got %d, want %d", pui.Reserved, wantReserved)
}
if pui.Free > pui.Reserved && pui.Available != pui.Free-pui.Reserved {
t.Fatalf("percent available: got %d, want %d", pui.Available, pui.Free-pui.Reserved)
}

// Bytes mode: reserved is the exact byte count and available is free minus it.
reserved := pui.Free / 4
bui, err := GetUsageInfo(dir, 0, reserved, true)
if err != nil {
t.Fatalf("bytes GetUsageInfo failed: %v", err)
}
if bui.Reserved != reserved {
t.Fatalf("bytes reserved: got %d, want %d", bui.Reserved, reserved)
}
if bui.Available != bui.Free-reserved {
t.Fatalf("bytes available: got %d, want %d", bui.Available, bui.Free-reserved)
}

// Bytes mode where the reservation exceeds free space clamps available to 0.
cui, err := GetUsageInfo(dir, 0, bui.Free+1, true)
if err != nil {
t.Fatalf("clamp GetUsageInfo failed: %v", err)
}
if cui.Available != 0 {
t.Fatalf("clamp available: got %d, want 0", cui.Available)
}
}
Loading