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
5 changes: 4 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2099,7 +2099,10 @@ Stores configuration object

The store configuration object can have the following keys:

* ``dotenv``: this is an object. Right now no keys are supported.
* ``dotenv``: this is an object, supporting the following keys:

* ``quote`` (boolean; default ``false``): when ``true``, values are
double-quoted on emit.

* ``ini``: this is an object. Right now no keys are supported.

Expand Down
4 changes: 3 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ func FindConfigFile(start string) (string, error) {
return result.Path, err
}

type DotenvStoreConfig struct{}
type DotenvStoreConfig struct {
Quote bool `yaml:"quote"`
}

type INIStoreConfig struct{}

Expand Down
36 changes: 33 additions & 3 deletions stores/dotenv/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dotenv //import "github.com/getsops/sops/v3/stores/dotenv"
import (
"bytes"
"fmt"
"strconv"
"strings"

"github.com/getsops/sops/v3"
Expand Down Expand Up @@ -44,10 +45,26 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) {
// LoadPlainFile returns the contents of a plaintext file loaded onto a
// sops runtime object
func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) {
lines := bytes.Split(in, []byte("\n"))

// Detect quoted vs unquoted by looking at the first metadata value
quote := store.config.Quote
for _, line := range lines {
if !bytes.HasPrefix(line, []byte(stores.SopsPrefix)) {
continue
}
_, raw, ok := bytes.Cut(line, []byte("="))
if !ok {
continue
}
quote = bytes.HasPrefix(raw, []byte(`"`))
break
}

var branches sops.TreeBranches
var branch sops.TreeBranch

for _, line := range bytes.Split(in, []byte("\n")) {
for _, line := range lines {
if len(line) == 0 {
continue
}
Expand All @@ -61,9 +78,19 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) {
if pos == -1 {
return nil, fmt.Errorf("invalid dotenv input line: %s", line)
}
var value string
if quote {
var err error
value, err = strconv.Unquote(string(line[pos+1:]))
if err != nil {
return nil, fmt.Errorf("invalid quoted dotenv value for key %q: %w", line[:pos], err)
}
} else {
value = strings.Replace(string(line[pos+1:]), "\\n", "\n", -1)
}
branch = append(branch, sops.TreeItem{
Key: string(line[:pos]),
Value: strings.Replace(string(line[pos+1:]), "\\n", "\n", -1),
Value: value,
})
}
}
Expand Down Expand Up @@ -99,7 +126,10 @@ func (store *Store) EmitPlainFile(in sops.TreeBranches) ([]byte, error) {
value, ok := item.Value.(string)
if !ok {
value = stores.ValToString(item.Value)
} else {
}
if store.config.Quote {
value = strconv.Quote(value)
} else if ok {
value = strings.ReplaceAll(value, "\n", "\\n")
}

Expand Down
72 changes: 72 additions & 0 deletions stores/dotenv/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -88,6 +89,77 @@ func TestEmitEncryptedFileStability(t *testing.T) {
}
}

var QUOTED_PLAIN = []byte(strings.TrimLeft(`
VAR1="val1"
VAR2="val2"
#comment
VAR3_unencrypted="val3"
VAR4="val4\nval4"
JSON="{ \"app_id\": \"123\" }"
`, "\n"))

var QUOTED_BRANCH = sops.TreeBranch{
sops.TreeItem{
Key: "VAR1",
Value: "val1",
},
sops.TreeItem{
Key: "VAR2",
Value: "val2",
},
sops.TreeItem{
Key: sops.Comment{Value: "comment"},
Value: nil,
},
sops.TreeItem{
Key: "VAR3_unencrypted",
Value: "val3",
},
sops.TreeItem{
Key: "VAR4",
Value: "val4\nval4",
},
sops.TreeItem{
Key: "JSON",
Value: `{ "app_id": "123" }`,
},
}

func TestQuotedLoadPlainFile(t *testing.T) {
branches, err := (&Store{config: config.DotenvStoreConfig{Quote: true}}).LoadPlainFile(QUOTED_PLAIN)
assert.Nil(t, err)
assert.Equal(t, QUOTED_BRANCH, branches[0])
}

func TestQuotedEmitPlainFile(t *testing.T) {
branches := sops.TreeBranches{
QUOTED_BRANCH,
}
bytes, err := (&Store{config: config.DotenvStoreConfig{Quote: true}}).EmitPlainFile(branches)
assert.Nil(t, err)
assert.Equal(t, QUOTED_PLAIN, bytes)
}

func TestQuotedLoadDetectsUnquoted(t *testing.T) {
unquoted, err := (&Store{}).EmitEncryptedFile(sops.Tree{
Branches: sops.TreeBranches{BRANCH},
})
assert.Nil(t, err)
branches, err := (&Store{config: config.DotenvStoreConfig{Quote: true}}).LoadPlainFile(unquoted)
assert.Nil(t, err)
assert.Equal(t, BRANCH, branches[0][:len(BRANCH)])
}

func TestUnquotedLoadDetectsQuoted(t *testing.T) {
quoted, err := (&Store{config: config.DotenvStoreConfig{Quote: true}}).EmitEncryptedFile(sops.Tree{
Branches: sops.TreeBranches{BRANCH},
})
assert.Nil(t, err)
branches, err := (&Store{}).LoadPlainFile(quoted)
assert.Nil(t, err)
assert.Equal(t, BRANCH, branches[0][:len(BRANCH)])
}

func TestHasSopsTopLevelKey(t *testing.T) {
ok := (&Store{}).HasSopsTopLevelKey(sops.TreeBranch{
sops.TreeItem{
Expand Down
Loading