v0.14.0 815fb98

ci_settings.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6	"errors"
7	"net/url"
8	"strings"
9	"time"
10	"unicode"
11	"unicode/utf8"
12)
13
14// Secrets, variables and environments (ADR 0019, CI-3 plan D2).
15
16// Limits of names and values.
17const (
18	// MaxSettingValueBytes is the largest secret or variable: 48 KB as
19	// docs.github.com writes it, taken as 48 KiB.
20	MaxSettingValueBytes = 48 * 1024
21	// MaxSettingNameLen is the longest name of a secret, variable or
22	// environment, in characters.
23	MaxSettingNameLen = 255
24	// ReservedSettingPrefix starts no secret or variable name: GitHub keeps it
25	// for its own.
26	ReservedSettingPrefix = "GITHUB_"
27)
28
29// ErrSecretsKeyMismatch means the data was sealed with another
30// ERMITE_SECRETS_KEY than the one the server runs with: it refuses to start
31// rather than hand out values it cannot read or seal new ones with a key the
32// old ones do not open with.
33var ErrSecretsKeyMismatch = errors.New("ERMITE_SECRETS_KEY is not the key the stored secrets were sealed with")
34
35// ParseSettingName checks the name of a secret or variable and returns it in
36// upper case, the way it is stored: letters, digits and "_", not starting with
37// a digit or with GITHUB_, 1 to 255 characters, case-insensitive.
38func ParseSettingName(s string) (string, error) {
39	if s == "" || len(s) > MaxSettingNameLen {
40		return "", &TextError{Reason: "must be 1 to 255 characters"}
41	}
42	for i := 0; i < len(s); i++ {
43		c := s[i]
44		switch {
45		case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_':
46		case c >= '0' && c <= '9' && i > 0:
47		default:
48			return "", &TextError{Reason: "may only hold letters, digits and underscores, and must not start with a digit"}
49		}
50	}
51	up := strings.ToUpper(s)
52	if strings.HasPrefix(up, ReservedSettingPrefix) {
53		return "", &TextError{Reason: "must not start with " + ReservedSettingPrefix}
54	}
55	return up, nil
56}
57
58// CheckSecretValue checks the value of a secret: 1 byte to 48 KiB.
59func CheckSecretValue(v string) error {
60	switch {
61	case v == "":
62		return &TextError{Reason: "must not be empty"}
63	case len(v) > MaxSettingValueBytes:
64		return &TextError{Reason: "must be at most 49,152 bytes (48 KiB)"}
65	}
66	return nil
67}
68
69// CheckVariableValue checks the value of a variable: at most 48 KiB, may be
70// empty.
71func CheckVariableValue(v string) error {
72	if len(v) > MaxSettingValueBytes {
73		return &TextError{Reason: "must be at most 49,152 bytes (48 KiB)"}
74	}
75	return nil
76}
77
78// ParseEnvironmentName checks the name of an environment: 1 to 255 characters
79// of valid UTF-8 without control characters or leading and trailing whitespace
80// (the one rule for the API, the CLI and workflows: a name is never trimmed).
81// Names are unique without regard to case; the case given is kept.
82func ParseEnvironmentName(s string) (string, error) {
83	if s == "" || utf8.RuneCountInString(s) > MaxSettingNameLen {
84		return "", &TextError{Reason: "must be 1 to 255 characters"}
85	}
86	if !utf8.ValidString(s) {
87		return "", &TextError{Reason: "must be valid UTF-8"}
88	}
89	first, _ := utf8.DecodeRuneInString(s)
90	last, _ := utf8.DecodeLastRuneInString(s)
91	if unicode.IsSpace(first) || unicode.IsSpace(last) {
92		return "", &TextError{Reason: "must not start or end with whitespace"}
93	}
94	for _, r := range s {
95		if unicode.IsControl(r) || isBidiControl(r) {
96			return "", &TextError{Reason: "must not hold control characters"}
97		}
98	}
99	return s, nil
100}
101
102// SettingsScopeKind tells where secrets and variables live.
103type SettingsScopeKind uint8
104
105// Scope kinds.
106const (
107	// ScopeKindInstance is the whole server.
108	ScopeKindInstance SettingsScopeKind = iota + 1
109	// ScopeKindRepo is one repository.
110	ScopeKindRepo
111	// ScopeKindEnvironment is one environment of a repository.
112	ScopeKindEnvironment
113)
114
115// SettingsScope says where a secret or a variable lives. Build it with
116// InstanceScope, RepoScope or EnvironmentScope; the zero value is invalid.
117type SettingsScope struct {
118	Kind SettingsScopeKind
119	Repo RepoName
120	// Env is the environment name in the case it was given. Scopes compare by
121	// Key, which folds the case.
122	Env string
123}
124
125// InstanceScope is the scope of the whole server.
126func InstanceScope() SettingsScope { return SettingsScope{Kind: ScopeKindInstance} }
127
128// RepoScope is the scope of a repository.
129func RepoScope(repo RepoName) SettingsScope { return SettingsScope{Kind: ScopeKindRepo, Repo: repo} }
130
131// EnvironmentScope is the scope of an environment of a repository; the name
132// must follow ParseEnvironmentName.
133func EnvironmentScope(repo RepoName, env string) SettingsScope {
134	return SettingsScope{Kind: ScopeKindEnvironment, Repo: repo, Env: env}
135}
136
137// Valid reports whether the scope is well formed.
138func (s SettingsScope) Valid() bool {
139	switch s.Kind {
140	case ScopeKindInstance:
141		return true
142	case ScopeKindRepo:
143		return !s.Repo.IsZero()
144	case ScopeKindEnvironment:
145		if s.Repo.IsZero() {
146			return false
147		}
148		_, err := ParseEnvironmentName(s.Env)
149		return err == nil
150	}
151	return false
152}
153
154// Key is the scope as the storage and the encryption name it: "i",
155// "r/<repo>" or "e/<repo>/<environment>". The environment is lower-cased and
156// escaped so that its key holds no "/" and two names that differ by case are
157// one scope.
158func (s SettingsScope) Key() string {
159	switch s.Kind {
160	case ScopeKindInstance:
161		return "i"
162	case ScopeKindRepo:
163		return "r/" + s.Repo.String()
164	case ScopeKindEnvironment:
165		return "e/" + s.Repo.String() + "/" + EnvironmentKey(s.Env)
166	}
167	return ""
168}
169
170// EnvironmentKey is the part of a storage key that names an environment: its
171// name lower-cased and path-escaped.
172func EnvironmentKey(name string) string { return url.PathEscape(strings.ToLower(name)) }
173
174// SecretAAD is the additional data a secret is sealed with. It binds the
175// ciphertext to its scope and name, so a stored value moved to another secret
176// does not open.
177func SecretAAD(scope SettingsScope, name string) []byte {
178	return []byte("ermite-secret\x00" + scope.Key() + "\x00" + name)
179}
180
181// Secret describes a stored secret; it never holds the value.
182type Secret struct {
183	Name      string
184	UpdatedAt time.Time
185}
186
187// Variable is a configuration variable.
188type Variable struct {
189	Name      string
190	Value     string
191	UpdatedAt time.Time
192}
193
194// Environment is a named deployment target of a repository.
195type Environment struct {
196	Repo      RepoName
197	Name      string
198	CreatedAt time.Time
199}

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 2

Tags 44

↑↓ moveenter openesc close