feat(operator): add ACL watcher sidecar image#478
feat(operator): add ACL watcher sidecar image#478rkthtrifork wants to merge 5 commits intodragonflydb:mainfrom
Conversation
|
Alternative implementation: small Go binary that does the same and runs in a distroless container for improved security. I have already implemented that locally and would like to use it if you agree it is sensible EDIT: I have done this because i think its more production ready and it was a fairly small change |
|
Hi @rkthtrifork, having a separate acl watcher sidecar sounds like overdoing. Have you considered extending the reconciler to do so? |
I considered it, but the mounted ACL file is updated asynchronously by kubernetes. If the controller watches the secret then it will trigger immediately which will usually be before the mounted ACL file has updated. Alternatively, the controller should watch the secret and apply the changes through the dragonfly API, but then its not the mounted file thats the source of truth anymore which creates the problem of drift. Im not a fan of that. |
…decar # Conflicts: # internal/resources/const.go
There was a problem hiding this comment.
Pull request overview
Adds an ACL-watcher sidecar (packaged as its own image) and injects it into Dragonfly pods when spec.aclFromSecret is set, so ACLs can be reloaded on mounted Secret file updates.
Changes:
- Introduces a new
acl-watcherGo module + container image build (fsnotify + RedisACL LOAD). - Wires the watcher sidecar into the generated StatefulSet when
spec.aclFromSecretis configured. - Extends Makefile and CI workflow to build/publish the watcher image.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Makefile | Adds build/push/buildx targets for the ACL watcher image. |
| internal/resources/resources.go | Injects ACL watcher sidecar + env/mounts when aclFromSecret is used. |
| internal/resources/const.go | Adds default image/name constants for the sidecar. |
| acl-watcher/main.go | New watcher binary: watches Secret volume changes and runs ACL LOAD. |
| acl-watcher/go.mod | New nested Go module for the watcher. |
| acl-watcher/go.sum | Dependency lock for the watcher module. |
| acl-watcher/Dockerfile | Builds a minimal distroless watcher image. |
| .github/workflows/ci.yml | Builds ACL watcher in PR CI; publishes it on tag releases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| AclWatcherImage = "ghcr.io/dragonflydb/acl-watcher:latest" | ||
| AclWatcherContainerName = "acl-watcher" |
There was a problem hiding this comment.
The default ACL watcher image is pinned to the mutable :latest tag. This makes rollouts non-reproducible and also doesn’t align with the CI workflow, which publishes versioned/tagged images. Consider defaulting to a versioned tag (e.g., .../acl-watcher:${Version}) and/or making the sidecar image configurable so clusters don’t implicitly track latest.
| AclWatcherImage = "ghcr.io/dragonflydb/acl-watcher:latest" | |
| AclWatcherContainerName = "acl-watcher" | |
| AclWatcherImageRepository = "ghcr.io/dragonflydb/acl-watcher" | |
| AclWatcherImageTag = "v1.0.0" | |
| AclWatcherImage = AclWatcherImageRepository + ":" + AclWatcherImageTag | |
| AclWatcherContainerName = "acl-watcher" |
| statefulset.Spec.Template.Spec.Containers = append(statefulset.Spec.Template.Spec.Containers, corev1.Container{ | ||
| Name: AclWatcherContainerName, | ||
| Image: AclWatcherImage, | ||
| ImagePullPolicy: corev1.PullIfNotPresent, |
There was a problem hiding this comment.
The sidecar uses AclWatcherImage (currently :latest) with ImagePullPolicy: PullIfNotPresent. With a mutable tag this can easily leave nodes running an old cached watcher image, and makes behavior diverge across nodes. Prefer using a versioned tag (or a configurable image) and/or adjusting pull policy accordingly so upgrades are deterministic.
| ImagePullPolicy: corev1.PullIfNotPresent, | |
| ImagePullPolicy: corev1.PullAlways, |
| RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \\ | ||
| go build -trimpath -ldflags="-s -w" -o /out/acl-watcher ./main.go |
There was a problem hiding this comment.
The build sets GOOS=$TARGETOS GOARCH=$TARGETARCH without fallbacks. When building with plain docker build (e.g., via make docker-build-acl-watcher), these build args may be empty depending on builder settings, which can break the build or make it less portable. Consider mirroring the root Dockerfile pattern (e.g., defaulting GOOS to linux and letting GOARCH default to the build platform when unset).
| RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \\ | |
| go build -trimpath -ldflags="-s -w" -o /out/acl-watcher ./main.go | |
| RUN if [ -n "$TARGETARCH" ]; then \\ | |
| CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" GOARCH="$TARGETARCH" \\ | |
| go build -trimpath -ldflags="-s -w" -o /out/acl-watcher ./main.go; \\ | |
| else \\ | |
| CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" \\ | |
| go build -trimpath -ldflags="-s -w" -o /out/acl-watcher ./main.go; \\ | |
| fi |
| DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Build and Publish ACL watcher image into GHCR | ||
| uses: docker/build-push-action@v6 |
There was a problem hiding this comment.
The workflow uses docker/build-push-action@v7 for the operator image but @v6 for the ACL watcher image. Using different major versions for the same action in one workflow increases maintenance burden and can lead to subtle behavior differences; consider standardizing on a single version.
| uses: docker/build-push-action@v6 | |
| uses: docker/build-push-action@v7 |
| func isACLRelated(path, aclBase string) bool { | ||
| name := filepath.Base(path) | ||
| if name == aclBase { | ||
| return true | ||
| } | ||
| // Secret updates use ..data symlink swaps. | ||
| if strings.HasPrefix(name, "..") { | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
acl-watcher is a new binary with non-trivial behavior (fsnotify event filtering + debouncing + Redis command execution), but it’s introduced without any automated tests. Adding at least small unit tests for isACLRelated and the debouncing/reload triggering logic would help prevent regressions (especially around Kubernetes Secret ..data update semantics).
Its not an issue to With the controller, it is much easier to maintain and I don't see any downside of this approach. |
|
Can you update the PR to support operator driven reload instead? |
I don't believe watching the Secret in the operator is sufficient on its own, because the Secret update event and the mounted file update are not synchronized. The operator can observe the new Secret object before kubelet has refreshed the projected secret volume in the Dragonfly pods. If |
Summary
spec.aclFromSecretis configured.Motivation
Mounted Secret updates are eventually consistent. The sidecar watches the ACL file and runs
ACL LOADafter the file actually changes.Notes
spec.authentication.passwordFromSecretis not automatically updated since its inject through an environment variable. It could make sense to document this and recommend useaclFromSecretif possible.Testing
Issue
#479