Skip to content

Commit 93fdf60

Browse files
committed
Add configurable cache size limit for S3 uploads
Skip uploading build cache to S3 when it exceeds the size limit. Configurable via APPPACK_MAX_CACHE_SIZE_GB environment variable (default: 7GB). Set to 0 to disable the limit entirely. Invalid or negative values fall back to the default with a warning.
1 parent c1cf649 commit 93fdf60

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

builder/build/build.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"io"
66
"os"
77
"os/exec"
8+
"path/filepath"
9+
"strconv"
810
"strings"
911
"sync"
1012

@@ -17,8 +19,10 @@ import (
1719
)
1820

1921
const (
20-
DockerHubMirror = "registry.apppackcdn.net"
21-
CacheDirectory = "/tmp/apppack-cache"
22+
DockerHubMirror = "registry.apppackcdn.net"
23+
CacheDirectory = "/tmp/apppack-cache"
24+
DefaultMaxCacheSizeGB = 7
25+
MaxCacheSizeEnvVar = "APPPACK_MAX_CACHE_SIZE_GB"
2226
)
2327

2428
func stripParamPrefix(params map[string]string, prefix string, final *map[string]string) {
@@ -220,8 +224,50 @@ func (b *Build) pushImages(config *containers.BuildConfig) error {
220224
return nil
221225
}
222226

227+
func dirSize(path string) (int64, error) {
228+
var size int64
229+
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
230+
if err != nil {
231+
return err
232+
}
233+
if !info.IsDir() {
234+
size += info.Size()
235+
}
236+
return nil
237+
})
238+
return size, err
239+
}
240+
241+
func getMaxCacheSizeGB() int {
242+
maxGB := DefaultMaxCacheSizeGB
243+
if val := os.Getenv(MaxCacheSizeEnvVar); val != "" {
244+
parsed, err := strconv.Atoi(val)
245+
if err != nil {
246+
fmt.Printf("WARNING: Invalid %s value '%s', using default %dGB\n", MaxCacheSizeEnvVar, val, DefaultMaxCacheSizeGB)
247+
} else if parsed < 0 {
248+
fmt.Printf("WARNING: Negative %s value '%d', using default %dGB\n", MaxCacheSizeEnvVar, parsed, DefaultMaxCacheSizeGB)
249+
} else {
250+
maxGB = parsed // 0 disables the limit
251+
}
252+
}
253+
return maxGB
254+
}
255+
223256
func (b *Build) archiveCache() error {
224257
fmt.Println("Archiving build cache to S3 ...")
258+
maxGB := getMaxCacheSizeGB()
259+
if maxGB > 0 {
260+
maxSize := int64(maxGB) * 1024 * 1024 * 1024
261+
size, err := dirSize(CacheDirectory)
262+
if err != nil {
263+
b.Log().Warn().Err(err).Msg("failed to calculate cache directory size")
264+
} else if size > maxSize {
265+
sizeMB := size / (1024 * 1024)
266+
fmt.Printf("WARNING: Cache directory is %dMB, exceeding %dGB limit. Skipping cache upload.\n", sizeMB, maxGB)
267+
b.Log().Warn().Int64("size_bytes", size).Int("max_gb", maxGB).Msg("cache directory exceeds size limit, skipping upload")
268+
return nil
269+
}
270+
}
225271
quiet := b.Log().GetLevel() > zerolog.DebugLevel
226272
return b.aws.SyncToS3(CacheDirectory, b.ArtifactBucket, "cache", quiet)
227273
}

builder/build/build_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package build
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57
"testing"
68
)
79

@@ -94,3 +96,80 @@ func TestGenerateDockerEnvStrings(t *testing.T) {
9496
t.Errorf("expected %d elements, got %d", len(expected), len(actual))
9597
}
9698
}
99+
100+
func TestGetMaxCacheSizeGB(t *testing.T) {
101+
tests := []struct {
102+
name string
103+
envValue string
104+
want int
105+
}{
106+
{"unset uses default", "", DefaultMaxCacheSizeGB},
107+
{"valid value", "10", 10},
108+
{"zero disables limit", "0", 0},
109+
{"invalid string falls back to default", "abc", DefaultMaxCacheSizeGB},
110+
{"negative falls back to default", "-5", DefaultMaxCacheSizeGB},
111+
{"float falls back to default", "7.5", DefaultMaxCacheSizeGB},
112+
{"whitespace falls back to default", " ", DefaultMaxCacheSizeGB},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
if tt.envValue == "" {
118+
os.Unsetenv(MaxCacheSizeEnvVar)
119+
} else {
120+
os.Setenv(MaxCacheSizeEnvVar, tt.envValue)
121+
}
122+
defer os.Unsetenv(MaxCacheSizeEnvVar)
123+
124+
got := getMaxCacheSizeGB()
125+
if got != tt.want {
126+
t.Errorf("getMaxCacheSizeGB() = %d, want %d", got, tt.want)
127+
}
128+
})
129+
}
130+
}
131+
132+
func TestDirSize(t *testing.T) {
133+
// Create a temp directory with known file sizes
134+
tmpDir, err := os.MkdirTemp("", "dirsize-test")
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
defer os.RemoveAll(tmpDir)
139+
140+
// Create files with known sizes
141+
file1 := filepath.Join(tmpDir, "file1.txt")
142+
file2 := filepath.Join(tmpDir, "file2.txt")
143+
if err := os.WriteFile(file1, make([]byte, 1000), 0644); err != nil {
144+
t.Fatal(err)
145+
}
146+
if err := os.WriteFile(file2, make([]byte, 500), 0644); err != nil {
147+
t.Fatal(err)
148+
}
149+
150+
// Create subdirectory with file
151+
subDir := filepath.Join(tmpDir, "subdir")
152+
if err := os.Mkdir(subDir, 0755); err != nil {
153+
t.Fatal(err)
154+
}
155+
file3 := filepath.Join(subDir, "file3.txt")
156+
if err := os.WriteFile(file3, make([]byte, 250), 0644); err != nil {
157+
t.Fatal(err)
158+
}
159+
160+
size, err := dirSize(tmpDir)
161+
if err != nil {
162+
t.Errorf("dirSize() error = %v", err)
163+
}
164+
expectedSize := int64(1750)
165+
if size != expectedSize {
166+
t.Errorf("dirSize() = %d, want %d", size, expectedSize)
167+
}
168+
}
169+
170+
func TestDirSizeNonExistent(t *testing.T) {
171+
_, err := dirSize("/nonexistent/path/that/does/not/exist")
172+
if err == nil {
173+
t.Error("dirSize() expected error for non-existent path, got nil")
174+
}
175+
}

0 commit comments

Comments
 (0)