diff --git a/builder/build/build.go b/builder/build/build.go index 3a7605d..8ae4c3b 100644 --- a/builder/build/build.go +++ b/builder/build/build.go @@ -5,6 +5,8 @@ import ( "io" "os" "os/exec" + "path/filepath" + "strconv" "strings" "sync" @@ -17,8 +19,10 @@ import ( ) const ( - DockerHubMirror = "registry.apppackcdn.net" - CacheDirectory = "/tmp/apppack-cache" + DockerHubMirror = "registry.apppackcdn.net" + CacheDirectory = "/tmp/apppack-cache" + DefaultMaxCacheSizeGB = 7 + MaxCacheSizeEnvVar = "APPPACK_MAX_CACHE_SIZE_GB" ) func stripParamPrefix(params map[string]string, prefix string, final *map[string]string) { @@ -220,8 +224,50 @@ func (b *Build) pushImages(config *containers.BuildConfig) error { return nil } +func dirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + size += info.Size() + } + return nil + }) + return size, err +} + +func getMaxCacheSizeGB() int { + maxGB := DefaultMaxCacheSizeGB + if val := os.Getenv(MaxCacheSizeEnvVar); val != "" { + parsed, err := strconv.Atoi(val) + if err != nil { + fmt.Printf("WARNING: Invalid %s value '%s', using default %dGB\n", MaxCacheSizeEnvVar, val, DefaultMaxCacheSizeGB) + } else if parsed < 0 { + fmt.Printf("WARNING: Negative %s value '%d', using default %dGB\n", MaxCacheSizeEnvVar, parsed, DefaultMaxCacheSizeGB) + } else { + maxGB = parsed // 0 disables the limit + } + } + return maxGB +} + func (b *Build) archiveCache() error { fmt.Println("Archiving build cache to S3 ...") + maxGB := getMaxCacheSizeGB() + if maxGB > 0 { + maxSize := int64(maxGB) * 1024 * 1024 * 1024 + size, err := dirSize(CacheDirectory) + if err != nil { + b.Log().Warn().Err(err).Msg("failed to calculate cache directory size") + } else if size > maxSize { + sizeMB := size / (1024 * 1024) + fmt.Printf("WARNING: Cache directory is %dMB, exceeding %dGB limit. Skipping cache upload.\n", sizeMB, maxGB) + b.Log().Warn().Int64("size_bytes", size).Int("max_gb", maxGB).Msg("cache directory exceeds size limit, skipping upload") + return nil + } + } quiet := b.Log().GetLevel() > zerolog.DebugLevel return b.aws.SyncToS3(CacheDirectory, b.ArtifactBucket, "cache", quiet) } diff --git a/builder/build/build_test.go b/builder/build/build_test.go index 7e760e5..e7f10a2 100644 --- a/builder/build/build_test.go +++ b/builder/build/build_test.go @@ -2,6 +2,8 @@ package build import ( "fmt" + "os" + "path/filepath" "testing" ) @@ -94,3 +96,80 @@ func TestGenerateDockerEnvStrings(t *testing.T) { t.Errorf("expected %d elements, got %d", len(expected), len(actual)) } } + +func TestGetMaxCacheSizeGB(t *testing.T) { + tests := []struct { + name string + envValue string + want int + }{ + {"unset uses default", "", DefaultMaxCacheSizeGB}, + {"valid value", "10", 10}, + {"zero disables limit", "0", 0}, + {"invalid string falls back to default", "abc", DefaultMaxCacheSizeGB}, + {"negative falls back to default", "-5", DefaultMaxCacheSizeGB}, + {"float falls back to default", "7.5", DefaultMaxCacheSizeGB}, + {"whitespace falls back to default", " ", DefaultMaxCacheSizeGB}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue == "" { + os.Unsetenv(MaxCacheSizeEnvVar) + } else { + os.Setenv(MaxCacheSizeEnvVar, tt.envValue) + } + defer os.Unsetenv(MaxCacheSizeEnvVar) + + got := getMaxCacheSizeGB() + if got != tt.want { + t.Errorf("getMaxCacheSizeGB() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestDirSize(t *testing.T) { + // Create a temp directory with known file sizes + tmpDir, err := os.MkdirTemp("", "dirsize-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Create files with known sizes + file1 := filepath.Join(tmpDir, "file1.txt") + file2 := filepath.Join(tmpDir, "file2.txt") + if err := os.WriteFile(file1, make([]byte, 1000), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file2, make([]byte, 500), 0644); err != nil { + t.Fatal(err) + } + + // Create subdirectory with file + subDir := filepath.Join(tmpDir, "subdir") + if err := os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + file3 := filepath.Join(subDir, "file3.txt") + if err := os.WriteFile(file3, make([]byte, 250), 0644); err != nil { + t.Fatal(err) + } + + size, err := dirSize(tmpDir) + if err != nil { + t.Errorf("dirSize() error = %v", err) + } + expectedSize := int64(1750) + if size != expectedSize { + t.Errorf("dirSize() = %d, want %d", size, expectedSize) + } +} + +func TestDirSizeNonExistent(t *testing.T) { + _, err := dirSize("/nonexistent/path/that/does/not/exist") + if err == nil { + t.Error("dirSize() expected error for non-existent path, got nil") + } +}