@@ -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
18451893func 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