Compare commits
2
Commits
09335adcb5
...
81ffe12c84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ffe12c84 | ||
|
|
fe94e99801 |
No files matched your search
@@ -142,7 +142,8 @@ contain `//`, or have `..` as a path segment.
|
|||||||
|
|
||||||
#### `secret get <secret-name> [--version <version>]`
|
#### `secret get <secret-name> [--version <version>]`
|
||||||
|
|
||||||
Retrieves and outputs a secret value to stdout.
|
Retrieves and outputs a secret value to stdout. If the value cannot be written,
|
||||||
|
as when stdout is a file on a full disk, it prints the error and exits 1.
|
||||||
|
|
||||||
- `--version, -v`: Get a specific version (default: current)
|
- `--version, -v`: Get a specific version (default: current)
|
||||||
|
|
||||||
@@ -197,7 +198,8 @@ for confirmation, naming the version, the secret and its vault (see
|
|||||||
|
|
||||||
#### `secret generate mnemonic`
|
#### `secret generate mnemonic`
|
||||||
|
|
||||||
Generates a cryptographically secure BIP39 mnemonic phrase.
|
Generates a cryptographically secure BIP39 mnemonic phrase and prints it to
|
||||||
|
stdout. The warning to keep it safe goes to stderr.
|
||||||
|
|
||||||
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
|
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-14: `secret get` fails when it cannot write the value to stdout
|
||||||
|
(https://git.eeqj.de/sneak/secret/issues/155). The write's error was ignored,
|
||||||
|
so a script that sent the value to a file on a full disk saw `secret get`
|
||||||
|
succeed and was left with an empty or cut-short file. That error is now the
|
||||||
|
command's error, and `secret get` exits 1. A stdout the shell closed (`>&-`)
|
||||||
|
still gives exit 0, because the Go runtime opens `/dev/null` in its place
|
||||||
|
before `secret` starts and the write there succeeds.
|
||||||
|
|
||||||
|
- 2026-10-14: `secret generate mnemonic`, `secret vault list` and
|
||||||
|
`secret unlocker list` print their output to stdout, as `secret list` does, so
|
||||||
|
it can be piped or captured (https://git.eeqj.de/sneak/secret/issues/138).
|
||||||
|
They printed with cobra's `cmd.Println` and `cmd.Printf`, which write to
|
||||||
|
stderr when no output writer is set, so `secret vault list --json | jq`
|
||||||
|
received nothing. "No versions found" from `secret version list` moved to
|
||||||
|
stdout too. Messages such as "Created vault", and the warning after the
|
||||||
|
mnemonic, stay on stderr.
|
||||||
- 2026-10-14: Removing the current unlocker no longer leaves the vault unable to
|
- 2026-10-14: Removing the current unlocker no longer leaves the vault unable to
|
||||||
open through the unlockers it still has
|
open through the unlockers it still has
|
||||||
(https://git.eeqj.de/sneak/secret/issues/137). `current-unlocker` went on
|
(https://git.eeqj.de/sneak/secret/issues/137). `current-unlocker` went on
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output mnemonic to stdout
|
// Output mnemonic to stdout
|
||||||
cmd.Println(mnemonic)
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), mnemonic)
|
||||||
|
|
||||||
// Output helpful information to stderr
|
// Output helpful information to stderr
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
|||||||
@@ -443,7 +443,10 @@ func (cli *Instance) GetSecret(cmd *cobra.Command, secretName string) error {
|
|||||||
defer value.Destroy()
|
defer value.Destroy()
|
||||||
|
|
||||||
// Write the value straight from locked memory, with no trailing newline
|
// Write the value straight from locked memory, with no trailing newline
|
||||||
_, _ = cmd.OutOrStdout().Write(value.Bytes())
|
_, err = cmd.OutOrStdout().Write(value.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write secret value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -478,7 +481,10 @@ func (cli *Instance) GetSecretWithVersion(
|
|||||||
secret.Debug("Got secret value", "valueLength", value.Size())
|
secret.Debug("Got secret value", "valueLength", value.Size())
|
||||||
|
|
||||||
// Write the value straight from locked memory, with no trailing newline
|
// Write the value straight from locked memory, with no trailing newline
|
||||||
_, _ = cmd.OutOrStdout().Write(value.Bytes())
|
_, err = cmd.OutOrStdout().Write(value.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write secret value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
secret.Debug("Printed value to stdout")
|
secret.Debug("Printed value to stdout")
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/secret/internal/bip39"
|
||||||
"sneak.berlin/go/secret/internal/secret"
|
"sneak.berlin/go/secret/internal/secret"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,3 +92,66 @@ func TestGetCommandOutputsToStdout(t *testing.T) {
|
|||||||
// Nothing should be in stderr
|
// Nothing should be in stderr
|
||||||
assert.Empty(t, stderr.String(), "stderr should be empty")
|
assert.Empty(t, stderr.String(), "stderr should be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMnemonicAndListingsOutputToStdout tests that 'secret generate
|
||||||
|
// mnemonic', 'secret vault list' and 'secret unlocker list' print their
|
||||||
|
// output to stdout, so that it can be piped
|
||||||
|
func TestMnemonicAndListingsOutputToStdout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
secretPath := secretBinaryPath(t)
|
||||||
|
|
||||||
|
// run runs the secret binary and returns what it wrote to stdout and to
|
||||||
|
// stderr
|
||||||
|
run := func(args ...string) (string, string) {
|
||||||
|
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||||
|
cmd := exec.CommandContext(t.Context(), secretPath, args...)
|
||||||
|
cmd.Env = []string{
|
||||||
|
secret.EnvStateDir + "=" + tempDir,
|
||||||
|
secret.EnvMnemonic + "=" + testMnemonic,
|
||||||
|
secret.EnvUnlockPassphrase + "=test-passphrase",
|
||||||
|
"PATH=/usr/bin:/bin",
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
require.NoError(t, err, "%v should succeed: %s", args, stderr.String())
|
||||||
|
|
||||||
|
return stdout.String(), stderr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, stderr := run("generate", "mnemonic")
|
||||||
|
assert.True(t, bip39.IsMnemonicValid(strings.TrimSpace(stdout)),
|
||||||
|
"stdout should hold the mnemonic, not %q", stdout)
|
||||||
|
assert.Contains(t, stderr, "Save this mnemonic phrase securely",
|
||||||
|
"the warning should be on stderr")
|
||||||
|
|
||||||
|
run("init")
|
||||||
|
|
||||||
|
stdout, _ = run("vault", "list", "--json")
|
||||||
|
|
||||||
|
var vaults map[string]any
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(stdout), &vaults),
|
||||||
|
"stdout should hold the vaults as JSON, not %q", stdout)
|
||||||
|
assert.Equal(t, "default", vaults["currentVault"])
|
||||||
|
|
||||||
|
stdout, _ = run("unlocker", "list", "--json")
|
||||||
|
|
||||||
|
var unlockers map[string]any
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(stdout), &unlockers),
|
||||||
|
"stdout should hold the unlockers as JSON, not %q", stdout)
|
||||||
|
assert.Len(t, unlockers["unlockers"], 1)
|
||||||
|
|
||||||
|
stdout, _ = run("vault", "list")
|
||||||
|
assert.Contains(t, stdout, "default (current)")
|
||||||
|
|
||||||
|
stdout, _ = run("unlocker", "list")
|
||||||
|
assert.Contains(t, stdout, "Total: 1 unlocker(s)")
|
||||||
|
}
|
||||||
@@ -326,22 +326,25 @@ func (cli *Instance) printUnlockersJSON(
|
|||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Println(string(jsonBytes))
|
_, _ = fmt.Fprintln(cli.cmd.OutOrStdout(), string(jsonBytes))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// printUnlockersTable prints unlockers in a formatted table
|
// printUnlockersTable prints unlockers in a formatted table
|
||||||
func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
||||||
|
out := cli.cmd.OutOrStdout()
|
||||||
|
|
||||||
if len(unlockers) == 0 {
|
if len(unlockers) == 0 {
|
||||||
cli.cmd.Println("No unlockers found in current vault.")
|
_, _ = fmt.Fprintln(out, "No unlockers found in current vault.")
|
||||||
cli.cmd.Println("Run 'secret unlocker add passphrase' to create one.")
|
_, _ = fmt.Fprintln(out, "Run 'secret unlocker add passphrase' to create one.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
_, _ = fmt.Fprintf(out, " %-40s %-12s %-20s %s\n",
|
||||||
cli.cmd.Printf(" %-40s %-12s %-20s %s\n",
|
"UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
||||||
|
_, _ = fmt.Fprintf(out, " %-40s %-12s %-20s %s\n",
|
||||||
strings.Repeat("-", unlockerIDWidth), strings.Repeat("-", unlockerTypeWidth),
|
strings.Repeat("-", unlockerIDWidth), strings.Repeat("-", unlockerTypeWidth),
|
||||||
strings.Repeat("-", unlockerDateWidth), strings.Repeat("-", unlockerFlagsWidth))
|
strings.Repeat("-", unlockerDateWidth), strings.Repeat("-", unlockerFlagsWidth))
|
||||||
|
|
||||||
@@ -356,7 +359,7 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
|||||||
prefix = "* "
|
prefix = "* "
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf("%s%-40s %-12s %-20s %s\n",
|
_, _ = fmt.Fprintf(out, "%s%-40s %-12s %-20s %s\n",
|
||||||
prefix,
|
prefix,
|
||||||
unlocker.ID,
|
unlocker.ID,
|
||||||
unlocker.Type,
|
unlocker.Type,
|
||||||
@@ -364,7 +367,7 @@ func (cli *Instance) printUnlockersTable(unlockers []UnlockerInfo) error {
|
|||||||
flags)
|
flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.cmd.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
|
_, _ = fmt.Fprintf(out, "\nTotal: %d unlocker(s)\n", len(unlockers))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := cmd.OutOrStdout()
|
||||||
|
|
||||||
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
||||||
// Get current vault name for context
|
// Get current vault name for context
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
@@ -205,13 +207,13 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println(string(jsonBytes))
|
_, _ = fmt.Fprintln(out, string(jsonBytes))
|
||||||
} else {
|
} else {
|
||||||
// Text output
|
// Text output
|
||||||
cmd.Println("Available vaults:")
|
_, _ = fmt.Fprintln(out, "Available vaults:")
|
||||||
|
|
||||||
if len(vaults) == 0 {
|
if len(vaults) == 0 {
|
||||||
cmd.Println(" (none)")
|
_, _ = fmt.Fprintln(out, " (none)")
|
||||||
} else {
|
} else {
|
||||||
// Try to get current vault for marking
|
// Try to get current vault for marking
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
@@ -223,9 +225,9 @@ func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|||||||
|
|
||||||
for _, vaultName := range vaults {
|
for _, vaultName := range vaults {
|
||||||
if vaultName == currentVault {
|
if vaultName == currentVault {
|
||||||
cmd.Printf(" %s (current)\n", vaultName)
|
_, _ = fmt.Fprintf(out, " %s (current)\n", vaultName)
|
||||||
} else {
|
} else {
|
||||||
cmd.Printf(" %s\n", vaultName)
|
_, _ = fmt.Fprintf(out, " %s\n", vaultName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (cli *Instance) ListVersions(cmd *cobra.Command, secretName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
cmd.Println("No versions found")
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No versions found")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
|
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
|
||||||
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
|
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
|
||||||
// - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values
|
// - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values
|
||||||
|
// - TestGetSecretReturnsWriteError: Tests `secret get` when stdout fails
|
||||||
// - TestVersionCommandStructure: Tests command structure and help text
|
// - TestVersionCommandStructure: Tests command structure and help text
|
||||||
// - TestListVersionsEmptyOutput: Tests edge case with no versions
|
// - TestListVersionsEmptyOutput: Tests edge case with no versions
|
||||||
//
|
//
|
||||||
@@ -20,6 +21,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -360,6 +362,45 @@ func TestGetSecretWritesBinaryValue(t *testing.T) {
|
|||||||
assert.Equal(t, value, buf.Bytes())
|
assert.Equal(t, value, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetSecretReturnsWriteError checks that `secret get`, with and without
|
||||||
|
// a version, returns the error of a failed write of the value.
|
||||||
|
func TestGetSecretReturnsWriteError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
||||||
|
cli.Mnemonic = testMnemonicBuffer(t)
|
||||||
|
|
||||||
|
setupTestVault(t, fs)
|
||||||
|
|
||||||
|
vlt, err := vault.GetCurrentVault(fs, testStateDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
addTestSecret(t, vlt, []byte("value"), false)
|
||||||
|
|
||||||
|
vaultDir, err := vlt.GetDirectory()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
versions, err := secret.ListVersions(fs,
|
||||||
|
filepath.Join(vaultDir, "secrets.d", "test%secret"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, versions, 1)
|
||||||
|
|
||||||
|
// Stdout is a file closed before the value is written to it
|
||||||
|
out, err := os.Create(filepath.Join(t.TempDir(), "stdout"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, out.Close())
|
||||||
|
|
||||||
|
cmd := newRootCmd()
|
||||||
|
cmd.SetOut(out)
|
||||||
|
|
||||||
|
err = cli.GetSecret(cmd, "test/secret")
|
||||||
|
require.ErrorIs(t, err, os.ErrClosed)
|
||||||
|
|
||||||
|
err = cli.GetSecretWithVersion(cmd, "test/secret", versions[0])
|
||||||
|
require.ErrorIs(t, err, os.ErrClosed)
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:paralleltest // reads process environment to determine the state dir
|
//nolint:paralleltest // reads process environment to determine the state dir
|
||||||
func TestVersionCommandStructure(t *testing.T) {
|
func TestVersionCommandStructure(t *testing.T) {
|
||||||
// Test that version commands are properly structured
|
// Test that version commands are properly structured
|
||||||
|
|||||||
Reference in new issue
Block a user