From c162183713a3804f0020bcaad2de1c4f1f7cf4d0 Mon Sep 17 00:00:00 2001 From: dantedelucia Date: Wed, 17 Sep 2025 18:17:12 -0700 Subject: [PATCH 1/2] Join execSession.Close() error with existing err --- pkg/runner/deployer/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/runner/deployer/ssh.go b/pkg/runner/deployer/ssh.go index 024549b..e431ed8 100644 --- a/pkg/runner/deployer/ssh.go +++ b/pkg/runner/deployer/ssh.go @@ -102,7 +102,7 @@ func (p *SSH) Run(cmdSegs []string, handler DeployerHandler) (err error) { defer func() { if closeErr := execSession.Close(); closeErr != io.EOF { - err = errors.Join(closeErr) + err = errors.Join(err, closeErr) } }() From d05f56ce05762716da71212caee35f4e8b3351eb Mon Sep 17 00:00:00 2001 From: dantedelucia Date: Wed, 17 Sep 2025 10:48:00 -0700 Subject: [PATCH 2/2] Check for free space on error --- pkg/runner/deployer/filesystemstats.go | 49 ++++++++++++++++++++++++++ pkg/runner/deployer/ssh.go | 28 +++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 pkg/runner/deployer/filesystemstats.go diff --git a/pkg/runner/deployer/filesystemstats.go b/pkg/runner/deployer/filesystemstats.go new file mode 100644 index 0000000..7ae04cb --- /dev/null +++ b/pkg/runner/deployer/filesystemstats.go @@ -0,0 +1,49 @@ +package deployer + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "golang.org/x/crypto/ssh" + "io" +) + +type FileSystemStats struct { + Path string `json:"path"` + BlockSize uint64 `json:"blockSize"` + FreeBlocks uint64 `json:"freeBlocks"` +} + +func (fss *FileSystemStats) FreeBytes() uint64 { + return fss.BlockSize * fss.FreeBlocks +} + +func GetFileSystemStats(client *ssh.Client, path string) (stats FileSystemStats, err error) { + if client == nil { + return FileSystemStats{}, fmt.Errorf("SSH client cannot be nil") + } + + execSession, err := client.NewSession() + if err != nil { + return + } + + defer func() { + if closeErr := execSession.Close(); closeErr != io.EOF { + err = errors.Join(err, closeErr) + } + }() + + cmd := fmt.Sprintf("stat -f -c '{\"path\": %q, \"blockSize\": %%S, \"freeBlocks\": %%a}' %q", path, path) + + out, err := execSession.CombinedOutput(cmd) + if err != nil { + return stats, fmt.Errorf("remote stat failed (output: %q): %w", out, err) + } + + if err := json.Unmarshal(bytes.TrimSpace(out), &stats); err != nil { + return stats, fmt.Errorf("invalid JSON from remote stat (got %v): %w", string(out), err) + } + return +} diff --git a/pkg/runner/deployer/ssh.go b/pkg/runner/deployer/ssh.go index e431ed8..2714fa0 100644 --- a/pkg/runner/deployer/ssh.go +++ b/pkg/runner/deployer/ssh.go @@ -43,15 +43,22 @@ func (p *SSH) Deploy(statusCallback ProgressStatusCallback) (err error) { path := filepath.Join(p.Payload.RootPath, f.Path) dir := filepath.Dir(path) + parentDir := filepath.Dir(dir) if err := sftpClient.MkdirAll(dir); err != nil { - return fmt.Errorf("failed to create remote directory for %s: %w", dir, err) + return errors.Join( + fmt.Errorf("failed to create remote directory for %s: %w", dir, err), + p.checkFSSpace(parentDir), + ) } remoteFile, err := sftpClient.Create(path) if err != nil { - return fmt.Errorf("failed to create remote file %s: %w", path, err) + return errors.Join( + fmt.Errorf("failed to create remote file %s: %w", path, err), + p.checkFSSpace(parentDir), + ) } defer func() { @@ -72,7 +79,9 @@ func (p *SSH) Deploy(statusCallback ProgressStatusCallback) (err error) { } if _, err := io.Copy(remoteFile, tracker); err != nil { - return fmt.Errorf("failed to write to remote file %s: %w", path, err) + return errors.Join( + fmt.Errorf("failed to write to remote file %s: %w", path, err), + p.checkFSSpace(parentDir)) } } return nil @@ -135,3 +144,16 @@ func (p *SSH) Run(cmdSegs []string, handler DeployerHandler) (err error) { return nil } + +func (p *SSH) checkFSSpace(path string) error { + stats, err := GetFileSystemStats(p.Client, path) + if err != nil { + return fmt.Errorf("fsblocks check failed for %s: %w", path, err) + } + + if stats.FreeBytes() == 0 { + return fmt.Errorf("no space on device %s", stats.Path) + } + + return nil +}