service.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"net"
6	"path"
7	"strconv"
8	"strings"
9	"time"
10)
11
12const (
13	StandaloneServiceReadinessNone = "none"
14	StandaloneServiceReadinessTCP  = "tcp"
15	StandaloneServiceReadinessLog  = "log"
16)
17
18type StandaloneService struct {
19	Name      string
20	Image     string
21	Enabled   bool
22	Env       []string
23	EnvFile   string
24	Secrets   []StandaloneServiceSecretRef
25	Readiness StandaloneServiceReadiness
26	Cleanup   StandaloneServiceCleanup
27	Ports     []StandaloneServicePort
28	Volumes   []StandaloneServiceVolume
29}
30
31type StandaloneServiceStatus struct {
32	Name          string
33	ContainerID   string
34	ContainerName string
35	Status        ContainerStatus
36	ConfigHash    string
37}
38
39type StandaloneServicePort struct {
40	Name         string
41	Container    int
42	Protocol     NetworkProtocol
43	Publish      string
44	Private      bool
45	Public       bool
46	TrustedCIDRs []string
47}
48
49type StandaloneServiceVolume struct {
50	Source   string
51	Target   string
52	ReadOnly bool
53}
54
55type StandaloneServiceSecretRef struct {
56	Name string
57	Key  string
58}
59
60type StandaloneServiceReadiness struct {
61	Type       string
62	Path       string
63	Contains   string
64	Timeout    time.Duration
65	TimeoutSet bool
66}
67
68type StandaloneServiceCleanup struct {
69	PreserveVolumes bool
70	RemoveContainer bool
71}
72
73func (s StandaloneService) WithDefaults() StandaloneService {
74	if s.Readiness.Type == "" {
75		s.Readiness.Type = StandaloneServiceReadinessNone
76	}
77	s.Cleanup = s.Cleanup.WithDefaults()
78	return s
79}
80
81func (c StandaloneServiceCleanup) WithDefaults() StandaloneServiceCleanup {
82	if !c.PreserveVolumes && !c.RemoveContainer {
83		return StandaloneServiceCleanup{PreserveVolumes: true, RemoveContainer: true}
84	}
85	return c
86}
87
88func (s StandaloneService) Validate() error {
89	s = s.WithDefaults()
90	name := strings.TrimSpace(s.Name)
91	if name == "" {
92		return fmt.Errorf("standalone service name is required")
93	}
94	if s.Name != name {
95		return fmt.Errorf("standalone service name %q must not include leading or trailing whitespace", s.Name)
96	}
97	if s.Enabled && strings.TrimSpace(s.Image) == "" {
98		return fmt.Errorf("standalone service %q image is required when enabled", s.Name)
99	}
100	if s.EnvFile != "" && strings.TrimSpace(s.EnvFile) == "" {
101		return fmt.Errorf("standalone service %q env_file must be non-empty when set", s.Name)
102	}
103	if err := validateStandaloneServicePorts(s); err != nil {
104		return err
105	}
106	if err := validateStandaloneServiceVolumes(s); err != nil {
107		return err
108	}
109	if err := validateStandaloneServiceSecrets(s); err != nil {
110		return err
111	}
112	if err := validateStandaloneServiceReadiness(s); err != nil {
113		return err
114	}
115	return nil
116}
117
118func validateStandaloneServicePorts(s StandaloneService) error {
119	seen := make(map[string]struct{}, len(s.Ports))
120	for i, port := range s.Ports {
121		name, err := validateStandaloneServicePort(s.Name, i, port)
122		if err != nil {
123			return err
124		}
125		if _, ok := seen[name]; ok {
126			return fmt.Errorf("standalone service %q duplicate port name %q", s.Name, name)
127		}
128		seen[name] = struct{}{}
129	}
130	return nil
131}
132
133func validateStandaloneServicePort(serviceName string, index int, port StandaloneServicePort) (string, error) {
134	name := strings.TrimSpace(port.Name)
135	if name == "" {
136		return "", fmt.Errorf("standalone service %q port %d name is required", serviceName, index)
137	}
138	if port.Name != name {
139		return "", fmt.Errorf("standalone service %q port %q name must not include leading or trailing whitespace", serviceName, port.Name)
140	}
141	if port.Container <= 0 || port.Container > 65535 {
142		return "", fmt.Errorf("standalone service %q port %q container port must be 1-65535", serviceName, name)
143	}
144	if port.Protocol != NetworkProtocolTCP && port.Protocol != NetworkProtocolUDP {
145		return "", fmt.Errorf("standalone service %q port %q protocol must be tcp or udp", serviceName, name)
146	}
147	if port.Private && port.Public {
148		return "", fmt.Errorf("standalone service %q port %q cannot be both private and public", serviceName, name)
149	}
150	if err := validateStandaloneServicePublish(serviceName, name, port.Publish); err != nil {
151		return "", err
152	}
153	if port.Private && port.Publish != "" {
154		if err := validatePrivateStandaloneServicePublish(serviceName, name, port.Publish); err != nil {
155			return "", err
156		}
157	}
158	if err := validateStandaloneServiceTrustedCIDRs(serviceName, name, port.TrustedCIDRs); err != nil {
159		return "", err
160	}
161	return name, nil
162}
163
164func validateStandaloneServiceTrustedCIDRs(serviceName, portName string, cidrs []string) error {
165	for _, cidr := range cidrs {
166		if strings.TrimSpace(cidr) == "" {
167			return fmt.Errorf("standalone service %q port %q trusted_cidrs must not contain empty values", serviceName, portName)
168		}
169		if _, _, err := net.ParseCIDR(cidr); err != nil {
170			return fmt.Errorf("standalone service %q port %q trusted_cidr %q is invalid: %w", serviceName, portName, cidr, err)
171		}
172	}
173	return nil
174}
175
176func validateStandaloneServicePublish(serviceName, portName, publish string) error {
177	if publish == "" {
178		return nil
179	}
180	host, port, err := net.SplitHostPort(publish)
181	if err != nil {
182		return fmt.Errorf("standalone service %q port %q publish address %q is invalid: %w", serviceName, portName, publish, err)
183	}
184	if strings.TrimSpace(host) == "" || strings.TrimSpace(port) == "" {
185		return fmt.Errorf("standalone service %q port %q publish address %q must include host and port", serviceName, portName, publish)
186	}
187	portNumber, err := strconv.Atoi(port)
188	if err != nil || portNumber < 1 || portNumber > 65535 {
189		return fmt.Errorf("standalone service %q port %q publish address %q must include a valid port", serviceName, portName, publish)
190	}
191	return nil
192}
193
194func validatePrivateStandaloneServicePublish(serviceName, portName, publish string) error {
195	host, _, err := net.SplitHostPort(publish)
196	if err != nil {
197		return err
198	}
199	ip := net.ParseIP(host)
200	if ip == nil || !ip.IsLoopback() {
201		return fmt.Errorf("standalone service %q private port %q publish host %q must be loopback", serviceName, portName, host)
202	}
203	return nil
204}
205
206func validateStandaloneServiceVolumes(s StandaloneService) error {
207	seenTargets := make(map[string]struct{}, len(s.Volumes))
208	for i, volume := range s.Volumes {
209		target := strings.TrimSpace(volume.Target)
210		if target == "" {
211			return fmt.Errorf("standalone service %q volume %d target is required", s.Name, i)
212		}
213		if !path.IsAbs(target) {
214			return fmt.Errorf("standalone service %q volume %d target %q must be an absolute container path", s.Name, i, volume.Target)
215		}
216		if _, ok := seenTargets[target]; ok {
217			return fmt.Errorf("standalone service %q duplicate volume target %q", s.Name, target)
218		}
219		seenTargets[target] = struct{}{}
220	}
221	return nil
222}
223
224func validateStandaloneServiceSecrets(s StandaloneService) error {
225	for i, secret := range s.Secrets {
226		if strings.TrimSpace(secret.Name) == "" {
227			return fmt.Errorf("standalone service %q secret %d name is required", s.Name, i)
228		}
229		if strings.TrimSpace(secret.Key) == "" {
230			return fmt.Errorf("standalone service %q secret %d key is required", s.Name, i)
231		}
232	}
233	return nil
234}
235
236func validateStandaloneServiceReadiness(s StandaloneService) error {
237	readinessType := s.Readiness.Type
238	if readinessType == "" {
239		readinessType = StandaloneServiceReadinessNone
240	}
241	if s.Readiness.TimeoutSet && s.Readiness.Timeout <= 0 {
242		return fmt.Errorf("standalone service %q readiness timeout must be positive when set", s.Name)
243	}
244	switch readinessType {
245	case StandaloneServiceReadinessNone, StandaloneServiceReadinessTCP:
246		return nil
247	case StandaloneServiceReadinessLog:
248		if strings.TrimSpace(s.Readiness.Path) == "" {
249			return fmt.Errorf("standalone service %q log readiness path is required", s.Name)
250		}
251		if strings.TrimSpace(s.Readiness.Contains) == "" {
252			return fmt.Errorf("standalone service %q log readiness contains is required", s.Name)
253		}
254		return nil
255	default:
256		return fmt.Errorf("standalone service %q readiness type must be none, tcp, or log", s.Name)
257	}
258}

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