feat(ansible): support password lookups
Some checks are pending
CI / test (push) Waiting to run
CI / auto-fix (push) Waiting to run
CI / auto-merge (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 14:07:04 +00:00
parent 1c637a2199
commit 153bf5b863
2 changed files with 153 additions and 0 deletions

View file

@ -2,6 +2,7 @@ package ansible
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"io"
@ -3625,11 +3626,138 @@ func (e *Executor) lookupValue(expr string, host string, task *Task) (any, bool)
if value, ok := e.lookupConditionValue(arg, host, task, nil); ok {
return value, true
}
case "password":
if value, ok := e.lookupPassword(arg); ok {
return value, true
}
}
return nil, false
}
func (e *Executor) lookupPassword(arg string) (string, bool) {
spec := parsePasswordLookupSpec(arg)
if spec.path == "" {
return "", false
}
resolvedPath := e.resolveLocalPath(spec.path)
if data, err := coreio.Local.Read(resolvedPath); err == nil {
return strings.TrimRight(data, "\r\n"), true
}
password, err := generatePassword(spec.length, spec.chars)
if err != nil {
return "", false
}
if resolvedPath != "/dev/null" {
if err := coreio.Local.EnsureDir(pathDir(resolvedPath)); err != nil {
return "", false
}
if err := coreio.Local.Write(resolvedPath, password); err != nil {
return "", false
}
}
return password, true
}
type passwordLookupSpec struct {
path string
length int
chars string
}
func parsePasswordLookupSpec(arg string) passwordLookupSpec {
spec := passwordLookupSpec{
length: 20,
chars: passwordLookupCharset("ascii_letters,digits"),
}
fields := strings.Fields(arg)
for _, field := range fields {
key, value, ok := strings.Cut(field, "=")
if ok {
switch lower(strings.TrimSpace(key)) {
case "length":
if n, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && n > 0 {
spec.length = n
}
case "chars":
if chars := passwordLookupCharset(value); chars != "" {
spec.chars = chars
}
}
continue
}
if spec.path == "" {
spec.path = field
}
}
return spec
}
func passwordLookupCharset(value string) string {
if value == "" {
return ""
}
var chars strings.Builder
seen := make(map[rune]bool)
appendChars := func(set string) {
for _, r := range set {
if seen[r] {
continue
}
seen[r] = true
chars.WriteRune(r)
}
}
for _, token := range strings.Split(value, ",") {
switch lower(strings.TrimSpace(token)) {
case "ascii_letters":
appendChars("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
case "ascii_lowercase":
appendChars("abcdefghijklmnopqrstuvwxyz")
case "ascii_uppercase":
appendChars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
case "digits":
appendChars("0123456789")
case "hexdigits":
appendChars("0123456789abcdefABCDEF")
default:
appendChars(token)
}
}
return chars.String()
}
func generatePassword(length int, chars string) (string, error) {
if length <= 0 {
length = 20
}
if chars == "" {
chars = passwordLookupCharset("ascii_letters,digits")
}
buf := make([]byte, length)
if _, err := rand.Read(buf); err != nil {
return "", coreerr.E("Executor.lookupPassword", "generate password", err)
}
output := make([]byte, length)
for i, b := range buf {
output[i] = chars[int(b)%len(chars)]
}
return string(output), nil
}
// resolveLoop resolves loop items.
func (e *Executor) resolveLoop(loop any, host string) []any {
return e.resolveLoopWithTask(loop, host, nil)

View file

@ -910,6 +910,31 @@ func TestExecutorExtra_HandleLookup_Good_FileGlobLookup(t *testing.T) {
}), result)
}
func TestExecutorExtra_HandleLookup_Good_PasswordLookupReadsExistingFile(t *testing.T) {
dir := t.TempDir()
passPath := joinPath(dir, "secrets", "app.pass")
require.NoError(t, writeTestFile(passPath, []byte("s3cret\n"), 0600))
e := NewExecutor(dir)
result := e.handleLookup("lookup('password', 'secrets/app.pass')", "", nil)
assert.Equal(t, "s3cret", result)
}
func TestExecutorExtra_HandleLookup_Good_PasswordLookupCreatesFile(t *testing.T) {
dir := t.TempDir()
passPath := joinPath(dir, "secrets", "generated.pass")
e := NewExecutor(dir)
result := e.handleLookup("lookup('password', 'secrets/generated.pass length=12 chars=digits')", "", nil)
require.Len(t, result, 12)
content, err := readTestFile(passPath)
require.NoError(t, err)
assert.Equal(t, result, string(content))
assert.Len(t, content, 12)
}
func TestExecutorExtra_RunTaskOnHost_Good_LoopFromFileGlobLookup(t *testing.T) {
dir := t.TempDir()
require.NoError(t, writeTestFile(joinPath(dir, "files", "a.txt"), []byte("alpha"), 0644))