Skip to content
Draft
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
2 changes: 2 additions & 0 deletions example/actions/objects/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM alpine:3.20
RUN env
26 changes: 26 additions & 0 deletions example/actions/objects/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
action:
title: Credentials object
description: Test credentials processor with object
options:
- name: creds
title: Credentials opt
type: object
default: {}
process:
- processor: keyring.GetCredentials
options:
url_from_key: "url"
url_from_config: "my_action.url"

runtime:
type: container
image: envvars:latest
build:
context: ./
command:
- sh
- /action/main.sh
- "{{ .creds }}"
- "{{ .creds.url }}"
- "{{ .creds.username }}"
- "{{ .creds.password }}"
15 changes: 15 additions & 0 deletions example/actions/objects/main.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/sh

echo
# Print the total count of arguments
echo "Total number of arguments passed to the script: $#"
count=1
for arg in "$@"
do
if [ -z "$arg" ]; then
echo "- Argument $count: \"\""
else
echo "- Argument $count: $arg"
fi
count=$((count + 1))
done
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.24.1

require (
filippo.io/age v1.2.1
github.com/launchrctl/launchr v0.21.0
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550
github.com/stretchr/testify v1.10.0
golang.org/x/term v0.31.0
gopkg.in/yaml.v3 v3.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/launchrctl/launchr v0.21.0 h1:TruuJToGh4nvinoG6z4AgDtONrlshVws7tzUQak/8J8=
github.com/launchrctl/launchr v0.21.0/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo=
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550 h1:aQRn5Zcym/pH1kHdhWJ3AwE17Om1RC0WMnj9S33Wm9w=
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
Expand Down
94 changes: 89 additions & 5 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
)

