detached ed415c1

fix(lint): reduce standalone service lint complexity

commit
ed415c10221d2ef7293bafeb61c4f1935dcd86a3
author
brice · · 2026-06-30 15:57 UTC
parent
7b54de4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +68 -60
M internal/adapters/out/docker/runtime.go +57 -46
@@ -141,53 +141,11 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
141141 })
142142 log := zerowrap.FromCtx(ctx)
143143
144- // Convert ports to Docker format
145- exposedPorts := make(nat.PortSet)
146- portBindings := make(nat.PortMap)
147-
148- for _, port := range config.Ports {
149- containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
150- exposedPorts[containerPort] = struct{}{}
151-
152- // Bind to random available port on localhost only.
153- // SECURITY: Using 127.0.0.1 prevents direct access from the network,
154- // forcing all traffic through Gordon's reverse proxy where auth,
155- // rate limiting, and security headers are applied.
156- portBindings[containerPort] = []nat.PortBinding{
157- {
158- HostIP: "127.0.0.1",
159- HostPort: "0", // Docker will assign a random available port
160- },
161- }
162- }
163- for _, publish := range config.PortPublishes {
164- if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP {
165- return nil, fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol)
166- }
167- containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol))
168- exposedPorts[containerPort] = struct{}{}
169- portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{
170- HostIP: publish.HostIP,
171- HostPort: strconv.Itoa(publish.HostPort),
172- })
173- }
174-
175- // Convert volumes to Docker format
176- var binds []string
177- if config.Volumes != nil {
178- for containerPath, volumeName := range config.Volumes {
179- bind := fmt.Sprintf("%s:%s", volumeName, containerPath)
180- binds = append(binds, bind)
181- log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount")
182- }
183- }
184- if config.ReadOnlyVolumes != nil {
185- for containerPath, volumeName := range config.ReadOnlyVolumes {
186- bind := fmt.Sprintf("%s:%s:ro", volumeName, containerPath)
187- binds = append(binds, bind)
188- log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding read-only volume mount")
189- }
144+ exposedPorts, portBindings, err := buildPortBindings(config)
145+ if err != nil {
146+ return nil, err
190147 }
148+ binds := buildVolumeBinds(config, log)
191149
192150 // Create container configuration
193151 containerConfig := &container.Config{
@@ -261,6 +219,59 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
261219 return r.InspectContainer(ctx, resp.ID)
262220 }
263221
222+func buildPortBindings(config *domain.ContainerConfig) (nat.PortSet, nat.PortMap, error) {
223+ exposedPorts := make(nat.PortSet)
224+ portBindings := make(nat.PortMap)
225+ addLegacyPortBindings(config.Ports, exposedPorts, portBindings)
226+ if err := addExplicitPortBindings(config.PortPublishes, exposedPorts, portBindings); err != nil {
227+ return nil, nil, err
228+ }
229+ return exposedPorts, portBindings, nil
230+}
231+
232+func addLegacyPortBindings(ports []int, exposedPorts nat.PortSet, portBindings nat.PortMap) {
233+ for _, port := range ports {
234+ containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
235+ exposedPorts[containerPort] = struct{}{}
236+
237+ // Bind to random available port on localhost only.
238+ // SECURITY: Using 127.0.0.1 prevents direct access from the network,
239+ // forcing all traffic through Gordon's reverse proxy where auth,
240+ // rate limiting, and security headers are applied.
241+ portBindings[containerPort] = []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: "0"}}
242+ }
243+}
244+
245+func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts nat.PortSet, portBindings nat.PortMap) error {
246+ for _, publish := range publishes {
247+ if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP {
248+ return fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol)
249+ }
250+ containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol))
251+ exposedPorts[containerPort] = struct{}{}
252+ portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{
253+ HostIP: publish.HostIP,
254+ HostPort: strconv.Itoa(publish.HostPort),
255+ })
256+ }
257+ return nil
258+}
259+
260+func buildVolumeBinds(config *domain.ContainerConfig, log zerowrap.Logger) []string {
261+ binds := make([]string, 0, len(config.Volumes)+len(config.ReadOnlyVolumes))
262+ for containerPath, volumeName := range config.Volumes {
263+ bind := fmt.Sprintf("%s:%s", volumeName, containerPath)
264+ binds = append(binds, bind)
265+ log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount")
266+ }
267+ for containerPath, volumeName := range config.ReadOnlyVolumes {
268+ bind := fmt.Sprintf("%s:%s:ro", volumeName, containerPath)
269+ binds = append(binds, bind)
270+ log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding read-only volume mount")
271+ }
272+ return binds
273+}
274+
264275 // StartContainer starts a container.
265276 func (r *Runtime) StartContainer(ctx context.Context, containerID string) error {
266277 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -141,53 +141,11 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
141 })141 })
142 log := zerowrap.FromCtx(ctx)142 log := zerowrap.FromCtx(ctx)
143 143
144- // Convert ports to Docker format144+ exposedPorts, portBindings, err := buildPortBindings(config)
145- exposedPorts := make(nat.PortSet)145+ if err != nil {
146- portBindings := make(nat.PortMap)146+ return nil, err
147-
148- for _, port := range config.Ports {
149- containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
150- exposedPorts[containerPort] = struct{}{}
151-
152- // Bind to random available port on localhost only.
153- // SECURITY: Using 127.0.0.1 prevents direct access from the network,
154- // forcing all traffic through Gordon's reverse proxy where auth,
155- // rate limiting, and security headers are applied.
156- portBindings[containerPort] = []nat.PortBinding{
157- {
158- HostIP: "127.0.0.1",
159- HostPort: "0", // Docker will assign a random available port
160- },
161- }
162- }
163- for _, publish := range config.PortPublishes {
164- if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP {
165- return nil, fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol)
166- }
167- containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol))
168- exposedPorts[containerPort] = struct{}{}
169- portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{
170- HostIP: publish.HostIP,
171- HostPort: strconv.Itoa(publish.HostPort),
172- })
173- }
174-
175- // Convert volumes to Docker format
176- var binds []string
177- if config.Volumes != nil {
178- for containerPath, volumeName := range config.Volumes {
179- bind := fmt.Sprintf("%s:%s", volumeName, containerPath)
180- binds = append(binds, bind)
181- log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount")
182- }
183- }
184- if config.ReadOnlyVolumes != nil {
185- for containerPath, volumeName := range config.ReadOnlyVolumes {
186- bind := fmt.Sprintf("%s:%s:ro", volumeName, containerPath)
187- binds = append(binds, bind)
188- log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding read-only volume mount")
189- }
190 }147 }
148+ binds := buildVolumeBinds(config, log)
191 149
192 // Create container configuration150 // Create container configuration
193 containerConfig := &container.Config{151 containerConfig := &container.Config{
@@ -261,6 +219,59 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC
261 return r.InspectContainer(ctx, resp.ID)219 return r.InspectContainer(ctx, resp.ID)
262 }220 }
263 221
222+func buildPortBindings(config *domain.ContainerConfig) (nat.PortSet, nat.PortMap, error) {
223+ exposedPorts := make(nat.PortSet)
224+ portBindings := make(nat.PortMap)
225+ addLegacyPortBindings(config.Ports, exposedPorts, portBindings)
226+ if err := addExplicitPortBindings(config.PortPublishes, exposedPorts, portBindings); err != nil {
227+ return nil, nil, err
228+ }
229+ return exposedPorts, portBindings, nil
230+}
231+
232+func addLegacyPortBindings(ports []int, exposedPorts nat.PortSet, portBindings nat.PortMap) {
233+ for _, port := range ports {
234+ containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
235+ exposedPorts[containerPort] = struct{}{}
236+
237+ // Bind to random available port on localhost only.
238+ // SECURITY: Using 127.0.0.1 prevents direct access from the network,
239+ // forcing all traffic through Gordon's reverse proxy where auth,
240+ // rate limiting, and security headers are applied.
241+ portBindings[containerPort] = []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: "0"}}
242+ }
243+}
244+
245+func addExplicitPortBindings(publishes []domain.ContainerPortPublish, exposedPorts nat.PortSet, portBindings nat.PortMap) error {
246+ for _, publish := range publishes {
247+ if publish.Protocol != domain.NetworkProtocolTCP && publish.Protocol != domain.NetworkProtocolUDP {
248+ return fmt.Errorf("unsupported container port publish protocol %q", publish.Protocol)
249+ }
250+ containerPort := nat.Port(fmt.Sprintf("%d/%s", publish.ContainerPort, publish.Protocol))
251+ exposedPorts[containerPort] = struct{}{}
252+ portBindings[containerPort] = append(portBindings[containerPort], nat.PortBinding{
253+ HostIP: publish.HostIP,
254+ HostPort: strconv.Itoa(publish.HostPort),
255+ })
256+ }
257+ return nil
258+}
259+
260+func buildVolumeBinds(config *domain.ContainerConfig, log zerowrap.Logger) []string {
261+ binds := make([]string, 0, len(config.Volumes)+len(config.ReadOnlyVolumes))
262+ for containerPath, volumeName := range config.Volumes {
263+ bind := fmt.Sprintf("%s:%s", volumeName, containerPath)
264+ binds = append(binds, bind)
265+ log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount")
266+ }
267+ for containerPath, volumeName := range config.ReadOnlyVolumes {
268+ bind := fmt.Sprintf("%s:%s:ro", volumeName, containerPath)
269+ binds = append(binds, bind)
270+ log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding read-only volume mount")
271+ }
272+ return binds
273+}
274+
264 // StartContainer starts a container.275 // StartContainer starts a container.
265 func (r *Runtime) StartContainer(ctx context.Context, containerID string) error {276 func (r *Runtime) StartContainer(ctx context.Context, containerID string) error {
266 ctx = zerowrap.CtxWithFields(ctx, map[string]any{277 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
M internal/domain/traffic.go +10 -10
@@ -196,17 +196,9 @@ func ParseTrafficServiceRef(value string) (TrafficServiceRef, error) {
196196 case TrafficServiceRefExternalRoute:
197197 return TrafficServiceRef{Kind: TrafficServiceRefExternalRoute, Domain: rest}, nil
198198 case TrafficServiceRefNetworkService:
199- name, portName, ok := strings.Cut(rest, ":")
200- if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
201- return TrafficServiceRef{}, fmt.Errorf("invalid network service ref %q", value)
202- }
203- return TrafficServiceRef{Kind: TrafficServiceRefNetworkService, Name: name, PortName: portName}, nil
199+ return parseNamedPortServiceRef(value, rest, TrafficServiceRefNetworkService, "network service")
204200 case TrafficServiceRefService:
205- name, portName, ok := strings.Cut(rest, ":")
206- if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
207- return TrafficServiceRef{}, fmt.Errorf("invalid service ref %q", value)
208- }
209- return TrafficServiceRef{Kind: TrafficServiceRefService, Name: name, PortName: portName}, nil
201+ return parseNamedPortServiceRef(value, rest, TrafficServiceRefService, "service")
210202 case TrafficServiceRefStatic:
211203 return TrafficServiceRef{Kind: TrafficServiceRefStatic, Name: rest, Reserved: true}, nil
212204 default:
@@ -214,6 +206,14 @@ func ParseTrafficServiceRef(value string) (TrafficServiceRef, error) {
214206 }
215207 }
216208
209+func parseNamedPortServiceRef(value, rest string, kind TrafficServiceRefKind, label string) (TrafficServiceRef, error) {
210+ name, portName, ok := strings.Cut(rest, ":")
211+ if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
212+ return TrafficServiceRef{}, fmt.Errorf("invalid %s ref %q", label, value)
213+ }
214+ return TrafficServiceRef{Kind: kind, Name: name, PortName: portName}, nil
215+}
216+
217217 func (g TrafficGraph) Validate() error {
218218 if err := validateTrafficOptions(g.Options); err != nil {
219219 return err
@@ -196,17 +196,9 @@ func ParseTrafficServiceRef(value string) (TrafficServiceRef, error) {
196 case TrafficServiceRefExternalRoute:196 case TrafficServiceRefExternalRoute:
197 return TrafficServiceRef{Kind: TrafficServiceRefExternalRoute, Domain: rest}, nil197 return TrafficServiceRef{Kind: TrafficServiceRefExternalRoute, Domain: rest}, nil
198 case TrafficServiceRefNetworkService:198 case TrafficServiceRefNetworkService:
199- name, portName, ok := strings.Cut(rest, ":")199+ return parseNamedPortServiceRef(value, rest, TrafficServiceRefNetworkService, "network service")
200- if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
201- return TrafficServiceRef{}, fmt.Errorf("invalid network service ref %q", value)
202- }
203- return TrafficServiceRef{Kind: TrafficServiceRefNetworkService, Name: name, PortName: portName}, nil
204 case TrafficServiceRefService:200 case TrafficServiceRefService:
205- name, portName, ok := strings.Cut(rest, ":")201+ return parseNamedPortServiceRef(value, rest, TrafficServiceRefService, "service")
206- if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
207- return TrafficServiceRef{}, fmt.Errorf("invalid service ref %q", value)
208- }
209- return TrafficServiceRef{Kind: TrafficServiceRefService, Name: name, PortName: portName}, nil
210 case TrafficServiceRefStatic:202 case TrafficServiceRefStatic:
211 return TrafficServiceRef{Kind: TrafficServiceRefStatic, Name: rest, Reserved: true}, nil203 return TrafficServiceRef{Kind: TrafficServiceRefStatic, Name: rest, Reserved: true}, nil
212 default:204 default:
@@ -214,6 +206,14 @@ func ParseTrafficServiceRef(value string) (TrafficServiceRef, error) {
214 }206 }
215 }207 }
216 208
209+func parseNamedPortServiceRef(value, rest string, kind TrafficServiceRefKind, label string) (TrafficServiceRef, error) {
210+ name, portName, ok := strings.Cut(rest, ":")
211+ if !ok || name == "" || portName == "" || strings.Contains(portName, ":") {
212+ return TrafficServiceRef{}, fmt.Errorf("invalid %s ref %q", label, value)
213+ }
214+ return TrafficServiceRef{Kind: kind, Name: name, PortName: portName}, nil
215+}
216+
217 func (g TrafficGraph) Validate() error {217 func (g TrafficGraph) Validate() error {
218 if err := validateTrafficOptions(g.Options); err != nil {218 if err := validateTrafficOptions(g.Options); err != nil {
219 return err219 return err
M internal/usecase/services/service.go +1 -4
@@ -6,7 +6,6 @@ import (
66 "crypto/sha256"
77 "encoding/hex"
88 "encoding/json"
9- "errors"
109 "fmt"
1110 "io"
1211 "maps"
@@ -506,9 +505,7 @@ func (s *Service) waitLogReadiness(ctx context.Context, containerID string, svc
506505 if err == nil && found {
507506 return nil
508507 }
509- if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
510- // Continue polling until timeout; many services create the log after start.
511- }
508+ // Continue polling until timeout; many services create the log after start.
512509 select {
513510 case <-ctx.Done():
514511 return fmt.Errorf("standalone service %q log readiness timed out: %w", svc.Name, ctx.Err())
@@ -6,7 +6,6 @@ import (
6 "crypto/sha256"6 "crypto/sha256"
7 "encoding/hex"7 "encoding/hex"
8 "encoding/json"8 "encoding/json"
9- "errors"
10 "fmt"9 "fmt"
11 "io"10 "io"
12 "maps"11 "maps"
@@ -506,9 +505,7 @@ func (s *Service) waitLogReadiness(ctx context.Context, containerID string, svc
506 if err == nil && found {505 if err == nil && found {
507 return nil506 return nil
508 }507 }
509- if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {508+ // Continue polling until timeout; many services create the log after start.
510- // Continue polling until timeout; many services create the log after start.
511- }
512 select {509 select {
513 case <-ctx.Done():510 case <-ctx.Done():
514 return fmt.Errorf("standalone service %q log readiness timed out: %w", svc.Name, ctx.Err())511 return fmt.Errorf("standalone service %q log readiness timed out: %w", svc.Name, ctx.Err())

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