Files
lazysecrets/internal/provider/repo/compose.go
T
keb de38b8b281
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 1s
feat: Docker Compose files in repos
compose.yaml, docker-compose.yml and their variants tracked by git are
listed under Repos, their services' environment variables as secrets,
named service.VAR.

- environment as a map or a list, and <<: *anchors shared between
  services; the secret patterns pick the keys, the rest hidden, with
  compose.secretPatterns for compose files' own.
- ${VAR}, $VAR, ${VAR:?…} and variables without a value are marked as
  references; docker-compose.example.yml and the like are hidden.
- Jasypt values decrypt with the service's JASYPT_ENCRYPTOR_PASSWORD.
- A blue Docker icon, themeable with composeColor.
2026-10-15 14:19:30 +02:00

142 lines
3.6 KiB
Go

package repo
import (
"path"
"regexp"
"strings"
"go.yaml.in/yaml/v3"
)
// Docker Compose files: compose.yaml, docker-compose.yml and their
// variants (docker-compose.test.yml). Their keys are the services'
// environment variables, named service.VAR.
var composeType = configType{
name: "compose", is: IsCompose, format: func(string) string { return "yaml" }, parse: parseCompose,
jasyptSetting: func(name string) string {
service, _, _ := strings.Cut(name, ".")
return service + "." + jasyptEnv
},
placeholder: composePlaceholder,
}
var composeName = regexp.MustCompile(`^(docker-)?compose([.-][^/]+)?\.ya?ml$`)
// IsCompose reports whether f is named like a Docker Compose file.
func IsCompose(f string) bool {
return composeName.MatchString(strings.ToLower(path.Base(f)))
}
// composePlaceholderRe is compose's ${VAR}, ${VAR:-default}, ${VAR-default},
// ${VAR:?error}, ${VAR?error} or $VAR.
var composePlaceholderRe = regexp.MustCompile(`^\$(?:\{(\w+)(?:(:?-)([^}]*)|:?\?[^}]*)?\}|(\w+))$`)
func composePlaceholder(v string) (name, def string, hasDef, ok bool) {
m := composePlaceholderRe.FindStringSubmatch(v)
if m == nil {
return "", "", false, false
}
return m[1] + m[4], m[3], m[2] != "", true
}
func parseCompose(_ string, b []byte) ([]configKey, error) {
var doc yaml.Node
if err := yaml.Unmarshal(b, &doc); err != nil {
return nil, err
}
if len(doc.Content) == 0 {
return nil, nil
}
var out []configKey
for _, service := range pairs(field(doc.Content[0], "services")) {
for _, k := range environment(field(service.value, "environment")) {
k.name = service.key.Value + "." + k.key
out = append(out, k)
}
}
return out, nil
}
// environment reads a service's environment, a map (KEY: value) or a list
// (- KEY=value). A variable without a value (KEY:, - KEY) is taken from
// where compose runs: a reference.
func environment(n *yaml.Node) []configKey {
var out []configKey
switch n = deref(n); {
case n == nil:
case n.Kind == yaml.MappingNode:
for _, p := range pairs(n) {
v := deref(p.value)
ref := v.Tag == "!!null" || reference(composePlaceholder, v.Value)
out = append(out, configKey{key: p.key.Value, value: v.Value, ref: ref, line: p.key.Line})
}
case n.Kind == yaml.SequenceNode:
for _, item := range n.Content {
item = deref(item)
k, v, ok := strings.Cut(item.Value, "=")
out = append(out, configKey{key: k, value: v, ref: !ok || reference(composePlaceholder, v), line: item.Line})
}
}
return out
}
type pair struct{ key, value *yaml.Node }
// pairs are a mapping's entries, with those merged in by << (anchors
// shared between services) first and its own over them.
func pairs(n *yaml.Node) []pair {
n = deref(n)
if n == nil || n.Kind != yaml.MappingNode {
return nil
}
var merged, own []pair
for i := 0; i+1 < len(n.Content); i += 2 {
k, v := n.Content[i], n.Content[i+1]
if k.Value != "<<" {
own = append(own, pair{k, v})
continue
}
from := []*yaml.Node{v}
if v.Kind == yaml.SequenceNode {
from = v.Content
}
for _, m := range from {
merged = append(merged, pairs(m)...)
}
}
var out []pair
for _, p := range append(merged, own...) {
if i := indexOf(out, p.key.Value); i >= 0 {
out[i] = p
} else {
out = append(out, p)
}
}
return out
}
func indexOf(ps []pair, key string) int {
for i, p := range ps {
if p.key.Value == key {
return i
}
}
return -1
}
func field(n *yaml.Node, key string) *yaml.Node {
ps := pairs(n)
if i := indexOf(ps, key); i >= 0 {
return ps[i].value
}
return nil
}
func deref(n *yaml.Node) *yaml.Node {
for n != nil && n.Kind == yaml.AliasNode {
n = n.Alias
}
return n
}