Skip to content

Commit f0f1435

Browse files
committed
Support for ternary values keyring processor to actions
1 parent 186b404 commit f0f1435

File tree

7 files changed

+189
-9
lines changed

7 files changed

+189
-9
lines changed

example/actions/objects/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM alpine:3.20
2+
RUN env
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
action:
2+
title: Credentials object
3+
description: Test credentials processor with object
4+
options:
5+
- name: creds
6+
title: Credentials opt
7+
type: object
8+
default: {}
9+
process:
10+
- processor: keyring.GetCredentials
11+
options:
12+
url_from_key: "url"
13+
url_from_config: "my_action.url"
14+
15+
runtime:
16+
type: container
17+
image: envvars:latest
18+
build:
19+
context: ./
20+
command:
21+
- sh
22+
- /action/main.sh
23+
- "{{ .creds }}"
24+
- "{{ .creds.url }}"
25+
- "{{ .creds.username }}"
26+
- "{{ .creds.password }}"

example/actions/objects/main.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/sh
2+
3+
echo
4+
# Print the total count of arguments
5+
echo "Total number of arguments passed to the script: $#"
6+
count=1
7+
for arg in "$@"
8+
do
9+
if [ -z "$arg" ]; then
10+
echo "- Argument $count: \"\""
11+
else
12+
echo "- Argument $count: $arg"
13+
fi
14+
count=$((count + 1))
15+
done

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ toolchain go1.24.1
66

77
require (
88
filippo.io/age v1.2.1
9-
github.com/launchrctl/launchr v0.21.0
9+
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550
1010
github.com/stretchr/testify v1.10.0
1111
golang.org/x/term v0.31.0
1212
gopkg.in/yaml.v3 v3.0.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
253253
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
254254
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
255255
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
256-
github.com/launchrctl/launchr v0.21.0 h1:TruuJToGh4nvinoG6z4AgDtONrlshVws7tzUQak/8J8=
257-
github.com/launchrctl/launchr v0.21.0/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo=
256+
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550 h1:aQRn5Zcym/pH1kHdhWJ3AwE17Om1RC0WMnj9S33Wm9w=
257+
github.com/launchrctl/launchr v0.21.2-0.20250625094547-5e88fd38c550/go.mod h1:C7H4FHMSjNi4fUt36rzfyE/2xSpdBdUuq7n7IPAzqEo=
258258
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
259259
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
260260
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=

plugin.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import (
1818
)
1919

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

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

75-
addValueProcessors(m, p.k)
76+
addValueProcessors(m, p.k, p.cfg)
7677
return nil
7778
}
7879

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

