Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# We include .git in the build context because excluding it would break the
# "make release" target, which uses git to retrieve the build version and tag.
#.git

crowdsec-custom-bouncer
crowdsec-custom-bouncer-*
crowdsec-custom-bouncer.tgz
docs/
debian/
rpm/
test/
78 changes: 78 additions & 0 deletions .github/workflows/release_publish_docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Publish Docker image

on:
release:
types:
- released
- prereleased

permissions:
contents: read
packages: write

jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Prepare
id: prep
run: |
DOCKER_IMAGE=crowdsecurity/blocklist-mirror
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/blocklist-mirror
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/* ]]; then
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -E 's#/+#-#g')
elif [[ $GITHUB_REF == refs/pull/* ]]; then
VERSION=pr-${{ github.event.number }}
fi
TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}"
if [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.prerelease }}" == "false" ]]; then
TAGS=$TAGS,${DOCKER_IMAGE}:latest,${GHCR_IMAGE}:latest
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
if: github.event_name == 'release'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name == 'release' }}
tags: ${{ steps.prep.outputs.tags }}
# Supported by golang:1.18-alpine: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x
# Supported by alpine: same
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x
labels: |
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
org.opencontainers.image.revision=${{ github.sha }}
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ARG GOVERSION=1.24

FROM docker.io/golang:${GOVERSION}-alpine AS build

WORKDIR /go/src/cs-custom-bouncer

RUN apk add --update --no-cache make git
COPY . .

RUN make build DOCKER_BUILD=1

FROM alpine:3.21
COPY --from=build /go/src/cs-custom-bouncer/crowdsec-custom-bouncer /usr/local/bin/crowdsec-custom-bouncer
COPY --from=build /go/src/cs-custom-bouncer/config/crowdsec-custom-bouncer.yaml /etc/crowdsec/bouncers/crowdsec-custom-bouncer.yaml

ENTRYPOINT ["/usr/local/bin/crowdsec-custom-bouncer", "-c", "/etc/crowdsec/bouncers/crowdsec-custom-bouncer.yaml"]
50 changes: 33 additions & 17 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func deleteDecisions(custom *custom.CustomBouncer, decisions []*models.Decision)

func addDecisions(custom *custom.CustomBouncer, decisions []*models.Decision) {
if len(decisions) == 1 {
log.Infof("adding 1 decision")
log.Info("adding 1 decision")
} else {
log.Infof("adding %d decisions", len(decisions))
}
Expand Down Expand Up @@ -102,19 +102,34 @@ func feedViaStdin(ctx context.Context, custom *custom.CustomBouncer, config *cfg

return c.Wait()
}
var err error
if config.TotalRetries == -1 {
for {
err = f()
log.Errorf("Binary exited: %s", err)
}
} else {
for i := 1; i <= config.TotalRetries; i++ {
err = f()
log.Errorf("Binary exited (retry %d/%d): %s", i, config.TotalRetries, err)

if config.TotalRetries == 0 {
config.TotalRetries = 1
}

attempt := 1
delay := 0 * time.Second

for config.TotalRetries == -1 || attempt <= config.TotalRetries {
time.Sleep(delay)
err := f()
switch {
case err == nil:
log.Warning("custom program exited with no error -- the command is not supposed to quit when using stdin")
case errors.Is(err, context.Canceled):
log.Info("custom program terminated")
return nil
case config.TotalRetries == 1:
log.Errorf("custom program exited: %s", err.Error())
default:
log.Errorf("custom program exited (retry %d/%d): %s", attempt, config.TotalRetries, err.Error())
}

delay = 2 * time.Second
attempt++
}
return errors.New("maximum retries exceeded for binary. Exiting")

return errors.New("maximum retries exceeded for program execution")
}

func Execute() error {
Expand Down Expand Up @@ -164,7 +179,7 @@ func Execute() error {

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

if err = custom.Init(); err != nil {
if err := custom.Init(); err != nil {
return err
}

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

err = bouncer.ConfigReader(strings.NewReader(configExpanded))
if err != nil {
if err := bouncer.ConfigReader(strings.NewReader(configExpanded)); err != nil {
return fmt.Errorf("unable to configure bouncer: %w", err)
}

Expand Down Expand Up @@ -212,7 +226,9 @@ func Execute() error {
prometheus.MustRegister(csbouncer.TotalLAPICalls, csbouncer.TotalLAPIError)
go func() {
log.Infof("Serving metrics at %s", listenOn+"/metrics")
log.Error(promServer.ListenAndServe())
if err := promServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
// don't need to cancel context here, prometheus is not critical
}()
}
Expand All @@ -235,7 +251,7 @@ func Execute() error {
log.Errorf("unable to shutdown prometheus server: %s", err)
}
}
return nil
return ctx.Err()
case decisions := <-bouncer.Stream:
if decisions == nil {
continue
Expand Down
2 changes: 1 addition & 1 deletion config/crowdsec-custom-bouncer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ log_compression: true
log_max_size: 100
log_max_backups: 3
log_max_age: 30
api_url: http://localhost:8080/
api_url: ${CROWDSEC_LAPI_URL}
api_key: ${API_KEY}

prometheus:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.1
require (
github.com/coreos/go-systemd/v22 v22.5.0
github.com/crowdsecurity/crowdsec v1.6.8
github.com/crowdsecurity/go-cs-bouncer v0.0.14
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0
github.com/crowdsecurity/go-cs-lib v0.0.16
github.com/prometheus/client_golang v1.18.0
github.com/sirupsen/logrus v1.9.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crowdsecurity/crowdsec v1.6.8 h1:c9C6Q0yBx6kUB878nH4Ib7TVwYF/1Dzw4LRuJvt0dJU=
github.com/crowdsecurity/crowdsec v1.6.8/go.mod h1:PiTkIhJ55g8jnkObrO55LSaw2dRRd37M5WVnS4rkiNI=
github.com/crowdsecurity/go-cs-bouncer v0.0.14 h1:0hxOaa59pMT274qDzJXNxps4QfMnhSNss+oUn36HTpw=
github.com/crowdsecurity/go-cs-bouncer v0.0.14/go.mod h1:4nSF37v7i98idHM6cw1o0V0XgiY25EjTLfFFXvqg6OA=
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0 h1:TczufDPouQEJLVRZqRxnFU/Rb7ilRxCwVuTK1FvpeSM=
github.com/crowdsecurity/go-cs-bouncer v0.0.15-0.20250331125736-2a8a151b96a0/go.mod h1:4nSF37v7i98idHM6cw1o0V0XgiY25EjTLfFFXvqg6OA=
github.com/crowdsecurity/go-cs-lib v0.0.16 h1:2/htodjwc/sfsv4deX8F/2Fzg1bOI8w3O1/BPSvvsB0=
github.com/crowdsecurity/go-cs-lib v0.0.16/go.mod h1:XwGcvTt4lMq4Tm1IRMSKMDf0CVrnytTU8Uoofa7AR+g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
8 changes: 5 additions & 3 deletions test/tests/bouncer/test_custom_bouncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,20 +169,22 @@ def test_binary_monitor(bouncer_with_lapi):

# Let's kill custom-stream and see if it's restarted max_retry times (2)
cb.halt_children()
time.sleep(2)
cb.wait_for_child(timeout=2)
assert len(cb.children()) == 1
cb.wait_for_lines_fnmatch(
[
"*Binary exited (retry 1/3): signal: killed*",
"*custom program exited (retry 1/3): signal: killed*",
]
)

cb.halt_children()
time.sleep(2)
cb.wait_for_child(timeout=2)
assert len(cb.children()) == 1
cb.wait_for_lines_fnmatch(
[
"*Binary exited (retry 2/3): signal: killed*",
"*custom program exited (retry 2/3): signal: killed*",
]
)

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


Expand Down