v3.4.1 de8f68e

store.go RawHistoryFolder

1// Package secretfiles stores file-delivered app secrets on the host.
2//
3// Layout: <data_dir>/secrets-run/<app>/<container-name>/<KEY>, directories
4// 0700 and files 0444. All access goes through an os.Root so symlinks
5// cannot escape the base directory. Values and paths are never logged.
6package secretfiles
7
8import (
9	"context"
10	"errors"
11	"fmt"
12	"io/fs"
13	"maps"
14	"os"
15	"path/filepath"
16	"slices"
17	"strings"
18	"syscall"
19
20	"github.com/bnema/gordon/internal/boundaries/out"
21	"github.com/bnema/gordon/internal/domain"
22)
23
24const (
25	dirMode  os.FileMode = 0o700
26	fileMode os.FileMode = 0o444
27	// maxContainerName bounds a container directory name.
28	maxContainerName = 255
29)
30
31// Store implements out.SecretFileStore on the local filesystem.
32type Store struct {
33	dir  string
34	root *os.Root
35}
36
37var _ out.SecretFileStore = (*Store)(nil)
38
39// NewStore opens (creating if needed) <dataDir>/secrets-run.
40func NewStore(dataDir string) (*Store, error) {
41	// Bind sources must be absolute: resolve a relative data dir once.
42	dir, err := filepath.Abs(filepath.Join(dataDir, "secrets-run"))
43	if err != nil {
44		return nil, fmt.Errorf("secret files: resolve base directory: %w", err)
45	}
46	if err := os.MkdirAll(dir, dirMode); err != nil {
47		return nil, fmt.Errorf("secret files: create base directory: %w", err)
48	}
49	info, err := os.Lstat(dir)
50	if err != nil {
51		return nil, fmt.Errorf("secret files: stat base directory: %w", err)
52	}
53	if !info.IsDir() {
54		return nil, errors.New("secret files: base directory must be a directory, not a symlink")
55	}
56	if err := os.Chmod(dir, dirMode); err != nil {
57		return nil, fmt.Errorf("secret files: chmod base directory: %w", err)
58	}
59	root, err := os.OpenRoot(dir)
60	if err != nil {
61		return nil, fmt.Errorf("secret files: open base directory: %w", err)
62	}
63	return &Store{dir: dir, root: root}, nil
64}
65
66// Dir returns the absolute base directory.
67func (s *Store) Dir() string { return s.dir }
68
69// Close releases the base directory handle.
70func (s *Store) Close() error { return s.root.Close() }
71
72// Write creates the container's directory with one 0444 file per key. The
73// directory must not exist yet: container names are unique per create, so
74// an existing directory belongs to another container and is never touched.
75// Crash leftovers are removed by the sweeps, not here.
76func (s *Store) Write(_ context.Context, app, container string, values map[string]string) (map[string]string, error) {
77	if err := validateSet(app, container); err != nil {
78		return nil, err
79	}
80	for key := range values {
81		if err := domain.ValidateEnvKey(key); err != nil {
82			return nil, fmt.Errorf("secret files: %w", err)
83		}
84	}
85	if err := s.root.MkdirAll(app, dirMode); err != nil {
86		return nil, fmt.Errorf("secret files: create app directory: %w", err)
87	}
88	if err := s.root.Chmod(app, dirMode); err != nil {
89		return nil, fmt.Errorf("secret files: chmod app directory: %w", err)
90	}
91	rel := filepath.Join(app, container)
92	if err := s.root.Mkdir(rel, dirMode); err != nil {
93		return nil, fmt.Errorf("secret files: create container directory: %w", err)
94	}
95	paths, err := s.writeFiles(rel, values)
96	if err != nil {
97		_ = s.root.RemoveAll(rel)
98		return nil, err
99	}
100	return paths, nil
101}
102
103func (s *Store) writeFiles(rel string, values map[string]string) (map[string]string, error) {
104	if err := s.root.Chmod(rel, dirMode); err != nil {
105		return nil, fmt.Errorf("secret files: chmod container directory: %w", err)
106	}
107	paths := make(map[string]string, len(values))
108	for _, key := range slices.Sorted(maps.Keys(values)) {
109		name := filepath.Join(rel, key)
110		if err := s.writeFile(name, values[key]); err != nil {
111			return nil, fmt.Errorf("secret files: write %s: %w", key, err)
112		}
113		paths[key] = filepath.Join(s.dir, name)
114	}
115	return paths, nil
116}
117
118func (s *Store) writeFile(name, value string) error {
119	f, err := s.root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
120	if err != nil {
121		return err
122	}
123	if _, err := f.WriteString(value); err != nil {
124		_ = f.Close()
125		return err
126	}
127	if err := f.Close(); err != nil {
128		return err
129	}
130	return s.root.Chmod(name, fileMode)
131}
132
133// Read returns one file's content.
134func (s *Store) Read(_ context.Context, app, container, key string) (string, error) {
135	if err := validateSet(app, container); err != nil {
136		return "", err
137	}
138	if err := domain.ValidateEnvKey(key); err != nil {
139		return "", fmt.Errorf("secret files: %w", err)
140	}
141	data, err := s.root.ReadFile(filepath.Join(app, container, key))
142	if err != nil {
143		return "", fmt.Errorf("secret files: read %s: %w", key, err)
144	}
145	return string(data), nil
146}
147
148// Remove deletes the container's directory and the app directory when it
149// becomes empty. A missing directory is not an error.
150func (s *Store) Remove(_ context.Context, app, container string) error {
151	if err := validateSet(app, container); err != nil {
152		return err
153	}
154	if err := s.root.RemoveAll(filepath.Join(app, container)); err != nil {
155		return fmt.Errorf("secret files: remove container directory: %w", err)
156	}
157	if err := s.root.Remove(app); err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, syscall.ENOTEMPTY) && !errors.Is(err, syscall.EEXIST) {
158		return fmt.Errorf("secret files: remove app directory: %w", err)
159	}
160	return nil
161}
162
163// List returns every container directory, sorted by app then container.
164func (s *Store) List(_ context.Context) ([]out.SecretFileSet, error) {
165	fsys := s.root.FS()
166	apps, err := fs.ReadDir(fsys, ".")
167	if err != nil {
168		return nil, fmt.Errorf("secret files: list apps: %w", err)
169	}
170	var sets []out.SecretFileSet
171	for _, app := range apps {
172		if !app.IsDir() {
173			continue
174		}
175		containers, err := fs.ReadDir(fsys, app.Name())
176		if err != nil {
177			return nil, fmt.Errorf("secret files: list containers: %w", err)
178		}
179		for _, c := range containers {
180			if c.IsDir() {
181				sets = append(sets, out.SecretFileSet{App: app.Name(), Container: c.Name()})
182			}
183		}
184	}
185	// fs.ReadDir returns entries sorted by name, so sets are already ordered.
186	return sets, nil
187}
188
189func validateSet(app, container string) error {
190	if err := domain.ValidateAppName(app); err != nil {
191		return fmt.Errorf("secret files: %w", err)
192	}
193	if container == "" || len(container) > maxContainerName || container == "." || container == ".." ||
194		filepath.Base(container) != container || strings.ContainsAny(container, "/\\\x00") {
195		return errors.New("secret files: invalid container name")
196	}
197	return nil
198}

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