Skip to content

Commit 45cdd26

Browse files
authored
Merge pull request #1754 from felixfontein/set-delete-idempotent
set subcommand: add --idempotent flag that will not write the file if no change happened
2 parents 8122a30 + 4c7dda8 commit 45cdd26

File tree

5 files changed

+231
-30
lines changed

5 files changed

+231
-30
lines changed

cmd/sops/main.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,10 @@ func main() {
13561356
Usage: "comma separated list of decryption key types",
13571357
EnvVar: "SOPS_DECRYPTION_ORDER",
13581358
},
1359+
cli.BoolFlag{
1360+
Name: "idempotent",
1361+
Usage: "do nothing if the given index already has the given value",
1362+
},
13591363
}, keyserviceFlags...),
13601364
Action: func(c *cli.Context) error {
13611365
if c.Bool("verbose") {
@@ -1393,7 +1397,7 @@ func main() {
13931397
if err != nil {
13941398
return toExitError(err)
13951399
}
1396-
output, err := set(setOpts{
1400+
output, changed, err := set(setOpts{
13971401
OutputStore: outputStore,
13981402
InputStore: inputStore,
13991403
InputPath: fileName,
@@ -1408,6 +1412,11 @@ func main() {
14081412
return toExitError(err)
14091413
}
14101414

1415+
if !changed && c.Bool("idempotent") {
1416+
log.Info("File not written due to no change")
1417+
return nil
1418+
}
1419+
14111420
// We open the file *after* the operations on the tree have been
14121421
// executed to avoid truncating it when there's errors
14131422
file, err := os.Create(fileName)
@@ -1845,7 +1854,7 @@ func main() {
18451854
if err != nil {
18461855
return toExitError(err)
18471856
}
1848-
output, err = set(setOpts{
1857+
output, _, err = set(setOpts{
18491858
OutputStore: outputStore,
18501859
InputStore: inputStore,
18511860
InputPath: fileName,

cmd/sops/set.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type setOpts struct {
2121
DecryptionOrder []string
2222
}
2323

24-
func set(opts setOpts) ([]byte, error) {
24+
func set(opts setOpts) ([]byte, bool, error) {
2525
// Load the file
2626
// TODO: Issue #173: if the file does not exist, create it with the contents passed in as opts.Value
2727
tree, err := common.LoadEncryptedFileWithBugFixes(common.GenericDecryptOpts{
@@ -32,7 +32,7 @@ func set(opts setOpts) ([]byte, error) {
3232
KeyServices: opts.KeyServices,
3333
})
3434
if err != nil {
35-
return nil, err
35+
return nil, false, err
3636
}
3737

3838
// Decrypt the file
@@ -44,22 +44,23 @@ func set(opts setOpts) ([]byte, error) {
4444
DecryptionOrder: opts.DecryptionOrder,
4545
})
4646
if err != nil {
47-
return nil, err
47+
return nil, false, err
4848
}
4949

5050
// Set the value
51-
tree.Branches[0] = tree.Branches[0].Set(opts.TreePath, opts.Value)
51+
var changed bool
52+
tree.Branches[0], changed = tree.Branches[0].Set(opts.TreePath, opts.Value)
5253

5354
err = common.EncryptTree(common.EncryptTreeOpts{
5455
DataKey: dataKey, Tree: tree, Cipher: opts.Cipher,
5556
})
5657
if err != nil {
57-
return nil, err
58+
return nil, false, err
5859
}
5960

6061
encryptedFile, err := opts.OutputStore.EmitEncryptedFile(*tree)
6162
if err != nil {
62-
return nil, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
63+
return nil, false, common.NewExitError(fmt.Sprintf("Could not marshal tree: %s", err), codes.ErrorDumpingTree)
6364
}
64-
return encryptedFile, err
65+
return encryptedFile, changed, err
6566
}

functional-tests/src/lib.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,115 @@ bar: baz",
296296
panic!("Output JSON does not have the expected structure");
297297
}
298298

299+
#[test]
300+
fn set_json_file_update_idempotent_write() {
301+
let file_path = prepare_temp_file(
302+
"test_set_update_idempotent_write.json",
303+
r#"{"a": 2, "b": "ba"}"#.as_bytes(),
304+
);
305+
assert!(
306+
Command::new(SOPS_BINARY_PATH)
307+
.arg("encrypt")
308+
.arg("-i")
309+
.arg(file_path.clone())
310+
.output()
311+
.expect("Error running sops")
312+
.status
313+
.success(),
314+
"sops didn't exit successfully"
315+
);
316+
let mut before = String::new();
317+
File::open(file_path.clone())
318+
.unwrap()
319+
.read_to_string(&mut before)
320+
.unwrap();
321+
let output = Command::new(SOPS_BINARY_PATH)
322+
.arg("set")
323+
.arg("--output-type")
324+
.arg("yaml")
325+
.arg(file_path.clone())
326+
.arg(r#"["b"]"#)
327+
.arg(r#""ba""#)
328+
.output()
329+
.expect("Error running sops");
330+
println!(
331+
"stdout: {}, stderr: {}",
332+
String::from_utf8_lossy(&output.stdout),
333+
String::from_utf8_lossy(&output.stderr)
334+
);
335+
assert!(output.status.success(), "sops didn't exit successfully");
336+
let mut after = String::new();
337+
File::open(file_path.clone())
338+
.unwrap()
339+
.read_to_string(&mut after)
340+
.unwrap();
341+
assert!(before != after);
342+
assert!(after.starts_with("a: "));
343+
let output = Command::new(SOPS_BINARY_PATH)
344+
.arg("decrypt")
345+
.arg("--input-type")
346+
.arg("yaml")
347+
.arg("--output-type")
348+
.arg("yaml")
349+
.arg(file_path.clone())
350+
.output()
351+
.expect("Error running sops");
352+
println!(
353+
"stdout: {}, stderr: {}",
354+
String::from_utf8_lossy(&output.stdout),
355+
String::from_utf8_lossy(&output.stderr)
356+
);
357+
let data = &String::from_utf8_lossy(&output.stdout);
358+
assert!(data == "a: 2\nb: ba\n");
359+
}
360+
361+
#[test]
362+
fn set_json_file_update_idempotent_nowrite() {
363+
let file_path = prepare_temp_file(
364+
"test_set_update_idempotent_nowrite.json",
365+
r#"{"a": 2, "b": "ba"}"#.as_bytes(),
366+
);
367+
assert!(
368+
Command::new(SOPS_BINARY_PATH)
369+
.arg("encrypt")
370+
.arg("-i")
371+
.arg(file_path.clone())
372+
.output()
373+
.expect("Error running sops")
374+
.status
375+
.success(),
376+
"sops didn't exit successfully"
377+
);
378+
let mut before = String::new();
379+
File::open(file_path.clone())
380+
.unwrap()
381+
.read_to_string(&mut before)
382+
.unwrap();
383+
let output = Command::new(SOPS_BINARY_PATH)
384+
.arg("set")
385+
.arg("--output-type")
386+
.arg("yaml")
387+
.arg("--idempotent")
388+
.arg(file_path.clone())
389+
.arg(r#"["b"]"#)
390+
.arg(r#""ba""#)
391+
.output()
392+
.expect("Error running sops");
393+
println!(
394+
"stdout: {}, stderr: {}",
395+
String::from_utf8_lossy(&output.stdout),
396+
String::from_utf8_lossy(&output.stderr)
397+
);
398+
assert!(output.status.success(), "sops didn't exit successfully");
399+
let mut after = String::new();
400+
File::open(file_path.clone())
401+
.unwrap()
402+
.read_to_string(&mut after)
403+
.unwrap();
404+
println!("before: {}\nafter: {}", &before, &after,);
405+
assert!(before == after);
406+
}
407+
299408
#[test]
300409
fn set_json_file_insert() {
301410
let file_path =

sops.go

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,48 @@ type TreeBranch []TreeItem
127127
// Trees usually have more than one branch
128128
type TreeBranches []TreeBranch
129129

130+
func equals(oneBranch interface{}, otherBranch interface{}) bool {
131+
switch oneBranch := oneBranch.(type) {
132+
case TreeBranch:
133+
otherBranch, ok := otherBranch.(TreeBranch)
134+
if !ok || len(oneBranch) != len(otherBranch) {
135+
return false
136+
}
137+
for i, item := range oneBranch {
138+
otherItem := otherBranch[i]
139+
if !equals(item.Key, otherItem.Key) || !equals(item.Value, otherItem.Value) {
140+
return false
141+
}
142+
}
143+
return true
144+
case []interface{}:
145+
otherBranch, ok := otherBranch.([]interface{})
146+
if !ok || len(oneBranch) != len(otherBranch) {
147+
return false
148+
}
149+
for i, item := range oneBranch {
150+
if !equals(item, otherBranch[i]) {
151+
return false
152+
}
153+
}
154+
return true
155+
case Comment:
156+
otherBranch, ok := otherBranch.(Comment)
157+
if !ok {
158+
return false
159+
}
160+
return oneBranch.Value == otherBranch.Value
161+
default:
162+
// Unexpected type
163+
return oneBranch == otherBranch
164+
}
165+
}
166+
167+
// Compare a branch with another one
168+
func (branch TreeBranch) Equals(other TreeBranch) bool {
169+
return equals(branch, other)
170+
}
171+
130172
func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} {
131173
switch component := path[0].(type) {
132174
case int:
@@ -156,47 +198,55 @@ func valueFromPathAndLeaf(path []interface{}, leaf interface{}) interface{} {
156198
}
157199
}
158200

159-
func set(branch interface{}, path []interface{}, value interface{}) interface{} {
201+
func set(branch interface{}, path []interface{}, value interface{}) (interface{}, bool) {
160202
switch branch := branch.(type) {
161203
case TreeBranch:
162204
for i, item := range branch {
163205
if item.Key == path[0] {
206+
var changed bool
164207
if len(path) == 1 {
208+
changed = !equals(branch[i].Value, value)
165209
branch[i].Value = value
166210
} else {
167-
branch[i].Value = set(item.Value, path[1:], value)
211+
branch[i].Value, changed = set(item.Value, path[1:], value)
168212
}
169-
return branch
213+
return branch, changed
170214
}
171215
}
172216
// Not found, need to add the next path entry to the branch
173217
value := valueFromPathAndLeaf(path, value)
174218
if newBranch, ok := value.(TreeBranch); ok && len(newBranch) > 0 {
175-
return append(branch, newBranch[0])
219+
return append(branch, newBranch[0]), true
176220
}
177-
return branch
221+
return branch, true
178222
case []interface{}:
179223
position := path[0].(int)
224+
var changed bool
180225
if len(path) == 1 {
181226
if position >= len(branch) {
182-
return append(branch, value)
227+
return append(branch, value), true
183228
}
229+
changed = !equals(branch[position], value)
184230
branch[position] = value
185231
} else {
186232
if position >= len(branch) {
187233
branch = append(branch, valueFromPathAndLeaf(path[1:], value))
234+
changed = true
235+
} else {
236+
branch[position], changed = set(branch[position], path[1:], value)
188237
}
189-
branch[position] = set(branch[position], path[1:], value)
190238
}
191-
return branch
239+
return branch, changed
192240
default:
193-
return valueFromPathAndLeaf(path, value)
241+
newValue := valueFromPathAndLeaf(path, value)
242+
return newValue, !equals(branch, newValue)
194243
}
195244
}
196245

197246
// Set sets a value on a given tree for the specified path
198-
func (branch TreeBranch) Set(path []interface{}, value interface{}) TreeBranch {
199-
return set(branch, path, value).(TreeBranch)
247+
func (branch TreeBranch) Set(path []interface{}, value interface{}) (TreeBranch, bool) {
248+
v, changed := set(branch, path, value)
249+
return v.(TreeBranch), changed
200250
}
201251

202252
func unset(branch interface{}, path []interface{}) (interface{}, error) {

0 commit comments

Comments
 (0)