Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
129 changes: 103 additions & 26 deletions cli/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -304,6 +305,12 @@ all projects in a monorepo (looks for blaxel.toml in subdirectories).`,
return
}

if err = deployment.ValidateArchiveSize(); err != nil {
err = fmt.Errorf("error validating blaxel deployment: %w", err)
core.PrintError("Deploy", err)
core.ExitWithError(err)
}

startTime := time.Now()

if !noTTY {
Expand Down Expand Up @@ -367,6 +374,31 @@ type Deployment struct {
skipBuild bool
}

const maxArchiveUploadSize = 5 * 1024 * 1024 * 1024

var errArchiveTooLarge = errors.New("archive size exceeds the 5 GB upload limit")

func (d *Deployment) ValidateArchiveSize() error {
if d.archive == nil {
return nil
}
fileInfo, err := os.Stat(d.archive.Name())
if err != nil {
return fmt.Errorf("failed to get archive file info: %w", err)
}
return archiveSizeError(fileInfo.Size(), core.IsVolumeTemplate(core.GetConfig().Type))
}

func archiveSizeError(size int64, volumeTemplate bool) error {
if size <= maxArchiveUploadSize {
return nil
}
if volumeTemplate {
return fmt.Errorf("%w; reduce the files in the volume template directory (.blaxelignore is not used for volume templates)", errArchiveTooLarge)
}
return fmt.Errorf("%w; reduce the archive size by adding files or directories to .blaxelignore", errArchiveTooLarge)
}
Comment thread
cursor[bot] marked this conversation as resolved.

func (d *Deployment) Generate(skipBuild bool) error {
if d.name == "" {
d.name = filepath.Base(filepath.Join(d.cwd, d.folder))
Expand Down Expand Up @@ -1115,6 +1147,34 @@ func (d *Deployment) runInteractiveDeployment(resources []*deploy.Resource, addi
}
}()

if core.IsVolumeTemplate(core.GetConfig().Type) {
model.UpdateResource(0, deploy.StatusCompressing, "Compressing files", nil)
model.AddBuildLog(0, "Starting compression of volume template files...")

var lastLoggedProgress int
d.progressCallback = func(status string, progress int) {
model.UpdateResource(0, deploy.StatusCompressing, status, nil)
if progress > 0 && progress%10 == 0 && progress != lastLoggedProgress {
model.AddBuildLog(0, fmt.Sprintf("Compression progress: %d%%", progress))
lastLoggedProgress = progress
}
}

if err := d.Tar(); err != nil {
model.UpdateResource(0, deploy.StatusFailed, "Compression failed", err)
model.AddBuildLog(0, fmt.Sprintf("Failed to compress files: %v", err))
model.Complete()
return
}
if err := d.ValidateArchiveSize(); err != nil {
model.UpdateResource(0, deploy.StatusFailed, "Archive too large", err)
model.AddBuildLog(0, err.Error())
model.Complete()
return
}
model.AddBuildLog(0, "Compression completed (100%)")
}

// Determine where main resources end and additional resources begin
mainResourceCount := len(resources) - len(additionalResources)

Expand Down Expand Up @@ -1163,32 +1223,6 @@ func (d *Deployment) runInteractiveDeployment(resources []*deploy.Resource, addi
func (d *Deployment) deployResourceInteractive(resource *deploy.Resource, model *deploy.InteractiveModel, idx int, deployment core.Result) {
config := core.GetConfig()

// For volume templates, handle compression first
if core.IsVolumeTemplate(config.Type) {
model.UpdateResource(idx, deploy.StatusCompressing, "Compressing files", nil)
model.AddBuildLog(idx, "Starting compression of volume template files...")

// Set up progress callback for compression
var lastLoggedProgress int
d.progressCallback = func(status string, progress int) {
model.UpdateResource(idx, deploy.StatusCompressing, status, nil)
// Log every 10% to avoid log spam
if progress > 0 && progress%10 == 0 && progress != lastLoggedProgress {
model.AddBuildLog(idx, fmt.Sprintf("Compression progress: %d%%", progress))
lastLoggedProgress = progress
}
}

// Create the tar archive
err := d.Tar()
if err != nil {
model.UpdateResource(idx, deploy.StatusFailed, "Compression failed", err)
model.AddBuildLog(idx, fmt.Sprintf("Failed to compress files: %v", err))
return
}
model.AddBuildLog(idx, "Compression completed (100%)")
}

// Start deployment
model.UpdateResource(idx, deploy.StatusDeploying, "Applying resource", nil)
model.AddBuildLog(idx, fmt.Sprintf("Starting deployment of %s/%s", resource.Kind, resource.Name))
Expand Down Expand Up @@ -1975,6 +2009,9 @@ func (d *Deployment) UploadWithRetry(url string, refreshURL func() (string, erro
if lastErr == nil {
return nil
}
if errors.Is(lastErr, errArchiveTooLarge) {
return lastErr
}
}
return lastErr
}
Expand All @@ -1992,6 +2029,9 @@ func (d *Deployment) Upload(url string) error {
if err != nil {
return fmt.Errorf("failed to get file info: %w", err)
}
if err := archiveSizeError(fileInfo.Size(), core.IsVolumeTemplate(core.GetConfig().Type)); err != nil {
return err
}

// Wrap the file reader with progress tracking
var reader io.Reader = archiveFile
Expand Down Expand Up @@ -2312,6 +2352,43 @@ func (d *Deployment) Zip() error {
}

func (d *Deployment) Tar() error {
config := core.GetConfig()
volumeDir := config.Directory
if volumeDir == "" {
volumeDir = "."
}
archiveRoot := filepath.Join(d.cwd, volumeDir)
if _, err := os.Stat(archiveRoot); err != nil {
if os.IsNotExist(err) {
return core.MarkExpectedError(
fmt.Errorf("volume template directory does not exist: %s", volumeDir),
core.CLIErrorNotFound,
)
}
return fmt.Errorf("failed to inspect volume template directory %q: %w", volumeDir, err)
}

var size int64
err := filepath.WalkDir(archiveRoot, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.Type().IsRegular() && filepath.Base(path) != "blaxel.toml" {
info, err := entry.Info()
if err != nil {
return err
}
if info.Size() > maxArchiveUploadSize-size {
return archiveSizeError(maxArchiveUploadSize+1, true)
}
size += info.Size()
}
return nil
})
if err != nil {
return err
}

tarFile, err := os.CreateTemp("", ".blaxel.tar")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
Expand Down
84 changes: 84 additions & 0 deletions cli/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cli
import (
"archive/tar"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -560,6 +562,30 @@ directory = "app"
assert.Contains(t, archivedFiles, "index.html")
}

func TestVolumeTemplateTarRejectsOversizedFilesBeforeCreatingArchive(t *testing.T) {
tempDir := t.TempDir()
archiveDir := t.TempDir()
t.Setenv("TMPDIR", archiveDir)
file, err := os.Create(filepath.Join(tempDir, "oversized.bin"))
require.NoError(t, err)
require.NoError(t, file.Truncate(5*1024*1024*1024+1))
require.NoError(t, file.Close())

core.ResetConfig()
core.SetConfigType("volumetemplate")
t.Cleanup(core.ResetConfig)
d := Deployment{cwd: tempDir}

err = d.Tar()

require.EqualError(t, err, "archive size exceeds the 5 GB upload limit; reduce the files in the volume template directory (.blaxelignore is not used for volume templates)")
assert.ErrorIs(t, err, errArchiveTooLarge)
assert.Nil(t, d.archive)
archives, err := filepath.Glob(filepath.Join(archiveDir, ".blaxel.tar*"))
require.NoError(t, err)
assert.Empty(t, archives)
}

func TestDeploymentReadBlaxelToml(t *testing.T) {
// Create a temp directory with blaxel.toml
tempDir, err := os.MkdirTemp("", "deploy_test")
Expand Down Expand Up @@ -661,6 +687,64 @@ func TestProgressReaderCallback(t *testing.T) {
assert.Equal(t, int64(100), lastTotalBytes)
}

func TestUploadWithRetryRejectsOversizedArchiveWithoutRetrying(t *testing.T) {
archive, err := os.CreateTemp(t.TempDir(), "archive-*.zip")
require.NoError(t, err)
require.NoError(t, archive.Truncate(5*1024*1024*1024+1))
require.NoError(t, archive.Close())

d := Deployment{archive: archive}
refreshes := 0
err = d.UploadWithRetry("http://localhost", func() (string, error) {
refreshes++
return "http://localhost", nil
})

require.EqualError(t, err, "archive size exceeds the 5 GB upload limit; reduce the archive size by adding files or directories to .blaxelignore")
assert.Zero(t, refreshes)
}

func TestValidateArchiveSizeGuidance(t *testing.T) {
archive, err := os.CreateTemp(t.TempDir(), "archive-*.tar")
require.NoError(t, err)
require.NoError(t, archive.Truncate(5*1024*1024*1024+1))
require.NoError(t, archive.Close())
d := Deployment{archive: archive}

t.Run("source code", func(t *testing.T) {
core.SetConfigType("sandbox")
t.Cleanup(core.ResetConfig)
err := d.ValidateArchiveSize()
require.EqualError(t, err, "archive size exceeds the 5 GB upload limit; reduce the archive size by adding files or directories to .blaxelignore")
assert.ErrorIs(t, err, errArchiveTooLarge)
})

t.Run("volume template", func(t *testing.T) {
core.SetConfigType("volumetemplate")
t.Cleanup(core.ResetConfig)
err := d.ValidateArchiveSize()
require.EqualError(t, err, "archive size exceeds the 5 GB upload limit; reduce the files in the volume template directory (.blaxelignore is not used for volume templates)")
assert.ErrorIs(t, err, errArchiveTooLarge)
})
}

func TestUploadAllowsArchiveAtSizeLimit(t *testing.T) {
archive, err := os.CreateTemp(t.TempDir(), "archive-*.zip")
require.NoError(t, err)
require.NoError(t, archive.Truncate(5*1024*1024*1024))
require.NoError(t, archive.Close())

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "stop", http.StatusBadRequest)
}))
defer server.Close()

d := Deployment{archive: archive}
err = d.Upload(server.URL)

require.EqualError(t, err, "upload failed with status: 400 Bad Request")
}

func TestDeploymentWithJobConfig(t *testing.T) {
// Create a temp directory with blaxel.toml for job
tempDir, err := os.MkdirTemp("", "deploy_test")
Expand Down
4 changes: 4 additions & 0 deletions cli/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ For private registries, supply credentials via --registry-cred or --docker-confi
core.PrintError("Push", fmt.Errorf("failed to package source code: %w", err))
core.ExitWithError(err)
}
if err = deployment.ValidateArchiveSize(); err != nil {
core.PrintError("Push", err)
core.ExitWithError(err)
}

// Call POST /images to get the presigned URL
fmt.Println("Requesting image build...")
Expand Down
Loading