Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ builds:
- -trimpath
env:
- CGO_ENABLED=0
- GOEXPERIMENT=greenteagc
ldflags:
- -s -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} -X main.GitCommit={{.ShortCommit}}
goos:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ vet: ## Examine source code
.PHONY: vet

build: export CGO_ENABLED=0
build: export GOEXPERIMENT=greenteagc
build: ## Build the application
@ go build \
-ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)" \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ profiles:
### Build from Source

Prerequisites:
- Go 1.21 or later
- Go 1.25.3 or later

Steps:
```bash
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/rvolykh/vui

go 1.25.1
go 1.25.3

require (
github.com/atotto/clipboard v0.1.4
Expand Down
40 changes: 28 additions & 12 deletions internal/engines/aws/aws_secrets_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,18 +303,26 @@ func (c *AWSClient) CreateSecret(path string, data map[string]any) error {

secretName := strings.Trim(path, "/")

// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
var secretString string
// Check if data has empty key marker (omitted key case)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
// Store value as plain string (no JSON conversion)
secretString = fmt.Sprintf("%v", value)
} else {
// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}
secretString = string(jsonData)
}

input := &secretsmanager.CreateSecretInput{
Name: aws.String(secretName),
SecretString: aws.String(string(jsonData)),
SecretString: aws.String(secretString),
}

_, err = c.client.CreateSecretWithContext(ctx, input)
_, err := c.client.CreateSecretWithContext(ctx, input)
if err != nil {
return fmt.Errorf("failed to create secret '%s': %w", path, err)
}
Expand All @@ -329,18 +337,26 @@ func (c *AWSClient) UpdateSecret(path string, data map[string]any) error {

secretName := strings.Trim(path, "/")

// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
var secretString string
// Check if data has empty key marker (omitted key case)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
// Store value as plain string (no JSON conversion)
secretString = fmt.Sprintf("%v", value)
} else {
// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}
secretString = string(jsonData)
}

input := &secretsmanager.UpdateSecretInput{
SecretId: aws.String(secretName),
SecretString: aws.String(string(jsonData)),
SecretString: aws.String(secretString),
}

_, err = c.client.UpdateSecretWithContext(ctx, input)
_, err := c.client.UpdateSecretWithContext(ctx, input)
if err != nil {
return fmt.Errorf("failed to update secret '%s': %w", path, err)
}
Expand Down
62 changes: 28 additions & 34 deletions internal/engines/aws/aws_ssm.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,33 +317,28 @@ func (c *AWSSSMClient) CreateSecret(path string, data map[string]any) error {
paramName = "/" + paramName
}

// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}

// Determine parameter type - if data contains sensitive info, use SecureString
// For simplicity, we'll use SecureString for all secrets
paramType := "SecureString"

// Check if the data suggests it's a plain string (single "value" key)
if len(data) == 1 {
if _, ok := data["value"]; ok {
// If it's a simple value, use String type
paramType = "String"
jsonData = []byte(fmt.Sprintf("%v", data["value"]))
var valueString string
// Check if data has empty key marker (omitted key case)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
// Store value as plain string (no JSON conversion)
valueString = fmt.Sprintf("%v", value)
} else {
// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}
valueString = string(jsonData)
}

input := &ssm.PutParameterInput{
Name: aws.String(paramName),
Value: aws.String(string(jsonData)),
Type: aws.String(paramType),
Value: aws.String(valueString),
Type: aws.String("SecureString"),
Overwrite: aws.Bool(false), // Don't overwrite existing parameters
}

_, err = c.client.PutParameterWithContext(ctx, input)
_, err := c.client.PutParameterWithContext(ctx, input)
if err != nil {
return fmt.Errorf("failed to create parameter '%s': %w", path, err)
}
Expand All @@ -362,34 +357,33 @@ func (c *AWSSSMClient) UpdateSecret(path string, data map[string]any) error {
paramName = "/" + paramName
}

// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}

paramType := "SecureString"
// Get existing parameter to determine type
getInput := &ssm.GetParameterInput{
Name: aws.String(paramName),
}
existingParam, err := c.client.GetParameterWithContext(ctx, getInput)

paramType := "SecureString"
if err == nil && existingParam != nil && existingParam.Parameter != nil && existingParam.Parameter.Type != nil {
paramType = *existingParam.Parameter.Type
}

var valueString string
// Check if data has empty key marker (omitted key case)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
// Store value as plain string (no JSON conversion)
valueString = fmt.Sprintf("%v", value)
} else {
// If parameter doesn't exist or we can't determine type, check data structure
if len(data) == 1 {
if _, ok := data["value"]; ok {
paramType = "String"
jsonData = []byte(fmt.Sprintf("%v", data["value"]))
}
// Convert data map to JSON string
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal secret data: %w", err)
}
valueString = string(jsonData)
}

input := &ssm.PutParameterInput{
Name: aws.String(paramName),
Value: aws.String(string(jsonData)),
Value: aws.String(valueString),
Type: aws.String(paramType),
Overwrite: aws.Bool(true), // Overwrite for updates
}
Expand Down
14 changes: 14 additions & 0 deletions internal/engines/vault/vault_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ func (c *VaultClient) CreateSecret(path string, data map[string]any) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Handle empty key marker (defensive check - handler should already transform for Vault)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
data = map[string]any{
"value": value,
}
}

// For KV v2, we need to wrap the data
secretData := map[string]any{
"data": data,
Expand All @@ -203,6 +210,13 @@ func (c *VaultClient) UpdateSecret(path string, data map[string]any) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Handle empty key marker (defensive check - handler should already transform for Vault)
if value, hasEmptyKey := data[""]; hasEmptyKey && len(data) == 1 {
data = map[string]any{
"value": value,
}
}

// For KV v2, we need to wrap the data
secretData := map[string]any{
"data": data,
Expand Down
25 changes: 15 additions & 10 deletions internal/ui/forms/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/rivo/tview"
"github.com/rvolykh/vui/internal/ui/common"
"github.com/rvolykh/vui/internal/utils"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -117,15 +118,18 @@ func (fb *KeyValueFormBuilder) rebuildForm() {
fb.logger.Infof("rebuildForm: Processing pair %d/%d: key='%s'", idx+1, len(keys), k)
v := fb.keyValuePairs[k]

// Display key label - show "(no key)" for empty keys
displayKey := utils.Coalesce(k, "(no key)")

if fb.config.ShowDeleteKeys {
fb.logger.Infof("rebuildForm: Adding delete button for key='%s'", k)
// For edit mode: show as input field with delete button
// Create a copy of k for the closure
key := k

valueField := tview.NewTextArea().SetLabel(key).SetText(v, true)
valueField := tview.NewTextArea().SetLabel(displayKey).SetText(v, true)
form.AddFormItem(valueField)
form.AddButton("Delete "+key, fb.createDeleteKeyHandler(key))
form.AddButton("Delete "+displayKey, fb.createDeleteKeyHandler(key))
} else {
displayValue := strings.ReplaceAll(v, "\n", "\\n")
if len(displayValue) > 40 {
Expand All @@ -134,7 +138,7 @@ func (fb *KeyValueFormBuilder) rebuildForm() {

fb.logger.Infof("rebuildForm: Adding read-only display for key='%s'", k)
// For create mode: show as read-only text
form.AddTextView(" • "+k, displayValue, 0, 1, false, false)
form.AddTextView(" • "+displayKey, displayValue, 0, 1, false, false)
}
}
fb.logger.Info("rebuildForm: Finished processing existing pairs")
Expand Down Expand Up @@ -202,16 +206,16 @@ func (fb *KeyValueFormBuilder) createAddKeyValueHandler() func() {
value := valueField.GetText()
fb.logger.Infof("Retrieved key='%s', value length=%d", key, len(value))

if key == "" {
fb.logger.Warn("Key cannot be empty")
return
}

if value == "" {
fb.logger.Warn("Value cannot be empty")
return
}

// Allow empty key when value is present - use empty string as marker
if key == "" {
fb.logger.Info("Key is empty but value is present - will use empty key marker")
}

// Add to the pairs map
fb.keyValuePairs[key] = value
fb.logger.Infof("Added key-value pair: %s", key)
Expand Down Expand Up @@ -250,9 +254,10 @@ func (fb *KeyValueFormBuilder) createSaveHandler() func() {
key := strings.TrimSpace(keyField.GetText())
value := valueField.GetText()

if key != "" && value != "" {
// Allow empty key when value is present
if value != "" {
fb.keyValuePairs[key] = value
fb.logger.Infof("Auto-added new key-value pair before save: %s", key)
fb.logger.Infof("Auto-added new key-value pair before save: key='%s'", key)
}
}

Expand Down