Skip to content

Commit a36846f

Browse files
committed
add --recursive flag
Signed-off-by: Aleksander Słomka <alex@alexslomka.xyz>
1 parent f9ae796 commit a36846f

File tree

2 files changed

+191
-125
lines changed

2 files changed

+191
-125
lines changed

cmd/sops/main.go

Lines changed: 173 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"context"
55
encodingjson "encoding/json"
66
"fmt"
7+
"io/fs"
78
"net"
89
"net/url"
910
"os"
1011
osExec "os/exec"
1112
"path/filepath"
1213
"reflect"
14+
"regexp"
15+
"slices"
1316
"strconv"
1417
"strings"
1518

@@ -1376,8 +1379,8 @@ func main() {
13761379
EnvVar: "SOPS_DECRYPTION_ORDER",
13771380
},
13781381
cli.BoolFlag{
1379-
Name: "idempotent",
1380-
Usage: "do nothing if the given index does not exist",
1382+
Name: "idempotent",
1383+
Usage: "do nothing if the given index does not exist",
13811384
},
13821385
}, keyserviceFlags...),
13831386
Action: func(c *cli.Context) error {
@@ -1626,6 +1629,10 @@ func main() {
16261629
Usage: "comma separated list of decryption key types",
16271630
EnvVar: "SOPS_DECRYPTION_ORDER",
16281631
},
1632+
cli.BoolFlag{
1633+
Name: "recursive",
1634+
Usage: "traverse all sub-directories and encrypt all files matching path_regex",
1635+
},
16291636
}, keyserviceFlags...)
16301637

