diff --git a/README.rst b/README.rst index 26e30d6dd..a3bfe1610 100644 --- a/README.rst +++ b/README.rst @@ -1560,6 +1560,17 @@ The value must be formatted as json. $ sops set ~/git/svc/sops/example.yaml '["an_array"][1]' '{"uid1":null,"uid2":1000,"uid3":["bob"]}' +You can also provide the value from a file or stdin: + +.. code:: sh + + # Provide the value from a file + $ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' > /tmp/example-value + $ sops set ~/git/svc/sops/example.yaml --value-file '["an_array"][1]' /tmp/example-value + + # Provide the value from stdin + $ echo '{"uid1":null,"uid2":1000,"uid3":["bob"]}' | sops set ~/git/svc/sops/example.yaml --value-stdin '["an_array"][1]' + Unset a sub-part in a document tree ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/cmd/sops/main.go b/cmd/sops/main.go index f40b181ee..ceb1977da 100644 --- a/cmd/sops/main.go +++ b/cmd/sops/main.go @@ -4,6 +4,7 @@ import ( "context" encodingjson "encoding/json" "fmt" + "io" "net" "net/url" "os" @@ -1374,8 +1375,8 @@ func main() { }, { Name: "set", - Usage: `set a specific key or branch in the input document. value must be a json encoded string. eg. '/path/to/file ["somekey"][0] {"somevalue":true}'`, - ArgsUsage: `file index value`, + Usage: `set a specific key or branch in the input document. value must be a JSON encoded string, for example '/path/to/file ["somekey"][0] {"somevalue":true}', or a path if --value-file is used, or omitted if --value-stdin is used`, + ArgsUsage: `file index [ value ]`, Flags: append([]cli.Flag{ cli.StringFlag{ Name: "input-type", @@ -1387,7 +1388,11 @@ func main() { }, cli.BoolFlag{ Name: "value-file", - Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings)", + Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-stdin", + }, + cli.BoolFlag{ + Name: "value-stdin", + Usage: "treat 'value' as a file to read the actual value from (avoids leaking secrets in process listings). Mutually exclusive with --value-file", }, cli.IntFlag{ Name: "shamir-secret-sharing-threshold", @@ -1411,8 +1416,17 @@ func main() { if c.Bool("verbose") { logging.SetLevel(logrus.DebugLevel) } - if c.NArg() != 3 { - return common.NewExitError("Error: no file specified, or index and value are missing", codes.NoFileSpecified) + if c.Bool("value-file") && c.Bool("value-stdin") { + return common.NewExitError("Error: cannot use both --value-file and --value-stdin", codes.ErrorGeneric) + } + if c.Bool("value-stdin") { + if c.NArg() != 2 { + return common.NewExitError("Error: file specified, or index and value are missing. Need precisely 2 positional arguments since --value-stdin is used.", codes.NoFileSpecified) + } + } else { + if c.NArg() != 3 { + return common.NewExitError("Error: no file specified, or index and value are missing. Need precisely 3 positional arguments.", codes.NoFileSpecified) + } } fileName, err := filepath.Abs(c.Args()[0]) if err != nil { @@ -1435,7 +1449,13 @@ func main() { } var data string - if c.Bool("value-file") { + if c.Bool("value-stdin") { + content, err := io.ReadAll(os.Stdin) + if err != nil { + return toExitError(err) + } + data = string(content) + } else if c.Bool("value-file") { filename := c.Args()[2] content, err := os.ReadFile(filename) if err != nil { diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index 6ed0289cb..08eccea9c 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -538,6 +538,57 @@ bar: baz", panic!("Output JSON does not have the expected structure"); } + #[test] + fn set_json_file_insert_with_value_stdin() { + let file_path = prepare_temp_file( + "test_set_json_file_insert_with_value_stdin.json", + r#"{"a": 2, "b": "ba"}"#.as_bytes(), + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + // let value_file = prepare_temp_file("insert_value_file.json", r#"{"cc": "ccc"}"#.as_bytes()); + let process = Command::new(SOPS_BINARY_PATH) + .arg("set") + .arg("--value-stdin") + .arg(file_path.clone()) + .arg(r#"["c"]"#) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("Error running sops"); + write_to_stdin(&process, b"{\"cc\": \"ccc\"}"); + let output = process.wait_with_output().expect("Failed to wait on sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let mut s = String::new(); + File::open(file_path) + .unwrap() + .read_to_string(&mut s) + .unwrap(); + let data: Value = serde_json::from_str(&s).expect("Error parsing sops's JSON output"); + if let Value::Mapping(data) = data { + let a = data.get(&Value::String("c".to_owned())).unwrap(); + if let &Value::Mapping(ref a) = a { + assert_encrypted!(&a, Value::String("cc".to_owned())); + return; + } + } + panic!("Output JSON does not have the expected structure"); + } + #[test] fn set_yaml_file_update() { let file_path = prepare_temp_file(