85+
// GetCredentialsProcessorOptions is a [action.ValueProcessorOptions] struct.
86+
type GetCredentialsProcessorOptions = *action.GenericValueProcessorOptions[struct {
87+
URLFromKey string `yaml:"url_from_key"`
88+
URLFromConfig string `yaml:"url_from_config"`
89+
}]
90+
8491
// addValueProcessors adds a keyring [action.ValueProcessor] to [action.Manager].
85-
func addValueProcessors(m action.Manager, keyring Keyring) {
92+
func addValueProcessors(m action.Manager, keyring Keyring, cfg launchr.Config) {
8693
m.AddValueProcessor(procGetKeyValue, action.GenericValueProcessor[GetKeyValueProcessorOptions]{
8794
Types: []jsonschema.Type{jsonschema.String},
8895
Fn: func(v any, opts GetKeyValueProcessorOptions, ctx action.ValueProcessorContext) (any, error) {
8996
return processGetByKey(v, opts, ctx, keyring)
9097
},
9198
})
99+
100+
m.AddValueProcessor(procGetCredentials, action.GenericValueProcessor[GetCredentialsProcessorOptions]{
101+
Types: []jsonschema.Type{jsonschema.Object},
102+
Fn: func(v any, opts GetCredentialsProcessorOptions, ctx action.ValueProcessorContext) (any, error) {
103+
return processGetCredentials(v, opts, ctx, keyring, cfg)
104+
},
105+
})
92106
}
93107

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

148+
func processGetCredentials(value any, opts GetCredentialsProcessorOptions, ctx action.ValueProcessorContext, k Keyring, cfg launchr.Config) (any, error) {
149+
// Init object
150+
if value == nil {
151+
value = make(map[string]interface{})
152+
}
153+
154+
m, ok := value.(map[string]interface{})
155+
if !ok {
156+
return value, fmt.Errorf("%s: invalid value type submitted: %T", procGetCredentials, value)
157+
}
158+
159+
var url string
160+
if opts.Fields.URLFromKey != "" {
161+
url, ok = m[opts.Fields.URLFromKey].(string)
162+
if !ok {
163+
launchr.Term().Warning().Printfln("%s: specified key `%s` doesn't exist in passed object", procGetCredentials, opts.Fields.URLFromKey)
164+
}
165+
}
166+
167+
if url == "" && opts.Fields.URLFromConfig != "" {
168+
err := cfg.Get(opts.Fields.URLFromConfig, &url)
169+
if err != nil {
170+
return value, err
171+
}
172+
}
173+
174+
if url == "" {
175+
return value, fmt.Errorf("%s: credentials URL is not provided in processor options or submitted data", procGetCredentials)
176+
}
177+
178+
ci, err := k.GetForURL(url)
179+
if err == nil {
180+
m["url"] = ci.URL
181+
m["username"] = ci.Username
182+
m["password"] = ci.Password
183+
184+
return value, nil
185+
}
186+
187+
streams := ctx.Input.Streams()
188+
isTerminal := streams != nil && streams.In().IsTerminal()
189+
if errors.Is(err, ErrNotFound) && isTerminal {
190+
item := CredentialsItem{URL: url}
191+
err = RequestCredentialsFromTty(&item)
192+
if err != nil {
193+
return value, err
194+
}
195+
196+
err = k.AddItem(item)
197+
if err != nil {
198+
return value, err
199+
}
200+
201+
// Ensure keyring storage will be accessible after save.
202+
defer k.ResetStorage()
203+
err = k.Save()
204+
if err != nil {
205+
return value, err
206+
}
207+
launchr.Term().Info().Printfln("URL credentials %q has been added to keyring", item.URL)
208+
209+
m["url"] = ci.URL
210+
m["username"] = ci.Username
211+
m["password"] = ci.Password
212+
return value, nil
213+
}
214+
215+
return value, buildNotFoundError(url, errTplNotFoundURL, err)
216+
}
217+
134218
// DiscoverActions implements [launchr.ActionDiscoveryPlugin] interface.
135219
func (p *Plugin) DiscoverActions(_ context.Context) ([]*action.Action, error) {
136220
// Action list.

plugin_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func Test_KeyringProcessor(t *testing.T) {
6464
mask: &launchr.SensitiveMask{},
6565
}
6666
am := action.NewManager()
67-
addValueProcessors(am, k)
67+
addValueProcessors(am, k, nil)
6868

6969
// Prepare test data.
7070
expected := "my_secret"
@@ -92,3 +92,56 @@ func Test_KeyringProcessor(t *testing.T) {
9292
})
9393
}
9494
}
95+
96+
const testCredentialsActionYaml = `
97+
runtime: plugin
98+
action:
99+
title: test credentials
100+
options:
101+
- name: credentials
102+
type: object
103+
process:
104+
- processor: keyring.GetCredentials
105+
options:
106+
url_from_key: "url"
107+
`
108+
109+
func Test_CredentialsProcessor(t *testing.T) {
110+
// Prepare services.
111+
k := &keyringService{
112+
store: &dataStoreYaml{file: &plainFile{fname: "teststorage.yaml"}},
113+
mask: &launchr.SensitiveMask{},
114+
}
115+
am := action.NewManager()
116+
addValueProcessors(am, k, nil)
117+
118+
// Prepare test data.
119+
expected := map[string]any{
120+
"url": "myurl",
121+
"username": "user",
122+
"password": "pass",
123+
}
124+
125+
err := k.AddItem(CredentialsItem{URL: "myurl", Username: "user", Password: "pass"})
126+
require.NoError(t, err)
127+
128+
expConfig := action.InputParams{
129+
"credentials": expected,
130+
}
131+
expGiven := action.InputParams{
132+
"credentials": map[string]any{
133+
"url": "myurl",
134+
},
135+
}
136+
tt := []action.TestCaseValueProcessor{
137+
{Name: "get keyring credentials - input with URL given", Yaml: testCredentialsActionYaml, Opts: expGiven, ExpOpts: expConfig},
138+
}
139+
140+
for _, tt := range tt {
141+
tt := tt
142+
t.Run(tt.Name, func(t *testing.T) {
143+
t.Parallel()
144+
tt.Test(t, am)
145+
})
146+
}
147+
}

0 commit comments

Comments
 (0)