16311638
app.Action = func(c *cli.Context) error {
@@ -1661,6 +1668,8 @@ func main() {
16611668
fileNameOverride := c.String("filename-override")
16621669
if fileNameOverride == "" {
16631670
fileNameOverride = fileName
1671+
} else if c.Bool("recursive") {
1672+
return common.NewExitError("Error: cannot operate on both --filename-override and --recursive", codes.ErrorConflictingParameters)
16641673
}
16651674

16661675
commandCount := 0
@@ -1683,163 +1692,202 @@ func main() {
16831692
// Load configuration here for backwards compatibility (error out in case of bad config files),
16841693
// but only when not just decrypting (https://github.com/getsops/sops/issues/868)
16851694
needsCreationRule := isEncryptMode || isRotateMode || isSetMode || isEditMode
1686-
if needsCreationRule {
1695+
if needsCreationRule && !c.Bool("recursive") {
16871696
_, err = loadConfig(c, fileNameOverride, nil)
16881697
if err != nil {
16891698
return toExitError(err)
16901699
}
16911700
}
16921701

1693-
inputStore := inputStore(c, fileNameOverride)
1694-
outputStore := outputStore(c, fileNameOverride)
16951702
svcs := keyservices(c)
16961703

16971704
order, err := decryptionOrder(c.String("decryption-order"))
16981705
if err != nil {
16991706
return toExitError(err)
17001707
}
1701-
var output []byte
1702-
if isEncryptMode {
1703-
encConfig, err := getEncryptConfig(c, fileNameOverride)
1704-
if err != nil {
1705-
return toExitError(err)
1706-
}
1707-
output, err = encrypt(encryptOpts{
1708-
OutputStore: outputStore,
1709-
InputStore: inputStore,
1710-
InputPath: fileName,
1711-
Cipher: aes.NewCipher(),
1712-
KeyServices: svcs,
1713-
encryptConfig: encConfig,
1714-
})
1715-
// While this check is also done below, the `err` in this scope shadows
1716-
// the `err` in the outer scope. **Only** do this in case --decrypt,
1717-
// --rotate-, and --set are not specified, though, to keep old behavior.
1718-
if err != nil && !isDecryptMode && !isRotateMode && !isSetMode {
1719-
return toExitError(err)
1720-
}
1708+
1709+
if c.Bool("recursive") {
1710+
return performActionRecursive(fileName, c, isEncryptMode, isEditMode, isDecryptMode, isRotateMode, isSetMode, svcs, order)
1711+
} else {
1712+
inputStore := inputStore(c, fileNameOverride)
1713+
outputStore := outputStore(c, fileNameOverride)
1714+
return performAction(isEncryptMode, isEditMode, isDecryptMode, isRotateMode, isSetMode, c, fileNameOverride, outputStore, inputStore, fileName, svcs, order)
17211715
}
1716+
}
1717+
err := app.Run(os.Args)
1718+
if err != nil {
1719+
log.Fatal(err)
1720+
}
1721+
}
17221722

1723-
if isDecryptMode {
1724-
var extract []interface{}
1725-
extract, err = parseTreePath(c.String("extract"))
1726-
if err != nil {
1727-
return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
1728-
}
1729-
output, err = decrypt(decryptOpts{
1730-
OutputStore: outputStore,
1731-
InputStore: inputStore,
1732-
InputPath: fileName,
1733-
Cipher: aes.NewCipher(),
1734-
Extract: extract,
1735-
KeyServices: svcs,
1736-
DecryptionOrder: order,
1737-
IgnoreMAC: c.Bool("ignore-mac"),
1738-
})
1723+
func performActionRecursive(fileName string, c *cli.Context, isEncryptMode bool, isEditMode bool, isDecryptMode bool, isRotateMode bool, isSetMode bool, svcs []keyservice.KeyServiceClient, order []string) error {
1724+
foundPath, err := config.FindConfigFile(".")
1725+
if err != nil {
1726+
return toExitError(err)
1727+
}
1728+
regs, err := config.LoadPathRegex(foundPath)
1729+
if err != nil {
1730+
return toExitError(err)
1731+
}
1732+
return filepath.Walk(fileName, func(path string, info fs.FileInfo, pathErr error) error {
1733+
checkMatch := func(r *regexp.Regexp) bool { return r.MatchString(path) }
1734+
if info.IsDir() || !slices.ContainsFunc(regs, checkMatch) {
1735+
return nil
17391736
}
1740-
if isRotateMode {
1741-
rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
1742-
if err != nil {
1743-
return toExitError(err)
1744-
}
1737+
inputStore := inputStore(c, path)
1738+
outputStore := outputStore(c, path)
1739+
err := performAction(isEncryptMode, isEditMode, isDecryptMode, isRotateMode, isSetMode, c, path, outputStore, inputStore, path, svcs, order)
1740+
if err != nil {
1741+
log.Errorln(err)
1742+
}
1743+
return nil
1744+
})
1745+
}
17451746

1746-
output, err = rotate(rotateOpts)
1747-
// While this check is also done below, the `err` in this scope shadows
1748-
// the `err` in the outer scope
1749-
if err != nil {
1750-
return toExitError(err)
1751-
}
1747+
func performAction(isEncryptMode bool, isEditMode bool, isDecryptMode bool, isRotateMode bool, isSetMode bool, c *cli.Context, fileNameOverride string, outputStore common.Store, inputStore common.Store, fileName string, svcs []keyservice.KeyServiceClient, order []string) error {
1748+
var output []byte
1749+
if isEncryptMode {
1750+
encConfig, err := getEncryptConfig(c, fileNameOverride)
1751+
if err != nil {
1752+
return toExitError(err)
1753+
}
1754+
output, err = encrypt(encryptOpts{
1755+
OutputStore: outputStore,
1756+
InputStore: inputStore,
1757+
InputPath: fileName,
1758+
Cipher: aes.NewCipher(),
1759+
KeyServices: svcs,
1760+
encryptConfig: encConfig,
1761+
})
1762+
// While this check is also done below, the `err` in this scope shadows
1763+
// the `err` in the outer scope. **Only** do this in case --decrypt,
1764+
// --rotate-, and --set are not specified, though, to keep old behavior.
1765+
if err != nil && !isDecryptMode && !isRotateMode && !isSetMode {
1766+
return toExitError(err)
17521767
}
1768+
}
17531769

1754-
if isSetMode {
1755-
var path []interface{}
1756-
var value interface{}
1757-
path, value, err = extractSetArguments(c.String("set"))
1758-
if err != nil {
1759-
return toExitError(err)
1760-
}
1761-
output, err = set(setOpts{
1762-
OutputStore: outputStore,
1763-
InputStore: inputStore,
1764-
InputPath: fileName,
1765-
Cipher: aes.NewCipher(),
1766-
KeyServices: svcs,
1767-
DecryptionOrder: order,
1768-
IgnoreMAC: c.Bool("ignore-mac"),
1769-
Value: value,
1770-
TreePath: path,
1771-
})
1770+
if isDecryptMode {
1771+
var extract []interface{}
1772+
extract, err := parseTreePath(c.String("extract"))
1773+
if err != nil {
1774+
return common.NewExitError(fmt.Errorf("error parsing --extract path: %s", err), codes.InvalidTreePathFormat)
1775+
}
1776+
output, err = decrypt(decryptOpts{
1777+
OutputStore: outputStore,
1778+
InputStore: inputStore,
1779+
InputPath: fileName,
1780+
Cipher: aes.NewCipher(),
1781+
Extract: extract,
1782+
KeyServices: svcs,
1783+
DecryptionOrder: order,
1784+
IgnoreMAC: c.Bool("ignore-mac"),
1785+
})
1786+
if err != nil {
1787+
return toExitError(err)
1788+
}
1789+
}
1790+
if isRotateMode {
1791+
rotateOpts, err := getRotateOpts(c, fileName, inputStore, outputStore, svcs, order)
1792+
if err != nil {
1793+
return toExitError(err)
17721794
}
17731795

1774-
if isEditMode {
1775-
_, statErr := os.Stat(fileName)
1776-
fileExists := statErr == nil
1777-
opts := editOpts{
1778-
OutputStore: outputStore,
1779-
InputStore: inputStore,
1780-
InputPath: fileName,
1781-
Cipher: aes.NewCipher(),
1782-
KeyServices: svcs,
1783-
DecryptionOrder: order,
1784-
IgnoreMAC: c.Bool("ignore-mac"),
1785-
ShowMasterKeys: c.Bool("show-master-keys"),
1786-
}
1787-
if fileExists {
1788-
output, err = edit(opts)
1789-
} else {
1790-
// File doesn't exist, edit the example file instead
1791-
encConfig, err := getEncryptConfig(c, fileNameOverride)
1792-
if err != nil {
1793-
return toExitError(err)
1794-
}
1795-
output, err = editExample(editExampleOpts{
1796-
editOpts: opts,
1797-
encryptConfig: encConfig,
1798-
})
1799-
// While this check is also done below, the `err` in this scope shadows
1800-
// the `err` in the outer scope
1801-
if err != nil {
1802-
return toExitError(err)
1803-
}
1804-
}
1796+
output, err = rotate(rotateOpts)
1797+
// While this check is also done below, the `err` in this scope shadows
1798+
// the `err` in the outer scope
1799+
if err != nil {
1800+
return toExitError(err)
18051801
}
1802+
}
18061803

1804+
if isSetMode {
1805+
var path []interface{}
1806+
var value interface{}
1807+
path, value, err := extractSetArguments(c.String("set"))
1808+
if err != nil {
1809+
return toExitError(err)
1810+
}
1811+
output, err = set(setOpts{
1812+
OutputStore: outputStore,
1813+
InputStore: inputStore,
1814+
InputPath: fileName,
1815+
Cipher: aes.NewCipher(),
1816+
KeyServices: svcs,
1817+
DecryptionOrder: order,
1818+
IgnoreMAC: c.Bool("ignore-mac"),
1819+
Value: value,
1820+
TreePath: path,
1821+
})
18071822
if err != nil {
18081823
return toExitError(err)
18091824
}
1825+
}
18101826

1811-
// We open the file *after* the operations on the tree have been
1812-
// executed to avoid truncating it when there's errors
1813-
if c.Bool("in-place") || isEditMode || isSetMode {
1814-
file, err := os.Create(fileName)
1827+
if isEditMode {
1828+
_, statErr := os.Stat(fileName)
1829+
fileExists := statErr == nil
1830+
opts := editOpts{
1831+
OutputStore: outputStore,
1832+
InputStore: inputStore,
1833+
InputPath: fileName,
1834+
Cipher: aes.NewCipher(),
1835+
KeyServices: svcs,
1836+
DecryptionOrder: order,
1837+
IgnoreMAC: c.Bool("ignore-mac"),
1838+
ShowMasterKeys: c.Bool("show-master-keys"),
1839+
}
1840+
if fileExists {
1841+
var err error
1842+
output, err = edit(opts)
18151843
if err != nil {
1816-
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
1844+
return toExitError(err)
18171845
}
1818-
defer file.Close()
1819-
_, err = file.Write(output)
1846+
} else {
1847+
// File doesn't exist, edit the example file instead
1848+
encConfig, err := getEncryptConfig(c, fileNameOverride)
18201849
if err != nil {
18211850
return toExitError(err)
18221851
}
1823-
log.Info("File written successfully")
1824-
return nil
1825-
}
1826-
1827-
outputFile := os.Stdout
1828-
if c.String("output") != "" {
1829-
file, err := os.Create(c.String("output"))
1852+
output, err = editExample(editExampleOpts{
1853+
editOpts: opts,
1854+
encryptConfig: encConfig,
1855+
})
1856+
// While this check is also done below, the `err` in this scope shadows
1857+
// the `err` in the outer scope
18301858
if err != nil {
1831-
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
1859+
return toExitError(err)
18321860
}
1833-
defer file.Close()
1834-
outputFile = file
18351861
}
1836-
_, err = outputFile.Write(output)
1837-
return toExitError(err)
18381862
}
1839-
err := app.Run(os.Args)
1840-
if err != nil {
1841-
log.Fatal(err)
1863+
1864+
// We open the file *after* the operations on the tree have been
1865+
// executed to avoid truncating it when there's errors
1866+
if c.Bool("in-place") || isEditMode || isSetMode {
1867+
file, err := os.Create(fileName)
1868+
if err != nil {
1869+
return common.NewExitError(fmt.Sprintf("Could not open in-place file for writing: %s", err), codes.CouldNotWriteOutputFile)
1870+
}
1871+
defer file.Close()
1872+
_, err = file.Write(output)
1873+
if err != nil {
1874+
return toExitError(err)
1875+
}
1876+
log.Info("File written successfully")
1877+
return nil
1878+
}
1879+
1880+
outputFile := os.Stdout
1881+
if c.String("output") != "" {
1882+
file, err := os.Create(c.String("output"))
1883+
if err != nil {
1884+
return common.NewExitError(fmt.Sprintf("Could not open output file for writing: %s", err), codes.CouldNotWriteOutputFile)
1885+
}
1886+
defer file.Close()
1887+
outputFile = file
18421888
}
1889+
_, err := outputFile.Write(output)
1890+
return toExitError(err)
18431891
}
18441892

18451893
func getEncryptConfig(c *cli.Context, fileName string) (encryptConfig, error) {
@@ -2030,7 +2078,7 @@ func keyservices(c *cli.Context) (svcs []keyservice.KeyServiceClient) {
20302078
"address",
20312079
fmt.Sprintf("%s://%s", url.Scheme, addr),
20322080
).Infof("Connecting to key service")
2033-
conn, err := grpc.Dial(addr, opts...)
2081+
conn, err := grpc.NewClient(addr, opts...)
20342082
if err != nil {
20352083
log.Fatalf("failed to listen: %v", err)
20362084
}
@@ -2159,7 +2207,7 @@ func keyGroups(c *cli.Context, file string) ([]sops.KeyGroup, error) {
21592207
if err != nil {
21602208
errMsg = fmt.Sprintf("%s: %s", errMsg, err)
21612209
}
2162-
return nil, fmt.Errorf(errMsg)
2210+
return nil, fmt.Errorf("%s", errMsg)
21632211
}
21642212
return conf.KeyGroups, err
21652213
}

0 commit comments

Comments
 (0)