Skip to content

Commit 983c5c3

Browse files
blotusbuixormmetc
authored
allow watcher to self-delete on shutdown (#3565)
* allow watcher to self-delete on shutdown * rename lapi endpoing * rename config option * test * fix tests * docker: $UNREGISTER_ON_EXIT * docker test * update test dependencies, with custom "stop_timeout" * update url in client * meh * wip * url --------- Co-authored-by: Thibault "bui" Koechlin <[email protected]> Co-authored-by: marco <[email protected]>
1 parent 52ebcc3 commit 983c5c3

File tree

13 files changed

+377
-231
lines changed

13 files changed

+377
-231
lines changed

cmd/crowdsec/serve.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/crowdsecurity/go-cs-lib/csdaemon"
1616
"github.com/crowdsecurity/go-cs-lib/trace"
1717

18+
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
1819
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
1920
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
2021
"github.com/crowdsecurity/crowdsec/pkg/database"
@@ -322,6 +323,18 @@ func HandleSignals(cConfig *csconfig.Config) error {
322323
if err == nil {
323324
log.Warning("Crowdsec service shutting down")
324325
}
326+
if cConfig.API != nil && cConfig.API.Client != nil && cConfig.API.Client.UnregisterOnExit {
327+
log.Warning("Unregistering watcher")
328+
lapiClient, err := apiclient.GetLAPIClient()
329+
if err != nil {
330+
return err
331+
}
332+
_, err = lapiClient.Auth.UnregisterWatcher(context.TODO())
333+
if err != nil {
334+
return fmt.Errorf("failed to unregister watcher: %w", err)
335+
}
336+
log.Warning("Watcher unregistered")
337+
}
325338

326339
return err
327340
}

docker/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ config.yaml) each time the container is run.
289289
| __Agent__ | | (these don't work with DISABLE_AGENT) |
290290
| `TYPE` | | [`Labels.type`](https://docs.crowdsec.net/Crowdsec/v1/references/acquisition/) for file in time-machine: `-e TYPE="<type>"` |
291291
| `DSN` | | Process a single source in time-machine: `-e DSN="file:///var/log/toto.log"` or `-e DSN="cloudwatch:///your/group/path:stream_name?profile=dev&backlog=16h"` or `-e DSN="journalctl://filters=_SYSTEMD_UNIT=ssh.service"` |
292+
| `UNREGISTER_ON_EXIT` | | Remove the agent from the LAPI when its container is stopped. |
292293
| | | |
293294
| __Bouncers__ | | |
294295
| `BOUNCER_KEY_<name>` | | Register a bouncer with the name `<name>` and a key equal to the value of the environment variable. |

docker/docker_start.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,8 @@ else
474474
conf_set '.api.server.enable=true'
475475
fi
476476

477+
conf_set_if "$UNREGISTER_ON_EXIT" '.api.client.unregister_on_exit=env(UNREGISTER_ON_EXIT)'
478+
477479
ARGS=""
478480
if [ "$CONFIG_FILE" != "" ]; then
479481
ARGS="-c $CONFIG_FILE"

docker/test/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"pytest>=8.3.4",
9-
"pytest-cs",
9+
"pytest-cs>=0.7.22",
1010
"pytest-dotenv>=0.5.2",
1111
"pytest-xdist>=3.6.1",
1212
]

docker/test/tests/test_agent_only.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import secrets
2+
import time
23
from http import HTTPStatus
34

45
import pytest
@@ -35,3 +36,52 @@ def test_split_lapi_agent(crowdsec, flavor: str) -> None:
3536
assert res.exit_code == 0
3637
stdout = res.output.decode()
3738
assert "You can successfully interact with Local API (LAPI)" in stdout
39+
40+
41+
def test_unregister_on_exit(crowdsec, flavor: str) -> None:
42+
rand = str(secrets.randbelow(10000))
43+
lapiname = f"lapi-{rand}"
44+
agentname = f"agent-{rand}"
45+
46+
lapi_env = {
47+
"AGENT_USERNAME": "testagent",
48+
"AGENT_PASSWORD": "testpassword",
49+
}
50+
51+
agent_env = {
52+
"AGENT_USERNAME": "testagent",
53+
"AGENT_PASSWORD": "testpassword",
54+
"DISABLE_LOCAL_API": "true",
55+
"LOCAL_API_URL": f"http://{lapiname}:8080",
56+
"UNREGISTER_ON_EXIT": "true",
57+
}
58+
59+
cs_lapi = crowdsec(name=lapiname, environment=lapi_env, flavor=flavor)
60+
cs_agent = crowdsec(name=agentname, environment=agent_env, flavor=flavor, stop_timeout=5)
61+
62+
with cs_lapi as lapi:
63+
lapi.wait_for_log("*CrowdSec Local API listening on *:8080*")
64+
lapi.wait_for_http(8080, "/health", want_status=HTTPStatus.OK)
65+
66+
res = lapi.cont.exec_run("cscli machines list")
67+
assert res.exit_code == 0
68+
# the machine is created in the lapi entrypoint
69+
assert "testagent" in res.output.decode()
70+
71+
with cs_agent as agent:
72+
agent.wait_for_log("*Starting processing data*")
73+
res = agent.cont.exec_run("cscli lapi status")
74+
assert res.exit_code == 0
75+
stdout = res.output.decode()
76+
assert "You can successfully interact with Local API (LAPI)" in stdout
77+
78+
res = lapi.cont.exec_run("cscli machines list")
79+
assert res.exit_code == 0
80+
assert "testagent" in res.output.decode()
81+
82+
time.sleep(2)
83+
84+
res = lapi.cont.exec_run("cscli machines list")
85+
assert res.exit_code == 0
86+
# and it's not there anymore
87+
assert "testagent" not in res.output.decode()

docker/test/uv.lock

Lines changed: 245 additions & 227 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apiclient/auth_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type enrollRequest struct {
1919
}
2020

2121
func (s *AuthService) UnregisterWatcher(ctx context.Context) (*Response, error) {
22-
u := fmt.Sprintf("%s/watchers", s.client.URLPrefix)
22+
u := fmt.Sprintf("%s/watchers/self", s.client.URLPrefix)
2323

2424
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
2525
if err != nil {

pkg/apiclient/auth_service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func TestWatcherUnregister(t *testing.T) {
188188
defer teardown()
189189
// body: models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password}
190190

191-
mux.HandleFunc("/watchers", func(w http.ResponseWriter, r *http.Request) {
191+
mux.HandleFunc("/watchers/self", func(w http.ResponseWriter, r *http.Request) {
192192
testMethod(t, r, "DELETE")
193193
assert.Equal(t, int64(0), r.ContentLength)
194194
w.WriteHeader(http.StatusOK)

pkg/apiserver/controllers/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (c *Controller) NewV1() error {
130130
jwtAuth.GET("/allowlists/check/:ip_or_range", c.HandlerV1.CheckInAllowlist)
131131
jwtAuth.HEAD("/allowlists/check/:ip_or_range", c.HandlerV1.CheckInAllowlist)
132132
jwtAuth.POST("/allowlists/check", c.HandlerV1.CheckInAllowlistBulk)
133+
jwtAuth.DELETE("/watchers/self", c.HandlerV1.DeleteMachine)
133134
}
134135

135136
apiKeyAuth := groupV1.Group("")

pkg/apiserver/controllers/v1/machines.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,27 @@ func (c *Controller) CreateMachine(gctx *gin.Context) {
8080
gctx.Status(http.StatusCreated)
8181
}
8282
}
83+
84+
func (c *Controller) DeleteMachine(gctx *gin.Context) {
85+
ctx := gctx.Request.Context()
86+
87+
machineID, err := getMachineIDFromContext(gctx)
88+
89+
if err != nil {
90+
gctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
91+
return
92+
}
93+
if machineID == "" {
94+
gctx.JSON(http.StatusBadRequest, gin.H{"message": "machineID not found in claims"})
95+
return
96+
}
97+
98+
if err := c.DBClient.DeleteWatcher(ctx, machineID); err != nil {
99+
c.HandleDBErrors(gctx, err)
100+
return
101+
}
102+
103+
log.WithFields(log.Fields{"ip": gctx.ClientIP(), "machine_id": machineID}).Info("Deleted machine")
104+
105+
gctx.Status(http.StatusNoContent)
106+
}

0 commit comments

Comments
 (0)