detached 6e78011

fix(security): harden reviewed remediation paths

commit
6e7801172120a364fb130bf617a09eb2b4a0ea5b
author
brice · · 2026-08-10 21:03 UTC
parent
271c36a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +77 -48
M .github/actions/deploy/README.md +10 -10
@@ -27,10 +27,10 @@ jobs:
2727 deploy:
2828 runs-on: ubuntu-latest
2929 steps:
30- - uses: actions/checkout@v6
30+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
3131
3232 - name: Deploy to Gordon
33- uses: bnema/gordon/.github/actions/deploy@main
33+ uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
3434 with:
3535 registry: registry.mydomain.com
3636 username: ${{ secrets.GORDON_USERNAME }}
@@ -52,11 +52,11 @@ jobs:
5252 deploy:
5353 runs-on: ubuntu-latest
5454 steps:
55- - uses: actions/checkout@v6
55+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
5656
5757 - name: Deploy to Gordon
5858 id: deploy
59- uses: bnema/gordon/.github/actions/deploy@main
59+ uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
6060 with:
6161 registry: registry.mydomain.com
6262 username: ${{ secrets.GORDON_USERNAME }}
@@ -148,11 +148,11 @@ jobs:
148148 deploy:
149149 runs-on: ubuntu-latest
150150 steps:
151- - uses: actions/checkout@v6
151+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
152152 with:
153153 ref: ${{ github.event.inputs.tag || github.ref }}
154154
155- - uses: bnema/gordon/.github/actions/deploy@main
155+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
156156 with:
157157 registry: registry.mydomain.com
158158 username: ${{ secrets.GORDON_USERNAME }}
@@ -174,8 +174,8 @@ jobs:
174174 deploy-api:
175175 runs-on: ubuntu-latest
176176 steps:
177- - uses: actions/checkout@v6
178- - uses: bnema/gordon/.github/actions/deploy@main
177+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
178+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
179179 with:
180180 registry: registry.mydomain.com
181181 username: ${{ secrets.GORDON_USERNAME }}
@@ -187,8 +187,8 @@ jobs:
187187 deploy-web:
188188 runs-on: ubuntu-latest
189189 steps:
190- - uses: actions/checkout@v6
191- - uses: bnema/gordon/.github/actions/deploy@main
190+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
191+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
192192 with:
193193 registry: registry.mydomain.com
194194 username: ${{ secrets.GORDON_USERNAME }}
@@ -27,10 +27,10 @@ jobs:
27 deploy:27 deploy:
28 runs-on: ubuntu-latest28 runs-on: ubuntu-latest
29 steps:29 steps:
30- - uses: actions/checkout@v630+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
31 31
32 - name: Deploy to Gordon32 - name: Deploy to Gordon
33- uses: bnema/gordon/.github/actions/deploy@main33+ uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
34 with:34 with:
35 registry: registry.mydomain.com35 registry: registry.mydomain.com
36 username: ${{ secrets.GORDON_USERNAME }}36 username: ${{ secrets.GORDON_USERNAME }}
@@ -52,11 +52,11 @@ jobs:
52 deploy:52 deploy:
53 runs-on: ubuntu-latest53 runs-on: ubuntu-latest
54 steps:54 steps:
55- - uses: actions/checkout@v655+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
56 56
57 - name: Deploy to Gordon57 - name: Deploy to Gordon
58 id: deploy58 id: deploy
59- uses: bnema/gordon/.github/actions/deploy@main59+ uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
60 with:60 with:
61 registry: registry.mydomain.com61 registry: registry.mydomain.com
62 username: ${{ secrets.GORDON_USERNAME }}62 username: ${{ secrets.GORDON_USERNAME }}
@@ -148,11 +148,11 @@ jobs:
148 deploy:148 deploy:
149 runs-on: ubuntu-latest149 runs-on: ubuntu-latest
150 steps:150 steps:
151- - uses: actions/checkout@v6151+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
152 with:152 with:
153 ref: ${{ github.event.inputs.tag || github.ref }}153 ref: ${{ github.event.inputs.tag || github.ref }}
154 154
155- - uses: bnema/gordon/.github/actions/deploy@main155+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
156 with:156 with:
157 registry: registry.mydomain.com157 registry: registry.mydomain.com
158 username: ${{ secrets.GORDON_USERNAME }}158 username: ${{ secrets.GORDON_USERNAME }}
@@ -174,8 +174,8 @@ jobs:
174 deploy-api:174 deploy-api:
175 runs-on: ubuntu-latest175 runs-on: ubuntu-latest
176 steps:176 steps:
177- - uses: actions/checkout@v6177+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
178- - uses: bnema/gordon/.github/actions/deploy@main178+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
179 with:179 with:
180 registry: registry.mydomain.com180 registry: registry.mydomain.com
181 username: ${{ secrets.GORDON_USERNAME }}181 username: ${{ secrets.GORDON_USERNAME }}
@@ -187,8 +187,8 @@ jobs:
187 deploy-web:187 deploy-web:
188 runs-on: ubuntu-latest188 runs-on: ubuntu-latest
189 steps:189 steps:
190- - uses: actions/checkout@v6190+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
191- - uses: bnema/gordon/.github/actions/deploy@main191+ - uses: bnema/gordon/.github/actions/deploy@271c36a4cc32fabd91b20f93cd006bdd2a01b181
192 with:192 with:
193 registry: registry.mydomain.com193 registry: registry.mydomain.com
194 username: ${{ secrets.GORDON_USERNAME }}194 username: ${{ secrets.GORDON_USERNAME }}
M docs/installation.md +8 -4
@@ -33,7 +33,8 @@ Manual archive installation requires downloading the matching `checksums.txt` re
3333
3434 **Linux (x86_64)**
3535 ```bash
36-wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz
36+wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_amd64.tar.gz,checksums.txt}
37+grep ' gordon_linux_amd64.tar.gz$' checksums.txt | sha256sum --check --strict
3738 tar -xzf gordon_linux_amd64.tar.gz
3839 chmod +x gordon
3940 sudo mv gordon /usr/local/bin/
@@ -41,7 +42,8 @@ sudo mv gordon /usr/local/bin/
4142
4243 **Linux (ARM64)** - for Raspberry Pi 4, AWS Graviton, Oracle Ampere, etc.
4344 ```bash
44-wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_arm64.tar.gz
45+wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_arm64.tar.gz,checksums.txt}
46+grep ' gordon_linux_arm64.tar.gz$' checksums.txt | sha256sum --check --strict
4547 tar -xzf gordon_linux_arm64.tar.gz
4648 chmod +x gordon
4749 sudo mv gordon /usr/local/bin/
@@ -49,7 +51,8 @@ sudo mv gordon /usr/local/bin/
4951
5052 **macOS (Apple Silicon)**
5153 ```bash
52-curl -LO https://github.com/bnema/gordon/releases/latest/download/gordon_darwin_arm64.tar.gz
54+curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_arm64.tar.gz,checksums.txt}
55+grep ' gordon_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 --check
5356 tar -xzf gordon_darwin_arm64.tar.gz
5457 chmod +x gordon
5558 sudo mv gordon /usr/local/bin/
@@ -57,7 +60,8 @@ sudo mv gordon /usr/local/bin/
5760
5861 **macOS (Intel)**
5962 ```bash
60-curl -LO https://github.com/bnema/gordon/releases/latest/download/gordon_darwin_amd64.tar.gz
63+curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_amd64.tar.gz,checksums.txt}
64+grep ' gordon_darwin_amd64.tar.gz$' checksums.txt | shasum -a 256 --check
6165 tar -xzf gordon_darwin_amd64.tar.gz
6266 chmod +x gordon
6367 sudo mv gordon /usr/local/bin/
@@ -33,7 +33,8 @@ Manual archive installation requires downloading the matching `checksums.txt` re
33 33
34 **Linux (x86_64)**34 **Linux (x86_64)**
35 ```bash35 ```bash
36-wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz36+wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_amd64.tar.gz,checksums.txt}
37+grep ' gordon_linux_amd64.tar.gz$' checksums.txt | sha256sum --check --strict
37 tar -xzf gordon_linux_amd64.tar.gz38 tar -xzf gordon_linux_amd64.tar.gz
38 chmod +x gordon39 chmod +x gordon
39 sudo mv gordon /usr/local/bin/40 sudo mv gordon /usr/local/bin/
@@ -41,7 +42,8 @@ sudo mv gordon /usr/local/bin/
41 42
42 **Linux (ARM64)** - for Raspberry Pi 4, AWS Graviton, Oracle Ampere, etc.43 **Linux (ARM64)** - for Raspberry Pi 4, AWS Graviton, Oracle Ampere, etc.
43 ```bash44 ```bash
44-wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_arm64.tar.gz45+wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_arm64.tar.gz,checksums.txt}
46+grep ' gordon_linux_arm64.tar.gz$' checksums.txt | sha256sum --check --strict
45 tar -xzf gordon_linux_arm64.tar.gz47 tar -xzf gordon_linux_arm64.tar.gz
46 chmod +x gordon48 chmod +x gordon
47 sudo mv gordon /usr/local/bin/49 sudo mv gordon /usr/local/bin/
@@ -49,7 +51,8 @@ sudo mv gordon /usr/local/bin/
49 51
50 **macOS (Apple Silicon)**52 **macOS (Apple Silicon)**
51 ```bash53 ```bash
52-curl -LO https://github.com/bnema/gordon/releases/latest/download/gordon_darwin_arm64.tar.gz54+curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_arm64.tar.gz,checksums.txt}
55+grep ' gordon_darwin_arm64.tar.gz$' checksums.txt | shasum -a 256 --check
53 tar -xzf gordon_darwin_arm64.tar.gz56 tar -xzf gordon_darwin_arm64.tar.gz
54 chmod +x gordon57 chmod +x gordon
55 sudo mv gordon /usr/local/bin/58 sudo mv gordon /usr/local/bin/
@@ -57,7 +60,8 @@ sudo mv gordon /usr/local/bin/
57 60
58 **macOS (Intel)**61 **macOS (Intel)**
59 ```bash62 ```bash
60-curl -LO https://github.com/bnema/gordon/releases/latest/download/gordon_darwin_amd64.tar.gz63+curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_amd64.tar.gz,checksums.txt}
64+grep ' gordon_darwin_amd64.tar.gz$' checksums.txt | shasum -a 256 --check
61 tar -xzf gordon_darwin_amd64.tar.gz65 tar -xzf gordon_darwin_amd64.tar.gz
62 chmod +x gordon66 chmod +x gordon
63 sudo mv gordon /usr/local/bin/67 sudo mv gordon /usr/local/bin/
M internal/adapters/in/cli/secret_input.go +18 -4
@@ -2,14 +2,28 @@ package cli
22
33 import (
44 "fmt"
5+ "io"
56 "os"
67 "strings"
8+
9+ "golang.org/x/sys/unix"
710 )
811
912 func readProtectedSecretFile(path string) (string, error) {
10- info, err := os.Stat(path)
13+ fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
14+ if err != nil {
15+ return "", fmt.Errorf("open secret file %s: %w", path, err)
16+ }
17+ file := os.NewFile(uintptr(fd), path)
18+ if file == nil {
19+ _ = unix.Close(fd)
20+ return "", fmt.Errorf("open secret file %s: invalid file descriptor", path)
21+ }
22+ defer file.Close()
23+
24+ info, err := file.Stat()
1125 if err != nil {
12- return "", err
26+ return "", fmt.Errorf("stat secret file %s: %w", path, err)
1327 }
1428 if !info.Mode().IsRegular() {
1529 return "", fmt.Errorf("%s is not a regular file", path)
@@ -17,9 +31,9 @@ func readProtectedSecretFile(path string) (string, error) {
1731 if info.Mode().Perm()&0o077 != 0 {
1832 return "", fmt.Errorf("%s must not be accessible by group or others (use chmod 600)", path)
1933 }
20- data, err := os.ReadFile(path)
34+ data, err := io.ReadAll(file)
2135 if err != nil {
22- return "", err
36+ return "", fmt.Errorf("read secret file %s: %w", path, err)
2337 }
2438 value := strings.TrimSpace(string(data))
2539 if value == "" {
@@ -2,14 +2,28 @@ package cli
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "io"
5 "os"6 "os"
6 "strings"7 "strings"
8+
9+ "golang.org/x/sys/unix"
7 )10 )
8 11
9 func readProtectedSecretFile(path string) (string, error) {12 func readProtectedSecretFile(path string) (string, error) {
10- info, err := os.Stat(path)13+ fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
14+ if err != nil {
15+ return "", fmt.Errorf("open secret file %s: %w", path, err)
16+ }
17+ file := os.NewFile(uintptr(fd), path)
18+ if file == nil {
19+ _ = unix.Close(fd)
20+ return "", fmt.Errorf("open secret file %s: invalid file descriptor", path)
21+ }
22+ defer file.Close()
23+
24+ info, err := file.Stat()
11 if err != nil {25 if err != nil {
12- return "", err26+ return "", fmt.Errorf("stat secret file %s: %w", path, err)
13 }27 }
14 if !info.Mode().IsRegular() {28 if !info.Mode().IsRegular() {
15 return "", fmt.Errorf("%s is not a regular file", path)29 return "", fmt.Errorf("%s is not a regular file", path)
@@ -17,9 +31,9 @@ func readProtectedSecretFile(path string) (string, error) {
17 if info.Mode().Perm()&0o077 != 0 {31 if info.Mode().Perm()&0o077 != 0 {
18 return "", fmt.Errorf("%s must not be accessible by group or others (use chmod 600)", path)32 return "", fmt.Errorf("%s must not be accessible by group or others (use chmod 600)", path)
19 }33 }
20- data, err := os.ReadFile(path)34+ data, err := io.ReadAll(file)
21 if err != nil {35 if err != nil {
22- return "", err36+ return "", fmt.Errorf("read secret file %s: %w", path, err)
23 }37 }
24 value := strings.TrimSpace(string(data))38 value := strings.TrimSpace(string(data))
25 if value == "" {39 if value == "" {
M internal/adapters/out/ratelimit/memory.go +28 -23
@@ -2,6 +2,7 @@
22 package ratelimit
33
44 import (
5+ "container/list"
56 "context"
67 "sync"
78 "time"
@@ -18,6 +19,7 @@ var _ out.RateLimiter = (*MemoryStore)(nil)
1819 // MemoryStore is an in-memory rate limiter implementation using golang.org/x/time/rate.
1920 // Each unique key gets its own independent rate limiter.
2021 type limiterEntry struct {
22+ key string
2123 limiter *rate.Limiter
2224 lastSeen time.Time
2325 }
@@ -28,8 +30,9 @@ const (
2830 )
2931
3032 type MemoryStore struct {
31- limiters map[string]limiterEntry
32- mu sync.RWMutex
33+ limiters map[string]*list.Element
34+ lru *list.List
35+ mu sync.Mutex
3336 rps float64
3437 burst int
3538 log zerowrap.Logger
@@ -38,7 +41,8 @@ type MemoryStore struct {
3841 // NewMemoryStore creates a new in-memory rate limiter store.
3942 func NewMemoryStore(rps float64, burst int, log zerowrap.Logger) *MemoryStore {
4043 return &MemoryStore{
41- limiters: make(map[string]limiterEntry),
44+ limiters: make(map[string]*list.Element),
45+ lru: list.New(),
4246 rps: rps,
4347 burst: burst,
4448 log: log,
@@ -62,39 +66,40 @@ func (s *MemoryStore) getLimiter(key string) *rate.Limiter {
6266 s.mu.Lock()
6367 defer s.mu.Unlock()
6468
65- if entry, exists := s.limiters[key]; exists {
69+ if element, exists := s.limiters[key]; exists {
70+ entry := element.Value.(*limiterEntry)
6671 entry.lastSeen = now
67- s.limiters[key] = entry
72+ s.lru.MoveToFront(element)
6873 return entry.limiter
6974 }
7075
7176 s.evictExpiredOrOldest(now)
7277 limiter := rate.NewLimiter(rate.Limit(s.rps), s.burst)
73- s.limiters[key] = limiterEntry{limiter: limiter, lastSeen: now}
78+ entry := &limiterEntry{key: key, limiter: limiter, lastSeen: now}
79+ s.limiters[key] = s.lru.PushFront(entry)
7480 return limiter
7581 }
7682
7783 func (s *MemoryStore) evictExpiredOrOldest(now time.Time) {
78- for key, entry := range s.limiters {
79- if now.Sub(entry.lastSeen) > limiterIdleTTL {
80- delete(s.limiters, key)
84+ for element := s.lru.Back(); element != nil; {
85+ entry := element.Value.(*limiterEntry)
86+ if now.Sub(entry.lastSeen) <= limiterIdleTTL {
87+ break
8188 }
89+ previous := element.Prev()
90+ s.removeElement(element)
91+ element = previous
8292 }
83- if len(s.limiters) < maxLimiterEntries {
84- return
93+ if len(s.limiters) >= maxLimiterEntries {
94+ s.removeElement(s.lru.Back())
8595 }
96+}
8697
87- var oldestKey string
88- var oldest time.Time
89- found := false
90- for key, entry := range s.limiters {
91- if !found || entry.lastSeen.Before(oldest) {
92- oldestKey = key
93- oldest = entry.lastSeen
94- found = true
95- }
96- }
97- if found {
98- delete(s.limiters, oldestKey)
98+func (s *MemoryStore) removeElement(element *list.Element) {
99+ if element == nil {
100+ return
99101 }
102+ entry := element.Value.(*limiterEntry)
103+ delete(s.limiters, entry.key)
104+ s.lru.Remove(element)
100105 }
@@ -2,6 +2,7 @@
2 package ratelimit2 package ratelimit
3 3
4 import (4 import (
5+ "container/list"
5 "context"6 "context"
6 "sync"7 "sync"
7 "time"8 "time"
@@ -18,6 +19,7 @@ var _ out.RateLimiter = (*MemoryStore)(nil)
18 // MemoryStore is an in-memory rate limiter implementation using golang.org/x/time/rate.19 // MemoryStore is an in-memory rate limiter implementation using golang.org/x/time/rate.
19 // Each unique key gets its own independent rate limiter.20 // Each unique key gets its own independent rate limiter.
20 type limiterEntry struct {21 type limiterEntry struct {
22+ key string
21 limiter *rate.Limiter23 limiter *rate.Limiter
22 lastSeen time.Time24 lastSeen time.Time
23 }25 }
@@ -28,8 +30,9 @@ const (
28 )30 )
29 31
30 type MemoryStore struct {32 type MemoryStore struct {
31- limiters map[string]limiterEntry33+ limiters map[string]*list.Element
32- mu sync.RWMutex34+ lru *list.List
35+ mu sync.Mutex
33 rps float6436 rps float64
34 burst int37 burst int
35 log zerowrap.Logger38 log zerowrap.Logger
@@ -38,7 +41,8 @@ type MemoryStore struct {
38 // NewMemoryStore creates a new in-memory rate limiter store.41 // NewMemoryStore creates a new in-memory rate limiter store.
39 func NewMemoryStore(rps float64, burst int, log zerowrap.Logger) *MemoryStore {42 func NewMemoryStore(rps float64, burst int, log zerowrap.Logger) *MemoryStore {
40 return &MemoryStore{43 return &MemoryStore{
41- limiters: make(map[string]limiterEntry),44+ limiters: make(map[string]*list.Element),
45+ lru: list.New(),
42 rps: rps,46 rps: rps,
43 burst: burst,47 burst: burst,
44 log: log,48 log: log,
@@ -62,39 +66,40 @@ func (s *MemoryStore) getLimiter(key string) *rate.Limiter {
62 s.mu.Lock()66 s.mu.Lock()
63 defer s.mu.Unlock()67 defer s.mu.Unlock()
64 68
65- if entry, exists := s.limiters[key]; exists {69+ if element, exists := s.limiters[key]; exists {
70+ entry := element.Value.(*limiterEntry)
66 entry.lastSeen = now71 entry.lastSeen = now
67- s.limiters[key] = entry72+ s.lru.MoveToFront(element)
68 return entry.limiter73 return entry.limiter
69 }74 }
70 75
71 s.evictExpiredOrOldest(now)76 s.evictExpiredOrOldest(now)
72 limiter := rate.NewLimiter(rate.Limit(s.rps), s.burst)77 limiter := rate.NewLimiter(rate.Limit(s.rps), s.burst)
73- s.limiters[key] = limiterEntry{limiter: limiter, lastSeen: now}78+ entry := &limiterEntry{key: key, limiter: limiter, lastSeen: now}
79+ s.limiters[key] = s.lru.PushFront(entry)
74 return limiter80 return limiter
75 }81 }
76 82
77 func (s *MemoryStore) evictExpiredOrOldest(now time.Time) {83 func (s *MemoryStore) evictExpiredOrOldest(now time.Time) {
78- for key, entry := range s.limiters {84+ for element := s.lru.Back(); element != nil; {
79- if now.Sub(entry.lastSeen) > limiterIdleTTL {85+ entry := element.Value.(*limiterEntry)
80- delete(s.limiters, key)86+ if now.Sub(entry.lastSeen) <= limiterIdleTTL {
87+ break
81 }88 }
89+ previous := element.Prev()
90+ s.removeElement(element)
91+ element = previous
82 }92 }
83- if len(s.limiters) < maxLimiterEntries {93+ if len(s.limiters) >= maxLimiterEntries {
84- return94+ s.removeElement(s.lru.Back())
85 }95 }
96+}
86 97
87- var oldestKey string98+func (s *MemoryStore) removeElement(element *list.Element) {
88- var oldest time.Time99+ if element == nil {
89- found := false100+ return
90- for key, entry := range s.limiters {
91- if !found || entry.lastSeen.Before(oldest) {
92- oldestKey = key
93- oldest = entry.lastSeen
94- found = true
95- }
96- }
97- if found {
98- delete(s.limiters, oldestKey)
99 }101 }
102+ entry := element.Value.(*limiterEntry)
103+ delete(s.limiters, entry.key)
104+ s.lru.Remove(element)
100 }105 }
M internal/usecase/auto/preview/handler.go +2 -3
@@ -79,11 +79,10 @@ func (h *AutoPreviewHandler) Handle(ctx context.Context, event domain.Event) err
7979 // semaphore prevents a push burst from creating an unbounded goroutine backlog.
8080 select {
8181 case h.jobs <- struct{}{}:
82+ case <-ctx.Done():
83+ return fmt.Errorf("schedule preview %s for %s: %w", previewName, baseRoute.Domain, ctx.Err())
8284 case <-h.serviceCtx.Done():
8385 return h.serviceCtx.Err()
84- default:
85- log.Warn().Str("preview", previewName).Str("base_route", baseRoute.Domain).Msg("preview worker queue is full, skipping event")
86- continue
8786 }
8887 go func(baseRoute baseRouteInfo, previewDomain string) {
8988 defer func() { <-h.jobs }()
@@ -79,11 +79,10 @@ func (h *AutoPreviewHandler) Handle(ctx context.Context, event domain.Event) err
79 // semaphore prevents a push burst from creating an unbounded goroutine backlog.79 // semaphore prevents a push burst from creating an unbounded goroutine backlog.
80 select {80 select {
81 case h.jobs <- struct{}{}:81 case h.jobs <- struct{}{}:
82+ case <-ctx.Done():
83+ return fmt.Errorf("schedule preview %s for %s: %w", previewName, baseRoute.Domain, ctx.Err())
82 case <-h.serviceCtx.Done():84 case <-h.serviceCtx.Done():
83 return h.serviceCtx.Err()85 return h.serviceCtx.Err()
84- default:
85- log.Warn().Str("preview", previewName).Str("base_route", baseRoute.Domain).Msg("preview worker queue is full, skipping event")
86- continue
87 }86 }
88 go func(baseRoute baseRouteInfo, previewDomain string) {87 go func(baseRoute baseRouteInfo, previewDomain string) {
89 defer func() { <-h.jobs }()88 defer func() { <-h.jobs }()
M internal/usecase/auto/preview/service.go +5 -1
@@ -339,6 +339,7 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
339339
340340 log := zerowrap.FromCtx(ctx)
341341 basePrefix := prefix + "-" + domain.StableResourceName(baseRoute) + "-"
342+ legacyBasePrefix := prefix + "-" + strings.ReplaceAll(baseRoute, ".", "-") + "-"
342343
343344 vols, err := s.volumeCloner.ListVolumes(ctx)
344345 if err != nil {
@@ -347,7 +348,7 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
347348
348349 var sourceVols []string
349350 for _, v := range vols {
350- if strings.HasPrefix(v.Name, basePrefix) {
351+ if strings.HasPrefix(v.Name, basePrefix) || strings.HasPrefix(v.Name, legacyBasePrefix) {
351352 sourceVols = append(sourceVols, v.Name)
352353 }
353354 }
@@ -358,6 +359,9 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
358359
359360 namer := func(sourceVolName string) string {
360361 pathSuffix := strings.TrimPrefix(sourceVolName, basePrefix)
362+ if pathSuffix == sourceVolName {
363+ pathSuffix = strings.TrimPrefix(sourceVolName, legacyBasePrefix)
364+ }
361365 return generateVolumeName(prefix, previewDomain, pathSuffix)
362366 }
363367
@@ -339,6 +339,7 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
339 339
340 log := zerowrap.FromCtx(ctx)340 log := zerowrap.FromCtx(ctx)
341 basePrefix := prefix + "-" + domain.StableResourceName(baseRoute) + "-"341 basePrefix := prefix + "-" + domain.StableResourceName(baseRoute) + "-"
342+ legacyBasePrefix := prefix + "-" + strings.ReplaceAll(baseRoute, ".", "-") + "-"
342 343
343 vols, err := s.volumeCloner.ListVolumes(ctx)344 vols, err := s.volumeCloner.ListVolumes(ctx)
344 if err != nil {345 if err != nil {
@@ -347,7 +348,7 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
347 348
348 var sourceVols []string349 var sourceVols []string
349 for _, v := range vols {350 for _, v := range vols {
350- if strings.HasPrefix(v.Name, basePrefix) {351+ if strings.HasPrefix(v.Name, basePrefix) || strings.HasPrefix(v.Name, legacyBasePrefix) {
351 sourceVols = append(sourceVols, v.Name)352 sourceVols = append(sourceVols, v.Name)
352 }353 }
353 }354 }
@@ -358,6 +359,9 @@ func (s *Service) cloneBaseRouteVolumes(ctx context.Context, baseRoute, previewD
358 359
359 namer := func(sourceVolName string) string {360 namer := func(sourceVolName string) string {
360 pathSuffix := strings.TrimPrefix(sourceVolName, basePrefix)361 pathSuffix := strings.TrimPrefix(sourceVolName, basePrefix)
362+ if pathSuffix == sourceVolName {
363+ pathSuffix = strings.TrimPrefix(sourceVolName, legacyBasePrefix)
364+ }
361 return generateVolumeName(prefix, previewDomain, pathSuffix)365 return generateVolumeName(prefix, previewDomain, pathSuffix)
362 }366 }
363 367
M internal/usecase/registry/service.go +6 -3
@@ -191,7 +191,7 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (s
191191 if validation.IsDigest(manifest.Reference) {
192192 matches, err := manifestDigestMatches(manifest.Reference, manifest.Data)
193193 if err != nil {
194- return "", err
194+ return "", fmt.Errorf("validate manifest digest: %w", err)
195195 }
196196 if !matches {
197197 return "", fmt.Errorf("%w: manifest content does not match %s", domain.ErrDigestMismatch, manifest.Reference)
@@ -237,6 +237,9 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (s
237237
238238 // DeleteManifest removes a manifest.
239239 func (s *Service) DeleteManifest(ctx context.Context, name, reference string) error {
240+ s.mutationMu.RLock()
241+ defer s.mutationMu.RUnlock()
242+
240243 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
241244 zerowrap.FieldLayer: "usecase",
242245 zerowrap.FieldUseCase: "DeleteManifest",
@@ -315,7 +318,7 @@ func (s *Service) repositoryReferencesDigest(name, target string) (bool, error)
315318 if errors.Is(err, domain.ErrManifestNotFound) {
316319 return false, nil
317320 }
318- return false, err
321+ return false, fmt.Errorf("list tags for repository %s: %w", name, err)
319322 }
320323
321324 queue := append([]string(nil), tags...)
@@ -330,7 +333,7 @@ func (s *Service) repositoryReferencesDigest(name, target string) (bool, error)
330333
331334 data, _, err := s.manifestStorage.GetManifest(name, reference)
332335 if err != nil {
333- return false, err
336+ return false, fmt.Errorf("get manifest %s for repository %s: %w", reference, name, err)
334337 }
335338 var refs manifestReferences
336339 if err := json.Unmarshal(data, &refs); err != nil {
@@ -191,7 +191,7 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (s
191 if validation.IsDigest(manifest.Reference) {191 if validation.IsDigest(manifest.Reference) {
192 matches, err := manifestDigestMatches(manifest.Reference, manifest.Data)192 matches, err := manifestDigestMatches(manifest.Reference, manifest.Data)
193 if err != nil {193 if err != nil {
194- return "", err194+ return "", fmt.Errorf("validate manifest digest: %w", err)
195 }195 }
196 if !matches {196 if !matches {
197 return "", fmt.Errorf("%w: manifest content does not match %s", domain.ErrDigestMismatch, manifest.Reference)197 return "", fmt.Errorf("%w: manifest content does not match %s", domain.ErrDigestMismatch, manifest.Reference)
@@ -237,6 +237,9 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (s
237 237
238 // DeleteManifest removes a manifest.238 // DeleteManifest removes a manifest.
239 func (s *Service) DeleteManifest(ctx context.Context, name, reference string) error {239 func (s *Service) DeleteManifest(ctx context.Context, name, reference string) error {
240+ s.mutationMu.RLock()
241+ defer s.mutationMu.RUnlock()
242+
240 ctx = zerowrap.CtxWithFields(ctx, map[string]any{243 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
241 zerowrap.FieldLayer: "usecase",244 zerowrap.FieldLayer: "usecase",
242 zerowrap.FieldUseCase: "DeleteManifest",245 zerowrap.FieldUseCase: "DeleteManifest",
@@ -315,7 +318,7 @@ func (s *Service) repositoryReferencesDigest(name, target string) (bool, error)
315 if errors.Is(err, domain.ErrManifestNotFound) {318 if errors.Is(err, domain.ErrManifestNotFound) {
316 return false, nil319 return false, nil
317 }320 }
318- return false, err321+ return false, fmt.Errorf("list tags for repository %s: %w", name, err)
319 }322 }
320 323
321 queue := append([]string(nil), tags...)324 queue := append([]string(nil), tags...)
@@ -330,7 +333,7 @@ func (s *Service) repositoryReferencesDigest(name, target string) (bool, error)
330 333
331 data, _, err := s.manifestStorage.GetManifest(name, reference)334 data, _, err := s.manifestStorage.GetManifest(name, reference)
332 if err != nil {335 if err != nil {
333- return false, err336+ return false, fmt.Errorf("get manifest %s for repository %s: %w", reference, name, err)
334 }337 }
335 var refs manifestReferences338 var refs manifestReferences
336 if err := json.Unmarshal(data, &refs); err != nil {339 if err := json.Unmarshal(data, &refs); err != nil {

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