feat(secrets): Reading from stdin and reproducible stdin #614
@ -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
|
||||
|
||||
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{
|
||||
|
@ -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" {
|
||||
|
Reference in New Issue
Block a user
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 forfmt.Sprintf
. It does mean you end up using onlylog.Fatal
and notlog.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)
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
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 masssed -i ...
on the entire codebase 🤣