const (
procGetKeyValue = "keyring.GetKeyValue"
errTplNotFoundURL = "%s not found in keyring. Use `%s keyring:login` to add it."
errTplNotFoundKey = "%s not found in keyring. Use `%s keyring:set` to add it."
procGetKeyValue = "keyring.GetKeyValue"
procGetCredentials = "keyring.GetCredentials" //nolint:gosec // G101: Processor name with masked credentials.
errTplNotFoundURL = "%s not found in keyring. Use `%s keyring:login` to add it."
errTplNotFoundKey = "%s not found in keyring. Use `%s keyring:set` to add it."

envVarPassphrase = launchr.EnvVar("keyring_passphrase")
envVarPassphraseFile = launchr.EnvVar("keyring_passphrase_file")
Expand Down Expand Up @@ -72,7 +73,7 @@ func (p *Plugin) OnAppInit(app launchr.App) error {
var m action.Manager
app.GetService(&m)

addValueProcessors(m, p.k)
addValueProcessors(m, p.k, p.cfg)
return nil
}

Expand All @@ -81,14 +82,27 @@ type GetKeyValueProcessorOptions = *action.GenericValueProcessorOptions[struct {
Key string `yaml:"key" validate:"not-empty"`
}]

// GetCredentialsProcessorOptions is a [action.ValueProcessorOptions] struct.
type GetCredentialsProcessorOptions = *action.GenericValueProcessorOptions[struct {
URLFromKey string `yaml:"url_from_key"`
URLFromConfig string `yaml:"url_from_config"`
}]

// addValueProcessors adds a keyring [action.ValueProcessor] to [action.Manager].
func addValueProcessors(m action.Manager, keyring Keyring) {
func addValueProcessors(m action.Manager, keyring Keyring, cfg launchr.Config) {
m.AddValueProcessor(procGetKeyValue, action.GenericValueProcessor[GetKeyValueProcessorOptions]{
Types: []jsonschema.Type{jsonschema.String},
Fn: func(v any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext) (any, error) {
return processGetByKey(v, opts, ctx, keyring)
},
})

m.AddValueProcessor(procGetCredentials, action.GenericValueProcessor[GetCredentialsProcessorOptions]{
Types: []jsonschema.Type{jsonschema.Object},
Fn: func(v any, opts GetCredentialsProcessorOptions, ctx action.ValueProcessorContext) (any, error) {
return processGetCredentials(v, opts, ctx, keyring, cfg)
},
})
}

func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext, k Keyring) (any, error) {
Expand Down Expand Up @@ -131,6 +145,76 @@ func processGetByKey(value any, opts GetKeyValueProcessorOptions, ctx action.Val
return value, buildNotFoundError(opts.Fields.Key, errTplNotFoundKey, err)
}

func processGetCredentials(value any, opts GetCredentialsProcessorOptions, ctx action.ValueProcessorContext, k Keyring, cfg launchr.Config) (any, error) {
// Init object
if value == nil {
value = make(map[string]interface{})
}

m, ok := value.(map[string]interface{})
if !ok {
return value, fmt.Errorf("%s: invalid value type submitted: %T", procGetCredentials, value)
}

var url string
if opts.Fields.URLFromKey != "" {
url, ok = m[opts.Fields.URLFromKey].(string)
if !ok {
launchr.Term().Warning().Printfln("%s: specified key `%s` doesn't exist in passed object", procGetCredentials, opts.Fields.URLFromKey)
}
}

if url == "" && opts.Fields.URLFromConfig != "" {
err := cfg.Get(opts.Fields.URLFromConfig, &url)
if err != nil {
return value, err
}
}

if url == "" {
return value, fmt.Errorf("%s: credentials URL is not provided in processor options or submitted data", procGetCredentials)
}

ci, err := k.GetForURL(url)
if err == nil {
m["url"] = ci.URL
m["username"] = ci.Username
m["password"] = ci.Password

return value, nil
}

streams := ctx.Input.Streams()
isTerminal := streams != nil && streams.In().IsTerminal()
if errors.Is(err, ErrNotFound) && isTerminal {
item := CredentialsItem{URL: url}
err = RequestCredentialsFromTty(&item)
if err != nil {
return value, err
}

err = k.AddItem(item)
if err != nil {
return value, err
}

// Ensure keyring storage will be accessible after save.
defer k.ResetStorage()
err = k.Save()
if err != nil {
return value, err
}
launchr.Term().Info().Printfln("URL credentials %q has been added to keyring", item.URL)

m["url"] = ci.URL
m["username"] = ci.Username
m["password"] = ci.Password
return value, nil
}

return value, buildNotFoundError(url, errTplNotFoundURL, err)
}

// DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface.
func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) {
// Action list.
Expand Down
55 changes: 54 additions & 1 deletion plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Test_KeyringProcessor(t *testing.T) {
mask: &launchr.SensitiveMask{},
}
am := action.NewManager()
addValueProcessors(am, k)
addValueProcessors(am, k, nil)

// Prepare test data.
expected := "my_secret"
Expand Down Expand Up @@ -92,3 +92,56 @@ func Test_KeyringProcessor(t *testing.T) {
})
}
}

const testCredentialsActionYaml = `
runtime: plugin
action:
title: test credentials
options:
- name: credentials
type: object
process:
- processor: keyring.GetCredentials
options:
url_from_key: "url"
`

func Test_CredentialsProcessor(t *testing.T) {
// Prepare services.
k := &keyringService{
store: &dataStoreYaml{file: &plainFile{fname: "teststorage.yaml"}},
mask: &launchr.SensitiveMask{},
}
am := action.NewManager()
addValueProcessors(am, k, nil)

// Prepare test data.
expected := map[string]any{
"url": "myurl",
"username": "user",
"password": "pass",
}

err := k.AddItem(CredentialsItem{URL: "myurl", Username: "user", Password: "pass"})
require.NoError(t, err)

expConfig := action.InputParams{
"credentials": expected,
}
expGiven := action.InputParams{
"credentials": map[string]any{
"url": "myurl",
},
}
tt := []action.TestCaseValueProcessor{
{Name: "get keyring credentials - input with URL given", Yaml: testCredentialsActionYaml, Opts: expGiven, ExpOpts: expConfig},
}

for _, tt := range tt {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
tt.Test(t, am)
})
}
}
Loading