v2.30.0-pre3 63e7f3a

pass_loader.go RawHistoryFolder

1// Package envloader implements the environment variable loader adapter.
2package envloader
3
4import (
5	"context"
6	"fmt"
7	"sort"
8	"strings"
9
10	"github.com/bnema/zerowrap"
11
12	"github.com/bnema/gordon/internal/adapters/out/domainsecrets"
13)
14
15// PassLoader implements the EnvLoader interface using pass-backed secrets.
16type PassLoader struct {
17	store *domainsecrets.PassStore
18	log   zerowrap.Logger
19}
20
21// NewPassLoader creates a new pass-based environment loader.
22func NewPassLoader(store *domainsecrets.PassStore, log zerowrap.Logger) (*PassLoader, error) {
23	if store == nil {
24		return nil, fmt.Errorf("pass store is required")
25	}
26
27	log.Debug().
28		Str(zerowrap.FieldLayer, "adapter").
29		Str(zerowrap.FieldAdapter, "envloader").
30		Str("provider", "pass").
31		Msg("env loader initialized")
32
33	return &PassLoader{
34		store: store,
35		log:   log,
36	}, nil
37}
38
39// LoadEnv loads environment variables for a given domain.
40func (l *PassLoader) LoadEnv(ctx context.Context, domain string) ([]string, error) {
41	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
42		zerowrap.FieldLayer:   "adapter",
43		zerowrap.FieldAdapter: "envloader",
44		zerowrap.FieldAction:  "LoadEnv",
45		"domain":              domain,
46	})
47	log := zerowrap.FromCtx(ctx)
48
49	var secretsMap map[string]string
50	var err error
51
52	if isAttachmentContainer(domain) {
53		secretsMap, err = l.store.GetAllAttachment(domain)
54		if err != nil {
55			return nil, log.WrapErr(err, "failed to load attachment env from pass")
56		}
57	} else {
58		secretsMap, err = l.store.GetAll(domain)
59		if err != nil {
60			return nil, log.WrapErr(err, "failed to load env from pass")
61		}
62	}
63
64	keys := make([]string, 0, len(secretsMap))
65	for key := range secretsMap {
66		keys = append(keys, key)
67	}
68	sort.Strings(keys)
69
70	envVars := make([]string, 0, len(keys))
71	for _, key := range keys {
72		envVars = append(envVars, fmt.Sprintf("%s=%s", key, secretsMap[key]))
73	}
74
75	log.Info().Int(zerowrap.FieldCount, len(envVars)).Msg("loaded environment variables for route")
76
77	return envVars, nil
78}
79
80// isAttachmentContainer determines whether a given name refers to an attachment container
81// or a domain. It uses a simple heuristic: attachment container names always start with
82// the "gordon-" prefix (e.g., "gordon-git-example-com-gitea-postgres").
83//
84// LIMITATION: This heuristic assumes domains never start with "gordon-". A domain like
85// "gordon-app.example.com" would be incorrectly classified as an attachment container.
86// This is an acceptable trade-off for simplicity, but if such domains become common,
87// this should be replaced with a more robust detection mechanism (e.g., checking both
88// GetAll() and GetAllAttachment() and seeing which returns results).
89func isAttachmentContainer(name string) bool {
90	// Attachment container names start with "gordon-" and never contain dots.
91	// Domain names (including preview domains like "gordon--test.bnema.dev")
92	// always contain dots, so checking for dots distinguishes them.
93	return strings.HasPrefix(name, "gordon-") && !strings.Contains(name, ".")
94}
95
96// CreateEnvFile is a no-op for pass-backed loader.
97func (l *PassLoader) CreateEnvFile(ctx context.Context, domain string) error {
98	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
99		zerowrap.FieldLayer:   "adapter",
100		zerowrap.FieldAdapter: "envloader",
101		zerowrap.FieldAction:  "CreateEnvFile",
102		"domain":              domain,
103	})
104	log := zerowrap.FromCtx(ctx)
105	log.Debug().Msg("pass loader does not create env files")
106	return nil
107}
108
109// EnvFileExists checks if a manifest exists for a domain in pass.
110func (l *PassLoader) EnvFileExists(domain string) (bool, error) {
111	exists, err := l.store.ManifestExists(domain)
112	if err != nil {
113		l.log.Warn().Err(err).Str("domain", domain).Msg("failed to check pass manifest")
114		return false, err
115	}
116	return exists, nil
117}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close