Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Bug Fixes:

- fix(kvstoreentry): close directory upload files and rewind before retrying ([#1942](https://github.com/fastly/cli/issues/1942))
- fix(compute/serve): provide default `override_host` to Viceroy via temporary manifest and handle IPv6 hostnames ([#1940](https://github.com/fastly/cli/pull/1940))

### Enhancements:
Expand Down
23 changes: 23 additions & 0 deletions pkg/commands/kvstoreentry/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ func (c *CreateCommand) ProcessDir(in io.Reader, out io.Writer) error {
mu.Unlock()
return
}
defer func() {
_ = f.Close()
}()

lr, err := fastly.FileLengthReader(f)
if err != nil {
Expand Down Expand Up @@ -317,6 +320,26 @@ func (c *CreateCommand) ProcessDir(in io.Reader, out io.Writer) error {
// NOTE: you can't type assert the error as it's not exported.
// https://github.com/golang/go/issues/54173
if strings.Contains(err.Error(), "net/http: cannot rewind body after connection loss") {
if _, seekErr := f.Seek(0, io.SeekStart); seekErr != nil {
mu.Lock()
processingErrors = append(processingErrors, ProcessErr{
File: filePath,
Err: seekErr,
})
mu.Unlock()
return
}
lr, rewindErr := fastly.FileLengthReader(f)
if rewindErr != nil {
mu.Lock()
processingErrors = append(processingErrors, ProcessErr{
File: filePath,
Err: rewindErr,
})
mu.Unlock()
return
}
opts.file = lr
err = insertKey(opts)
if err == nil {
return
Expand Down
128 changes: 128 additions & 0 deletions pkg/commands/kvstoreentry/create_processdir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package kvstoreentry

Check failure on line 1 in pkg/commands/kvstoreentry/create_processdir_test.go

View workflow job for this annotation

GitHub Actions / Lint (pinned golangci-lint version)

File is not properly formatted (gofumpt)

Check failure on line 1 in pkg/commands/kvstoreentry/create_processdir_test.go

View workflow job for this annotation

GitHub Actions / lint-latest (informational)

File is not properly formatted (gofumpt)

import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/fastly/go-fastly/v19/fastly"

"github.com/fastly/cli/pkg/argparser"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/mock"
)

func TestProcessDirClosesUploadedFiles(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "foo.txt")
if err := os.WriteFile(filePath, []byte("FOO\n"), 0o600); err != nil {
t.Fatal(err)
}

var capturedBody io.ReadCloser
cmd := &CreateCommand{
Base: argparser.Base{
Globals: &global.Data{
APIClient: &mock.API{
InsertKVStoreKeyFn: func(_ context.Context, in *fastly.InsertKVStoreKeyInput) error {
if in.Key == "foo.txt" && capturedBody == nil {
if closer, ok := in.Body.(io.ReadCloser); ok {
capturedBody = closer
}
}
return nil
},
},
Flags: global.Flags{AutoYes: true, NonInteractive: true},
},
},
dirPath: dir,
}

if err := cmd.ProcessDir(nil, io.Discard); err != nil {
t.Fatalf("ProcessDir: %v", err)
}
if capturedBody == nil {
t.Fatal("expected InsertKVStoreKey body to be captured")
}
if _, err := capturedBody.Read(make([]byte, 1)); !errors.Is(err, os.ErrClosed) {
t.Fatalf("expected body to be closed after upload, got read err: %v", err)
}
}

func TestProcessDirRetriesAfterConnectionLossWithRewoundBody(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "foo.txt")
if err := os.WriteFile(filePath, []byte("FOO\n"), 0o600); err != nil {
t.Fatal(err)
}

calls := 0
cmd := &CreateCommand{
Base: argparser.Base{
Globals: &global.Data{
APIClient: &mock.API{
InsertKVStoreKeyFn: func(_ context.Context, in *fastly.InsertKVStoreKeyInput) error {
calls++
if calls == 1 {
_, _ = io.ReadAll(in.Body)
return errors.New("net/http: cannot rewind body after connection loss")
}
data, err := io.ReadAll(in.Body)
if err != nil {
return err
}
if string(data) != "FOO\n" {
return errors.New("retry body mismatch")
}
return nil
},
},
Flags: global.Flags{AutoYes: true, NonInteractive: true},
},
},
dirPath: dir,
}

if err := cmd.ProcessDir(nil, io.Discard); err != nil {
t.Fatalf("ProcessDir: %v", err)
}
if calls != 2 {
t.Fatalf("expected 2 insert attempts, got %d", calls)
}
}

func TestProcessDirRetryStillFailsReportsConnectionLoss(t *testing.T) {
dir := t.TempDir()
filePath := filepath.Join(dir, "foo.txt")
if err := os.WriteFile(filePath, []byte("FOO\n"), 0o600); err != nil {
t.Fatal(err)
}

rewindErr := errors.New("net/http: cannot rewind body after connection loss")
cmd := &CreateCommand{
Base: argparser.Base{
Globals: &global.Data{
APIClient: &mock.API{
InsertKVStoreKeyFn: func(_ context.Context, _ *fastly.InsertKVStoreKeyInput) error {
return rewindErr
},
},
Flags: global.Flags{AutoYes: true, NonInteractive: true},
},
},
dirPath: dir,
}

err := cmd.ProcessDir(nil, io.Discard)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "failed to process all the provided files") {
t.Fatalf("unexpected error: %v", err)
}
}
Loading