feat(secrets): Reading from stdin and reproducible stdin #614

Merged
p4u1 merged 3 commits from p4u1/abra:insecrt-secret-from-stdin into main 2025-08-27 15:37:43 +00:00
2 changed files with 85 additions and 26 deletions

View File

@ -2,8 +2,11 @@ package app
import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
@ -157,7 +160,10 @@ environment. Typically, you can let Abra generate them for you on app creation
abra app secret insert 1312.net my_secret v1 mySuperSecret
# insert secret as file
abra app secret insert 1312.net my_secret v1 secret.txt -f`),
abra app secret insert 1312.net my_secret v1 secret.txt -f
# insert secret from stdin
echo "mmySuperSecret" | abra app secret insert 1312.net my_secret v1`),
Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -191,12 +197,9 @@ environment. Typically, you can let Abra generate them for you on app creation
name := args[1]
version := args[2]
data := ""
if len(args) > 3 {
data = args[3]
} else if internal.NoInput {
log.Fatal(i18n.G("must provide <data> argument if --no-input is passed"))
data, err := readSecretData(args)
if err != nil {
log.Fatal(err)
}
decentral1se marked this conversation as resolved
Review

It's a brave new world where we need to wrap all strings with i18n.G(...) so translators can keep up 🤸 You can basically use as a replacement for fmt.Sprintf. It does mean you end up using only log.Fatal and not log.Fatalf mostly. See the rest of the codebase for usage.

(I can sort this out after if you're moving fast and just wanna merge stuff)

It's a brave new world where we need to wrap all strings with `i18n.G(...)` so translators can keep up 🤸 You can basically use as a replacement for `fmt.Sprintf`. It does mean you end up using only `log.Fatal` and not `log.Fatalf` mostly. See the rest of the codebase for usage. (I can sort this out after if you're moving fast and just wanna merge stuff)
Review

Ah, forgot about that, will update it tomorrow.
Btw. would it be possible to override the log.Fatal functions, so we can't forget the i18n.G call

Ah, forgot about that, will update it tomorrow. Btw. would it be possible to override the log.Fatal functions, so we can't forget the i18n.G call
Review

Cool, sounds good. Yeh, that would be pretty amazing to wrap the logging functions 🤔 It'd probably be possible to wrap the logging functions and thread a string via i18n.G alright: https://git.coopcloud.tech/toolshed/abra/src/branch/main/pkg/log/log.go Maybe that's one for an additional PR which runs a mass sed -i ... on the entire codebase 🤣

Cool, sounds good. Yeh, that would be pretty amazing to wrap the logging functions 🤔 It'd probably be possible to wrap the logging functions and thread a string via `i18n.G` alright: https://git.coopcloud.tech/toolshed/abra/src/branch/main/pkg/log/log.go Maybe that's one for an additional PR which runs a mass `sed -i ...` on the entire codebase 🤣
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
@ -219,23 +222,6 @@ environment. Typically, you can let Abra generate them for you on app creation
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
}
if data == "" && !internal.NoInput {
log.Debug(i18n.G("secret data not provided on command-line, prompting"))
var prompt survey.Prompt
if !insertFromFile {
prompt = &survey.Password{
Message: i18n.G("specify secret value"),
}
} else {
prompt = &survey.Input{
Message: i18n.G("specify secret file"),
}
}
if err := survey.AskOne(prompt, &data); err != nil {
log.Fatal(err)
}
}
if insertFromFile {
raw, err := os.ReadFile(data)
if err != nil {
@ -263,6 +249,55 @@ environment. Typically, you can let Abra generate them for you on app creation
},
}
func readSecretData(args []string) (string, error) {
if len(args) == 4 {
return args[3], nil
}
if len(args) != 3 {
return "", errors.New(i18n.G("need 3 or 4 arguments"))
}
// First check if data is provided by stdin
fi, err := os.Stdin.Stat()
if err != nil {
return "", err
}
if fi.Mode()&os.ModeNamedPipe != 0 {
// Can't insert from stdin and read from file
if insertFromFile {
return "", errors.New(i18n.G("can not insert from file and read from stdin"))
}
log.Debug(i18n.G("reading secret data from stdin"))
bytes, err := io.ReadAll(os.Stdin)
if err != nil {
return "", errors.New(i18n.G("reading data from stdin: %s", err))
}
return string(bytes), nil
}
if internal.NoInput {
return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
}
log.Debug(i18n.G("secret data not provided on command-line or stdin, prompting"))
var prompt survey.Prompt
if !insertFromFile {
prompt = &survey.Password{
Message: i18n.G("specify secret value"),
}
} else {
prompt = &survey.Input{
Message: i18n.G("specify secret file"),
}
}
var data string
if err := survey.AskOne(prompt, &data); err != nil {
return "", err
}
return data, nil
}
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
@ -432,6 +467,10 @@ var AppSecretLsCommand = &cobra.Command{
log.Fatal(err)
}
// Sort secrets to ensure reproducible output
sort.Slice(secStats, func(i, j int) bool {
return secStats[i].LocalName < secStats[j].LocalName
})
var rows [][]string
for _, secStat := range secStats {
row := []string{

View File

@ -206,10 +206,10 @@ teardown(){
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar
assert_failure
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz
run $ABRA app secret insert "$TEST_APP_DOMAIN" bar baz --no-input
assert_failure
run $ABRA app secret insert "$TEST_APP_DOMAIN" test_pass_one v1 -n
run bash -c "echo foo | $ABRA app secret insert $TEST_APP_DOMAIN bar baz -f"
assert_failure
}
@ -251,6 +251,20 @@ teardown(){
assert_output --partial 'true'
}
@test "insert: create secret from stdin" {
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'false'
run bash -c "echo foo | $ABRA app secret insert $TEST_APP_DOMAIN test_pass_one v1"
assert_success
assert_output --partial 'successfully stored on server'
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'true'
}
@test "rm: validate arguments" {
run $ABRA app secret rm
assert_failure
@ -343,6 +357,12 @@ teardown(){
| jq -r ".[] | select(.name==\"test_pass_two\") | .version"'
assert_success
assert_output --partial 'v1'
# Can always expect the secret at this position
run bash -c '$ABRA app secret ls "$TEST_APP_DOMAIN" --machine \
| jq -r ".[1] | .name"'
assert_success
assert_output --partial 'test_pass_two'
}
@test "ls: bail if unstaged changes and no --chaos" {