From 20650860549322685e0673db447f95e9a8b5264e Mon Sep 17 00:00:00 2001 From: somaz Date: Mon, 28 Sep 2026 12:50:32 +0900 Subject: [PATCH] Skip files without a matching destination rule in recursive publish Signed-off-by: somaz --- cmd/sops/subcommand/publish/publish.go | 4 ++ cmd/sops/subcommand/publish/publish_test.go | 54 +++++++++++++++++++++ config/config.go | 16 +++++- config/config_test.go | 22 +++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 cmd/sops/subcommand/publish/publish_test.go diff --git a/cmd/sops/subcommand/publish/publish.go b/cmd/sops/subcommand/publish/publish.go index aed1118de5..3766e38553 100644 --- a/cmd/sops/subcommand/publish/publish.go +++ b/cmd/sops/subcommand/publish/publish.go @@ -49,6 +49,10 @@ func Run(opts Opts) error { conf, err := config.LoadDestinationRuleForFile(opts.ConfigPath, opts.InputPath, make(map[string]*string)) if err != nil { + if opts.Recursive && errors.Is(err, config.ErrNoMatchingDestination) { + fmt.Printf("Skipping %s: no matching destination rule\n", path) + return nil + } return err } if conf.Destination == nil { diff --git a/cmd/sops/subcommand/publish/publish_test.go b/cmd/sops/subcommand/publish/publish_test.go new file mode 100644 index 0000000000..51698f7d47 --- /dev/null +++ b/cmd/sops/subcommand/publish/publish_test.go @@ -0,0 +1,54 @@ +package publish + +import ( + "os" + "path/filepath" + "testing" + + "github.com/getsops/sops/v3/config" + "github.com/stretchr/testify/assert" +) + +func TestRunWithoutMatchingDestination(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, ".sops.yaml") + err := os.WriteFile(configPath, []byte(` +destination_rules: + - vault_path: "foo/" + path_regex: \.enc\.yml$ +`), 0o600) + assert.NoError(t, err) + inputPath := filepath.Join(dir, "parameters.yml") + err = os.WriteFile(inputPath, nil, 0o600) + assert.NoError(t, err) + + opts := Opts{ + ConfigPath: configPath, + InputPath: inputPath, + RootPath: dir, + Recursive: true, + } + assert.NoError(t, Run(opts)) + + opts.Recursive = false + assert.ErrorIs(t, Run(opts), config.ErrNoMatchingDestination) +} + +func TestRunRecursiveWithoutDestinationRules(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, ".sops.yaml") + err := os.WriteFile(configPath, []byte("creation_rules:\n - pgp: foo\n"), 0o600) + assert.NoError(t, err) + inputPath := filepath.Join(dir, "parameters.yml") + err = os.WriteFile(inputPath, nil, 0o600) + assert.NoError(t, err) + + err = Run(Opts{ + ConfigPath: configPath, + InputPath: inputPath, + RootPath: dir, + Recursive: true, + }) + assert.Error(t, err) + assert.NotErrorIs(t, err, config.ErrNoMatchingDestination) +} diff --git a/config/config.go b/config/config.go index 511df1bc15..be3ce46535 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ Package config provides a way to find and load SOPS configuration files package config //import "github.com/getsops/sops/v3/config" import ( + "errors" "fmt" "os" "path" @@ -507,6 +508,10 @@ func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) }, nil } +// ErrNoMatchingDestination is returned when the config has destination rules +// but none of them match the file path. +var ErrNoMatchingDestination = errors.New("no matching destination found in config") + func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) { var rule *creationRule var dRule *destinationRule @@ -519,7 +524,11 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio break } if r.PathRegex != "" { - if match, _ := regexp.MatchString(r.PathRegex, filePath); match { + reg, err := regexp.Compile(r.PathRegex) + if err != nil { + return nil, fmt.Errorf("can not compile regexp: %w", err) + } + if reg.MatchString(filePath) { dRule = &r rule = &dRule.RecreationRule break @@ -529,7 +538,10 @@ func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptio } if dRule == nil { - return nil, fmt.Errorf("error loading config: no matching destination found in config") + if len(conf.DestinationRules) == 0 { + return nil, errors.New("error loading config: no destination rules found in config") + } + return nil, fmt.Errorf("error loading config: %w", ErrNoMatchingDestination) } var dest publish.Destination diff --git a/config/config_test.go b/config/config_test.go index 04bed7f564..16e07ca4e9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -731,6 +731,28 @@ func TestLoadConfigFileWithVaultDestinationRules(t *testing.T) { assert.Contains(t, conf.Destination.Path("barfoo"), "/v1/kv/barfoo/barfoo") } +func TestLoadConfigFileWithNoMatchingDestinationRule(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfigWithVaultDestinationRules, t), "other/barfoo", nil) + assert.ErrorIs(t, err, ErrNoMatchingDestination) +} + +func TestLoadConfigFileWithoutDestinationRules(t *testing.T) { + _, err := parseDestinationRuleForFile(parseConfigFile(sampleConfig, t), "foobar2000", nil) + assert.EqualError(t, err, "error loading config: no destination rules found in config") + assert.NotErrorIs(t, err, ErrNoMatchingDestination) +} + +func TestLoadConfigFileWithInvalidDestinationRegex(t *testing.T) { + conf := parseConfigFile([]byte(` +destination_rules: + - vault_path: "foo/" + path_regex: "[" +`), t) + _, err := parseDestinationRuleForFile(conf, "foobar", nil) + assert.ErrorContains(t, err, "can not compile regexp") + assert.NotErrorIs(t, err, ErrNoMatchingDestination) +} + func TestCreationRuleNativeKeyLists(t *testing.T) { var sampleConfigWithNativeKeyLists = []byte(` creation_rules: