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
20 changes: 18 additions & 2 deletions pkg/controllers/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"path/filepath"
"sort"
"strings"
"syscall"
"text/template"
"time"

Expand Down Expand Up @@ -176,7 +177,7 @@ func MountSecrets(secrets []byte, mountPath string, maxReads int) (string, func(
return "", nil, Error{Err: errors.New("The mount path already exists. This may be due to another running instance of the Doppler CLI, or due to an improper shutdown. If this is unexpected, delete the file and try again.")}
}

if err := utils.CreateNamedPipe(mountPath, 0600); err != nil {
if err := utils.CreateNamedPipe(mountPath, 0o600); err != nil {
return "", nil, Error{Err: err, Message: "Unable to mount secrets file"}
}

Expand Down Expand Up @@ -224,14 +225,23 @@ func MountSecrets(secrets []byte, mountPath string, maxReads int) (string, func(
utils.HandleError(err, message)
}

numReads++
if enableReadsLimit {
numReads++
}
utils.LogDebug("Secrets mount opened by reader")

if _, err := f.Write(secrets); err != nil {
// race: cleanup has already begun; no need to error
if errors.Is(err, fs.ErrNotExist) && fifoCleanupStarted {
break
}
// broken pipe occurs when reader closes pipe before writing completes (eg. with vite dev server)
if errors.Is(err, syscall.EPIPE) {
utils.LogDebug("Reader closed pipe before write completed")
_ = f.Close()
continue
}

cleanupFIFO()
utils.HandleError(err, message)
}
Expand All @@ -241,6 +251,12 @@ func MountSecrets(secrets []byte, mountPath string, maxReads int) (string, func(
if errors.Is(err, fs.ErrNotExist) && fifoCleanupStarted {
break
}
// broken pipe on close is safe to ignore - the reader has already disconnected
if errors.Is(err, syscall.EPIPE) {
utils.LogDebug("Pipe closed by reader")
continue
}

cleanupFIFO()
utils.HandleError(err, message)
}
Expand Down
60 changes: 60 additions & 0 deletions pkg/controllers/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ limitations under the License.
package controllers

import (
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/DopplerHQ/cli/pkg/utils"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -106,3 +110,59 @@ func TestSecretsToBytes(t *testing.T) {
t.Errorf("Unable to convert secrets to byte array in %s format", format)
}
}

func TestMountSecrets(t *testing.T) {
if !utils.SupportsNamedPipes {
t.Skip("Named pipes not supported on this platform")
}

secrets := []byte(`{"SECRET_KEY":"secret_value"}`)
mountPath := filepath.Join(t.TempDir(), "secrets_mount")

path, cleanup, err := MountSecrets(secrets, mountPath, 1)
if !err.IsNil() {
t.Fatalf("MountSecrets failed: %v", err.Err)
}
defer cleanup()

time.Sleep(50 * time.Millisecond)

content, readErr := os.ReadFile(path)
assert.NoError(t, readErr)
assert.Equal(t, string(secrets), string(content))
}

func TestMountSecretsBrokenPipe(t *testing.T) {
if !utils.SupportsNamedPipes {
t.Skip("Named pipes not supported on this platform")
}

// large data to exceed pipe buffer and trigger EPIPE when reader closes early
secrets := make([]byte, 256*1024)
for i := range secrets {
secrets[i] = byte('A' + (i % 26))
}
mountPath := filepath.Join(t.TempDir(), "secrets_mount_epipe")

path, cleanup, err := MountSecrets(secrets, mountPath, 11)
if !err.IsNil() {
t.Fatalf("MountSecrets failed: %v", err.Err)
}
defer cleanup()

time.Sleep(50 * time.Millisecond)

for i := 0; i < 10; i++ {
f, openErr := os.OpenFile(path, os.O_RDONLY, 0)
if openErr != nil {
continue
}
f.Read(make([]byte, 1))
f.Close()
time.Sleep(5 * time.Millisecond)
}

content, readErr := os.ReadFile(path)
assert.NoError(t, readErr, "mount should survive broken pipe")
assert.Equal(t, secrets, content)
}