Skip to content

Commit 6783783

Browse files
authored
CI: build and publish docker image (#108)
* CI: build and publish docker image * refactor loop/error handling * tests * message * move TotalRetries default
1 parent a2f7c51 commit 6783783

File tree

10 files changed

+150
-25
lines changed

10 files changed

+150
-25
lines changed

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# We include .git in the build context because excluding it would break the
2+
# "make release" target, which uses git to retrieve the build version and tag.
3+
#.git
4+
5+
crowdsec-custom-bouncer
6+
crowdsec-custom-bouncer-*
7+
crowdsec-custom-bouncer.tgz
8+
docs/
9+
debian/
10+
rpm/
11+
test/
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Publish Docker image
2+
3+
on:
4+
release:
5+
types:
6+
- released
7+
- prereleased
8+
9+
permissions:
10+
contents: read
11+
packages: write
12+
13+
jobs:
14+
push_to_registry:
15+
name: Push Docker image to Docker Hub
16+
runs-on: ubuntu-latest
17+
steps:
18+
-
19+
name: Check out the repo
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
-
24+
name: Prepare
25+
id: prep
26+
run: |
27+
DOCKER_IMAGE=crowdsecurity/blocklist-mirror
28+
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/blocklist-mirror
29+
VERSION=edge
30+
if [[ $GITHUB_REF == refs/tags/* ]]; then
31+
VERSION=${GITHUB_REF#refs/tags/}
32+
elif [[ $GITHUB_REF == refs/heads/* ]]; then
33+
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -E 's#/+#-#g')
34+
elif [[ $GITHUB_REF == refs/pull/* ]]; then
35+
VERSION=pr-${{ github.event.number }}
36+
fi
37+
TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}"
38+
if [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.prerelease }}" == "false" ]]; then
39+
TAGS=$TAGS,${DOCKER_IMAGE}:latest,${GHCR_IMAGE}:latest
40+
fi
41+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
42+
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
43+
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
44+
-
45+
name: Set up QEMU
46+
uses: docker/setup-qemu-action@v3
47+
-
48+
name: Set up Docker Buildx
49+
uses: docker/setup-buildx-action@v3
50+
-
51+
name: Login to DockerHub
52+
if: github.event_name == 'release'
53+
uses: docker/login-action@v3
54+
with:
55+
username: ${{ secrets.DOCKER_USERNAME }}
56+
password: ${{ secrets.DOCKER_PASSWORD }}
57+
58+
- name: Login to GitHub Container Registry
59+
uses: docker/login-action@v3
60+
with:
61+
registry: ghcr.io
62+
username: ${{ github.repository_owner }}
63+
password: ${{ secrets.GITHUB_TOKEN }}
64+
-
65+
name: Build and push
66+
uses: docker/build-push-action@v5
67+
with:
68+
context: .
69+
file: ./Dockerfile
70+
push: ${{ github.event_name == 'release' }}
71+
tags: ${{ steps.prep.outputs.tags }}
72+
# Supported by golang:1.18-alpine: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x
73+
# Supported by alpine: same
74+
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x
75+
labels: |
76+
org.opencontainers.image.source=${{ github.event.repository.html_url }}
77+
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
78+
org.opencontainers.image.revision=${{ github.sha }}

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
ARG GOVERSION=1.24
2+
3+
FROM docker.io/golang:${GOVERSION}-alpine AS build
4+
5+
WORKDIR /go/src/cs-custom-bouncer
6+
7+
RUN apk add --update --no-cache make git
8+
COPY . .
9+
10+
RUN make build DOCKER_BUILD=1
11+
12+
FROM alpine:3.21
13+
COPY --from=build /go/src/cs-custom-bouncer/crowdsec-custom-bouncer /usr/local/bin/crowdsec-custom-bouncer
14+
COPY --from=build /go/src/cs-custom-bouncer/config/crowdsec-custom-bouncer.yaml /etc/crowdsec/bouncers/crowdsec-custom-bouncer.yaml
15+
16+
ENTRYPOINT ["/usr/local/bin/crowdsec-custom-bouncer", "-c", "/etc/crowdsec/bouncers/crowdsec-custom-bouncer.yaml"]

cmd/root.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func deleteDecisions(custom *custom.CustomBouncer, decisions []*models.Decision)
7474

7575
func addDecisions(custom *custom.CustomBouncer, decisions []*models.Decision) {
7676
if len(decisions) == 1 {
77-
log.Infof("adding 1 decision")
77+
log.Info("adding 1 decision")
7878
} else {
7979
log.Infof("adding %d decisions", len(decisions))
8080
}
@@ -102,19 +102,30 @@ func feedViaStdin(ctx context.Context, custom *custom.CustomBouncer, config *cfg
102102

103103
return c.Wait()
104104
}
105-
var err error
106-
if config.TotalRetries == -1 {
107-
for {
108-
err = f()
109-
log.Errorf("Binary exited: %s", err)
110-
}
111-
} else {
112-
for i := 1; i <= config.TotalRetries; i++ {
113-
err = f()
114-
log.Errorf("Binary exited (retry %d/%d): %s", i, config.TotalRetries, err)
105+
106+
attempt := 1
107+
delay := 0 * time.Second
108+
109+
for config.TotalRetries == -1 || attempt <= config.TotalRetries {
110+
time.Sleep(delay)
111+
err := f()
112+
switch {
113+
case err == nil:
114+
log.Warningf("custom program exited with no error (retry %d/%d) -- the command is not supposed to quit when using stdin", attempt, config.TotalRetries)
115+
case errors.Is(err, context.Canceled):
116+
log.Info("custom program terminated")
117+
return nil
118+
case config.TotalRetries == 1:
119+
log.Errorf("custom program exited: %s", err)
120+
default:
121+
log.Errorf("custom program exited (retry %d/%d): %s", attempt, config.TotalRetries, err)
115122
}
123+
124+
delay = 2 * time.Second
125+
attempt++
116126
}
117-
return errors.New("maximum retries exceeded for binary. Exiting")
127+
128+
return errors.New("maximum retries exceeded for program execution")
118129
}
119130

120131
func Execute() error {
@@ -164,7 +175,7 @@ func Execute() error {
164175

165176
log.Infof("Starting %s %s", name, version.String())
166177

167-
if err = custom.Init(); err != nil {
178+
if err := custom.Init(); err != nil {
168179
return err
169180
}
170181

@@ -178,8 +189,7 @@ func Execute() error {
178189
bouncer := &csbouncer.StreamBouncer{}
179190
bouncer.UserAgent = fmt.Sprintf("%s/%s", name, version.String())
180191

181-
err = bouncer.ConfigReader(strings.NewReader(configExpanded))
182-
if err != nil {
192+
if err := bouncer.ConfigReader(strings.NewReader(configExpanded)); err != nil {
183193
return fmt.Errorf("unable to configure bouncer: %w", err)
184194
}
185195

@@ -212,7 +222,9 @@ func Execute() error {
212222
prometheus.MustRegister(csbouncer.TotalLAPICalls, csbouncer.TotalLAPIError)
213223
go func() {
214224
log.Infof("Serving metrics at %s", listenOn+"/metrics")
215-
log.Error(promServer.ListenAndServe())
225+
if err := promServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
226+
log.Error(err)
227+
}
216228
// don't need to cancel context here, prometheus is not critical
217229
}()
218230
}
@@ -235,7 +247,7 @@ func Execute() error {
235247
log.Errorf("unable to shutdown prometheus server: %s", err)
236248
}
237249
}
238-
return nil
250+
return ctx.Err()
239251
case decisions := <-bouncer.Stream:
240252
if decisions == nil {
241253
continue

config/crowdsec-custom-bouncer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ log_compression: true
2020
log_max_size: 100
2121
log_max_backups: 3
2222
log_max_age: 30
23-
api_url: http://localhost:8080/
23+
api_url: ${CROWDSEC_LAPI_URL}
2424
api_key: ${API_KEY}
2525

2626
prometheus:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.24.1
55
require (
66
github.com/coreos/go-systemd/v22 v22.5.0
77
github.com/crowdsecurity/crowdsec v1.6.8
8-
github.com/crowdsecurity/go-cs-bouncer v0.0.14
8+
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0
99
github.com/crowdsecurity/go-cs-lib v0.0.16
1010
github.com/prometheus/client_golang v1.18.0
1111
github.com/sirupsen/logrus v1.9.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
1515
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1616
github.com/crowdsecurity/crowdsec v1.6.8 h1:c9C6Q0yBx6kUB878nH4Ib7TVwYF/1Dzw4LRuJvt0dJU=
1717
github.com/crowdsecurity/crowdsec v1.6.8/go.mod h1:PiTkIhJ55g8jnkObrO55LSaw2dRRd37M5WVnS4rkiNI=
18-
github.com/crowdsecurity/go-cs-bouncer v0.0.14 h1:0hxOaa59pMT274qDzJXNxps4QfMnhSNss+oUn36HTpw=
19-
github.com/crowdsecurity/go-cs-bouncer v0.0.14/go.mod h1:4nSF37v7i98idHM6cw1o0V0XgiY25EjTLfFFXvqg6OA=
18+
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0 h1:TczufDPouQEJLVRZqRxnFU/Rb7ilRxCwVuTK1FvpeSM=
19+
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0/go.mod h1:4nSF37v7i98idHM6cw1o0V0XgiY25EjTLfFFXvqg6OA=
2020
github.com/crowdsecurity/go-cs-lib v0.0.16 h1:2/htodjwc/sfsv4deX8F/2Fzg1bOI8w3O1/BPSvvsB0=
2121
github.com/crowdsecurity/go-cs-lib v0.0.16/go.mod h1:XwGcvTt4lMq4Tm1IRMSKMDf0CVrnytTU8Uoofa7AR+g=
2222
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

pkg/cfg/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,9 @@ func NewConfig(reader io.Reader) (*BouncerConfig, error) {
7878
config.CacheRetentionDuration = 10 * time.Second
7979
}
8080

81+
if config.TotalRetries == 0 {
82+
config.TotalRetries = 1
83+
}
84+
8185
return config, nil
8286
}

test/tests/bouncer/test_custom_bouncer.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,22 @@ def test_binary_monitor(bouncer_with_lapi):
169169

170170
# Let's kill custom-stream and see if it's restarted max_retry times (2)
171171
cb.halt_children()
172+
time.sleep(2)
172173
cb.wait_for_child(timeout=2)
173174
assert len(cb.children()) == 1
174175
cb.wait_for_lines_fnmatch(
175176
[
176-
"*Binary exited (retry 1/3): signal: killed*",
177+
"*custom program exited (retry 1/3): signal: killed*",
177178
]
178179
)
179180

180181
cb.halt_children()
182+
time.sleep(2)
181183
cb.wait_for_child(timeout=2)
182184
assert len(cb.children()) == 1
183185
cb.wait_for_lines_fnmatch(
184186
[
185-
"*Binary exited (retry 2/3): signal: killed*",
187+
"*custom program exited (retry 2/3): signal: killed*",
186188
]
187189
)
188190

@@ -192,7 +194,7 @@ def test_binary_monitor(bouncer_with_lapi):
192194
cb.proc.wait()
193195
assert not cb.proc.is_running()
194196
cb.wait_for_lines_fnmatch(
195-
["*Binary exited (retry 3/3): signal: killed*", "*maximum retries exceeded for binary. Exiting*"]
197+
["*custom program exited (retry 3/3): signal: killed*", "*maximum retries exceeded for program execution*"]
196198
)
197199

198200

test/tests/pkg/test_build_deb.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from pathlib import Path
2+
13
import pytest
24

35
pytestmark = pytest.mark.deb
46

57

68
# This test has the side effect of building the package and leaving it in the
79
# project's parent directory.
8-
def test_deb_build(deb_package, skip_unless_deb):
10+
def test_deb_build(deb_package: Path, skip_unless_deb):
911
"""Test that the package can be built."""
1012
assert deb_package.exists(), f"Package {deb_package} not found"

0 commit comments

Comments
 (0)