mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
Adds `coder secret import <file>` to bulk-import dotenv, JSON, or YAML secrets through the existing batch API. The command infers the format from the extension or accepts `--input-format`, supports non-interactive stdin, validates files locally before upload, and warns when imported keys cannot be injected as environment variables. Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
907 lines
27 KiB
Go
907 lines
27 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
"github.com/coder/coder/v2/testutil/expecter"
|
|
)
|
|
|
|
func TestSecretCreate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("MissingValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "create", "api-key")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "secret value must be provided by exactly one of --value or non-interactive stdin (pipe or redirect)")
|
|
})
|
|
|
|
t.Run("MissingValueOnTTY", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "--force-tty", "secret", "create", "api-key")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "secret value must be provided with --value or stdin via pipe or redirect")
|
|
})
|
|
|
|
t.Run("SuccessWithValueFlag", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"create",
|
|
"api-key",
|
|
"--value", "super-secret-value",
|
|
"--description", "API key for workspace tools",
|
|
"--env", "API_KEY",
|
|
"--file", "~/.api-key",
|
|
)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "api-key")
|
|
|
|
secret, err := client.UserSecretByName(ctx, codersdk.Me, "api-key")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "api-key", secret.Name)
|
|
require.Equal(t, "API key for workspace tools", secret.Description)
|
|
require.Equal(t, "API_KEY", secret.EnvName)
|
|
require.Equal(t, "~/.api-key", secret.FilePath)
|
|
})
|
|
|
|
t.Run("ValueFlagConflictsWithStdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"create",
|
|
"api-key",
|
|
"--value", "super-secret-value",
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("different-value")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "secret value may be provided by only one source, got --value, stdin")
|
|
})
|
|
|
|
t.Run("SuccessWithStdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"create",
|
|
"api-key",
|
|
"--description", "API key for workspace tools",
|
|
"--env", "API_KEY",
|
|
)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("super-secret-value")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "api-key")
|
|
|
|
secret, err := client.UserSecretByName(ctx, codersdk.Me, "api-key")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "api-key", secret.Name)
|
|
require.Equal(t, "API key for workspace tools", secret.Description)
|
|
require.Equal(t, "API_KEY", secret.EnvName)
|
|
})
|
|
|
|
t.Run("StdinTrailingNewlineWarnsAndPreservesValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
|
firstUser := coderdtest.CreateFirstUser(t, ownerClient)
|
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, firstUser.OrganizationID)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"create",
|
|
"api-key",
|
|
"--description", "API key for workspace tools",
|
|
"--env", "API_KEY",
|
|
)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("super-secret-value\n")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "api-key")
|
|
require.Contains(t, output.Stderr(), "secret value from stdin ends with a trailing newline")
|
|
|
|
secret, err := db.GetUserSecretByUserIDAndName(
|
|
dbauthz.AsSystemRestricted(ctx),
|
|
database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: user.ID,
|
|
Name: "api-key",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "super-secret-value\n", secret.Value)
|
|
})
|
|
|
|
t.Run("EmptyStdinIsNotProvided", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "create", "api-key")
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "secret value must be provided by exactly one of --value or non-interactive stdin (pipe or redirect)")
|
|
})
|
|
}
|
|
|
|
func TestSecretUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ServerValidationError", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
EnvName: "MY_SECRET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "update", "my-secret")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "At least one field must be provided")
|
|
})
|
|
|
|
t.Run("AllowsClearingFields", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
Description: "original description",
|
|
EnvName: "MY_SECRET",
|
|
FilePath: "~/.my-secret",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Clearing env_name and description while leaving file_path
|
|
// keeps the secret well-formed (still has an injection
|
|
// target). Trying to clear both env_name and file_path is
|
|
// covered by the server-side test below.
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"update",
|
|
"my-secret",
|
|
"--value", "rotated-secret",
|
|
"--description", "",
|
|
"--env", "",
|
|
)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "my-secret")
|
|
|
|
secret, err := client.UserSecretByName(ctx, codersdk.Me, "my-secret")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", secret.Description)
|
|
require.Equal(t, "", secret.EnvName)
|
|
require.Equal(t, "~/.my-secret", secret.FilePath)
|
|
})
|
|
|
|
t.Run("ClearingBothTargetsRejected", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
EnvName: "MY_SECRET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"update",
|
|
"my-secret",
|
|
"--env", "",
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Len(t, sdkErr.Validations, 1)
|
|
require.Equal(t, "env_name", sdkErr.Validations[0].Field)
|
|
require.Contains(t, sdkErr.Validations[0].Detail, "at least one of env_name or file_path")
|
|
})
|
|
|
|
t.Run("UpdatesValueFromEmptyFlag", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
EnvName: "MY_SECRET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"update",
|
|
"my-secret",
|
|
"--value", "",
|
|
)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "my-secret")
|
|
})
|
|
|
|
t.Run("UpdatesValueFromStdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
EnvName: "MY_SECRET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "update", "my-secret")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("rotated-secret")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "my-secret")
|
|
})
|
|
|
|
t.Run("ValueFlagConflictsWithStdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "my-secret",
|
|
Value: "original-value",
|
|
EnvName: "MY_SECRET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(
|
|
t,
|
|
"secret",
|
|
"update",
|
|
"my-secret",
|
|
"--value", "rotated-secret",
|
|
)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("different-value")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "secret value may be provided by only one source, got --value, stdin")
|
|
})
|
|
}
|
|
|
|
func TestSecretList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("TableOutput", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "tool-config",
|
|
Value: "config-value",
|
|
Description: "Tool configuration",
|
|
FilePath: "~/.config/tool/config.json",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
Description: "Service access token",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "list")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
out := output.Stdout()
|
|
assert.Contains(t, out, "NAME")
|
|
assert.Contains(t, out, "CREATED")
|
|
assert.Contains(t, out, "UPDATED")
|
|
assert.Contains(t, out, "ENV")
|
|
assert.Contains(t, out, "FILE")
|
|
assert.Contains(t, out, "DESCRIPTION")
|
|
assert.Contains(t, out, "service-token")
|
|
assert.Contains(t, out, "SERVICE_TOKEN")
|
|
assert.Contains(t, out, "tool-config")
|
|
assert.Contains(t, out, "~/.config/tool/config.json")
|
|
})
|
|
|
|
t.Run("JSONOutput", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
Description: "Service access token",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "list", "--output=json")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var got []codersdk.UserSecret
|
|
require.NoError(t, json.Unmarshal([]byte(output.Stdout()), &got))
|
|
require.Len(t, got, 1)
|
|
require.Equal(t, created, got[0])
|
|
})
|
|
|
|
t.Run("SingleSecretTableOutput", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "tool-config",
|
|
Value: "config-value",
|
|
Description: "Tool configuration",
|
|
FilePath: "~/.config/tool/config.json",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
Description: "Service access token",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "list", "service-token")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
out := output.Stdout()
|
|
assert.Contains(t, out, "NAME")
|
|
assert.Contains(t, out, "CREATED")
|
|
assert.Contains(t, out, "UPDATED")
|
|
assert.Contains(t, out, "ENV")
|
|
assert.Contains(t, out, "FILE")
|
|
assert.Contains(t, out, "DESCRIPTION")
|
|
assert.Contains(t, out, "service-token")
|
|
assert.Contains(t, out, "SERVICE_TOKEN")
|
|
assert.NotContains(t, out, "tool-config")
|
|
assert.NotContains(t, out, "~/.config/tool/config.json")
|
|
})
|
|
|
|
t.Run("SingleSecretJSONOutput", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
Description: "Service access token",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "list", "service-token", "--output=json")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var got []codersdk.UserSecret
|
|
require.NoError(t, json.Unmarshal([]byte(output.Stdout()), &got))
|
|
require.Len(t, got, 1)
|
|
require.Equal(t, created, got[0])
|
|
})
|
|
|
|
t.Run("EmptyState", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "list")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output.Stderr(), "No secrets found.")
|
|
})
|
|
}
|
|
|
|
func TestSecretDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "delete", "service-token")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
inv = inv.WithContext(ctx)
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
|
|
waiter := clitest.StartWithWaiter(t, inv)
|
|
stdout.ExpectMatch(ctx, "Delete secret")
|
|
stdout.ExpectMatch(ctx, "service-token")
|
|
stdin.WriteLine("yes")
|
|
stdout.ExpectMatch(ctx, "Deleted secret")
|
|
|
|
require.NoError(t, waiter.Wait())
|
|
|
|
_, err = client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("YesSkipsPrompt", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
_, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inv, root := clitest.New(t, "secret", "delete", "service-token", "--yes")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "Deleted secret")
|
|
require.NotContains(t, output.Stdout(), "Delete secret")
|
|
require.Empty(t, output.Stderr())
|
|
|
|
_, err = client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "delete", "missing-secret")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
inv = inv.WithContext(ctx)
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
|
|
waiter := clitest.StartWithWaiter(t, inv)
|
|
stdout.ExpectMatch(ctx, "Delete secret")
|
|
stdout.ExpectMatch(ctx, "missing-secret")
|
|
stdin.WriteLine("yes")
|
|
|
|
err := waiter.Wait()
|
|
require.ErrorContains(t, err, `delete secret "missing-secret"`)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
}
|
|
|
|
func TestSecretImport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
writeSecretsFile := func(t *testing.T, name, content string) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), name)
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
|
return path
|
|
}
|
|
|
|
t.Run("InfersFormatFromExtension", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
file string
|
|
content string
|
|
}{
|
|
{name: "Env", file: "secrets.env", content: "ALPHA=a\nBETA=b\n"},
|
|
{name: "JSON", file: "secrets.json", content: `{"ALPHA":"a","BETA":"b"}`},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "import", writeSecretsFile(t, tt.file, tt.content))
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
require.NoError(t, inv.WithContext(ctx).Run())
|
|
require.Contains(t, output.Stdout(), "Imported 2 secrets.")
|
|
require.NotContains(t, output.Stderr(), "without an environment variable name")
|
|
|
|
secret, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "ALPHA", secret.EnvName)
|
|
})
|
|
}
|
|
})
|
|
|
|
// The flag wins over the extension, and its value is matched
|
|
// case-insensitively.
|
|
t.Run("InputFormatOverridesExtension", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
path := writeSecretsFile(t, "secrets.json", "ALPHA=a\n")
|
|
inv, root := clitest.New(t, "secret", "import", path, "--input-format", "ENV")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
require.NoError(t, inv.WithContext(ctx).Run())
|
|
require.Contains(t, output.Stdout(), "Imported 1 secret.")
|
|
|
|
_, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Stdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "import", "-", "--input-format", "env")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
inv.Stdin = strings.NewReader("ALPHA=a\n")
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
require.NoError(t, inv.WithContext(ctx).Run())
|
|
require.Contains(t, output.Stdout(), "Imported 1 secret.")
|
|
|
|
_, err := client.UserSecretByName(ctx, codersdk.Me, "ALPHA")
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("UnknownExtension", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
path := writeSecretsFile(t, "secrets.txt", "ALPHA=a\n")
|
|
inv, root := clitest.New(t, "secret", "import", path)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.ErrorContains(t, err, "set --input-format to one of: env, json, yaml")
|
|
})
|
|
|
|
t.Run("RejectsLocalErrorsBeforeSending", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
content []byte
|
|
wantErr string
|
|
}{
|
|
{name: "Malformed", content: []byte("ALPHA=a\nNOEQUALS\n"), wantErr: "line 2: expected KEY=VALUE"},
|
|
{name: "InvalidUTF8", content: []byte{'A', 'L', 'P', 'H', 'A', '=', 0xff}, wantErr: "must contain valid UTF-8"},
|
|
{name: "InvalidEntry", content: []byte("ALPHA=a\nBETA=\n"), wantErr: `secret 2 ("BETA") value: Value is required.`},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var requests atomic.Int64
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests.Add(1)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := codersdk.New(must(url.Parse(server.URL)))
|
|
client.SetSessionToken("test-token")
|
|
path := filepath.Join(t.TempDir(), "secrets.env")
|
|
require.NoError(t, os.WriteFile(path, tt.content, 0o600))
|
|
inv, root := clitest.New(t, "secret", "import", path)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
err := inv.Run()
|
|
require.ErrorContains(t, err, tt.wantErr)
|
|
require.Zero(t, requests.Load(), "expected no API request")
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("RejectsTTYStdin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var requests atomic.Int64
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests.Add(1)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := codersdk.New(must(url.Parse(server.URL)))
|
|
client.SetSessionToken("test-token")
|
|
inv, root := clitest.New(t, "--force-tty", "secret", "import", "-", "--input-format", "env")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
err := inv.Run()
|
|
require.ErrorContains(t, err, "secrets file must be provided via non-interactive stdin (pipe or redirect)")
|
|
require.Zero(t, requests.Load(), "expected no API request")
|
|
})
|
|
|
|
t.Run("WarnsAboutKeysWithoutEnvNames", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
// PATH is reserved and MY-TOKEN is not a valid identifier, so both are
|
|
// imported without an env name.
|
|
require.Error(t, codersdk.UserSecretEnvNameValid("PATH"))
|
|
path := writeSecretsFile(t, "secrets.env", "ALPHA=a\nPATH=b\nMY-TOKEN=c\n")
|
|
inv, root := clitest.New(t, "secret", "import", path)
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
require.NoError(t, inv.WithContext(ctx).Run())
|
|
require.Contains(t, output.Stdout(), "Imported 3 secrets.")
|
|
require.Contains(t, output.Stderr(), `2 secrets imported without an environment variable name: "PATH", "MY-TOKEN"`)
|
|
|
|
secret, err := client.UserSecretByName(ctx, codersdk.Me, "PATH")
|
|
require.NoError(t, err)
|
|
require.Empty(t, secret.EnvName)
|
|
})
|
|
}
|
|
|
|
func TestSecretEnableDisable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Disable", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
EnvName: "SERVICE_TOKEN",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, created.Enabled)
|
|
|
|
inv, root := clitest.New(t, "secret", "disable", "service-token")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "Disabled secret")
|
|
require.Contains(t, output.Stdout(), "service-token")
|
|
|
|
got, err := client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
|
|
require.NoError(t, err)
|
|
assert.False(t, got.Enabled)
|
|
})
|
|
|
|
t.Run("Enable", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
setupCtx := testutil.Context(t, testutil.WaitMedium)
|
|
disabled := false
|
|
created, err := client.CreateUserSecret(setupCtx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: "service-token",
|
|
Value: "service-token-value",
|
|
EnvName: "SERVICE_TOKEN",
|
|
Enabled: &disabled,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, created.Enabled)
|
|
|
|
inv, root := clitest.New(t, "secret", "enable", "service-token")
|
|
output := clitest.Capture(inv)
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
require.Contains(t, output.Stdout(), "Enabled secret")
|
|
require.Contains(t, output.Stdout(), "service-token")
|
|
|
|
got, err := client.UserSecretByName(setupCtx, codersdk.Me, "service-token")
|
|
require.NoError(t, err)
|
|
assert.True(t, got.Enabled)
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, nil)
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
|
|
inv, root := clitest.New(t, "secret", "disable", "missing-secret")
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
err := inv.WithContext(ctx).Run()
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
}
|