Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
runs-on: ubuntu-latest
env:
IMG: ghcr.io/dragonflydb/operator:${{ github.sha }}
ACL_WATCHER_IMG: ghcr.io/dragonflydb/acl-watcher:${{ github.sha }}

steps:
- uses: actions/checkout@v6
Expand All @@ -25,6 +26,7 @@ jobs:
- name: Docker Build
run: |
make docker-build
make docker-build-acl-watcher

- uses: helm/kind-action@v1.13.0
with:
Expand All @@ -50,6 +52,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
env:
IMG: ghcr.io/dragonflydb/operator:${{ github.ref_name }}
ACL_WATCHER_IMG: ghcr.io/dragonflydb/acl-watcher:${{ github.ref_name }}
VERSION: ${{ github.ref_name }}
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -101,6 +104,21 @@ jobs:
DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

- name: Build and Publish ACL watcher image into GHCR
uses: docker/build-push-action@v6
with:
context: acl-watcher
file: acl-watcher/Dockerfile
push: true
tags: ghcr.io/dragonflydb/acl-watcher:${{ github.ref_name }}
platforms: |
linux/amd64
linux/arm64
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

- name: publish github release
uses: softprops/action-gh-release@v2
with:
Expand Down
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

# Image URL to use all building/pushing image targets
IMG ?= controller:latest
ACL_WATCHER_IMG ?= acl-watcher:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.30.0

Expand Down Expand Up @@ -78,10 +79,18 @@ run: manifests generate fmt vet ## Run a controller from your host.
docker-build: ## Build docker image with the manager.
docker build -t ${IMG} .

.PHONY: docker-build-acl-watcher
docker-build-acl-watcher: ## Build docker image with the ACL watcher.
docker build -t ${ACL_WATCHER_IMG} -f acl-watcher/Dockerfile acl-watcher

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
docker push ${IMG}

.PHONY: docker-push-acl-watcher
docker-push-acl-watcher: ## Push docker image with the ACL watcher.
docker push ${ACL_WATCHER_IMG}

.PHONY: docker-kind-load
docker-kind-load: ## Load docker image with the manager into kind cluster.
kind load docker-image ${IMG}
Expand All @@ -103,6 +112,13 @@ docker-buildx: test ## Build and push docker image for the manager for cross-pla
- docker buildx rm project-v3-builder
rm Dockerfile.cross

.PHONY: docker-buildx-acl-watcher
docker-buildx-acl-watcher: ## Build and push docker image for the ACL watcher for cross-platform support
- docker buildx create --name project-v3-builder
docker buildx use project-v3-builder
- docker buildx build --push --platform=$(PLATFORMS) --tag ${ACL_WATCHER_IMG} -f acl-watcher/Dockerfile acl-watcher
- docker buildx rm project-v3-builder

##@ Deployment

ifndef ignore-not-found
Expand Down
21 changes: 21 additions & 0 deletions acl-watcher/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM golang:1.25-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download

COPY main.go ./

ARG TARGETOS
ARG TARGETARCH

RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \\
go build -trimpath -ldflags="-s -w" -o /out/acl-watcher ./main.go

FROM gcr.io/distroless/static:nonroot

COPY --from=builder /out/acl-watcher /acl-watcher

USER nonroot:nonroot

ENTRYPOINT ["/acl-watcher"]
14 changes: 14 additions & 0 deletions acl-watcher/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/dragonflydb/dragonfly-operator/acl-watcher

go 1.25

require (
github.com/fsnotify/fsnotify v1.8.0
github.com/redis/go-redis/v9 v9.17.3
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
golang.org/x/sys v0.13.0 // indirect
)
14 changes: 14 additions & 0 deletions acl-watcher/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
129 changes: 129 additions & 0 deletions acl-watcher/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"context"
"log"
"os"
"path/filepath"
"strings"
"time"

"github.com/fsnotify/fsnotify"
"github.com/redis/go-redis/v9"
)

const defaultDebounce = 1 * time.Second

type debouncer struct {
delay time.Duration
timer *time.Timer
}

func (d *debouncer) Trigger(fn func()) {
// Debounce: reset the timer so a burst of events triggers a single callback.
if d.timer != nil {
if !d.timer.Stop() {
select {
case <-d.timer.C:
default:
}
}
}
d.timer = time.AfterFunc(d.delay, fn)
}

func main() {
aclDir := mustGetenv("ACL_DIR")
aclFile := mustGetenv("ACL_FILE")
redisHost := mustGetenv("REDIS_HOST")
redisPort := mustGetenv("REDIS_PORT")
debounce := parseDuration(os.Getenv("DEBOUNCE"), defaultDebounce)

addr := redisHost + ":" + redisPort
log.Printf("acl-watcher starting: aclDir=%s aclFile=%s redisAddr=%s debounce=%s", aclDir, aclFile, addr, debounce)

client := redis.NewClient(&redis.Options{Addr: addr})
defer client.Close()

// Initial load
reloadACL(client, "initial")

watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("failed to create watcher: %v", err)
}
defer watcher.Close()

if err := watcher.Add(aclDir); err != nil {
log.Fatalf("failed to watch directory %s: %v", aclDir, err)
}

d := debouncer{delay: debounce}
trigger := func(reason string) {
d.Trigger(func() { reloadACL(client, reason) })
}

aclFileBase := filepath.Base(aclFile)

for {
select {
case evt, ok := <-watcher.Events:
if !ok {
log.Fatalf("watcher events channel closed")
}
if !isACLRelated(evt.Name, aclFileBase) {
continue
}
if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) || evt.Has(fsnotify.Rename) || evt.Has(fsnotify.Remove) || evt.Has(fsnotify.Chmod) {
trigger(evt.Op.String())
}
case err, ok := <-watcher.Errors:
if !ok {
log.Fatalf("watcher errors channel closed")
}
log.Printf("watcher error: %v", err)
}
}
}

func reloadACL(client *redis.Client, reason string) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

if err := client.Do(ctx, "ACL", "LOAD").Err(); err != nil {
log.Printf("ACL LOAD failed (%s): %v", reason, err)
return
}
log.Printf("ACL LOAD succeeded (%s)", reason)
}

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
}

func mustGetenv(key string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
log.Fatalf("missing required env var %s", key)
return ""
}

func parseDuration(value string, def time.Duration) time.Duration {
if value == "" {
return def
}
dur, err := time.ParseDuration(value)
if err != nil {
return def
}
return dur
}
7 changes: 5 additions & 2 deletions internal/resources/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ const (
// DragonflyImage is the default image of the Dragonfly to use
DragonflyImage = "docker.dragonflydb.io/dragonflydb/dragonfly"

// AclWatcherImage is the default image used for the ACL watcher sidecar.
AclWatcherImage = "ghcr.io/dragonflydb/acl-watcher:latest"
AclWatcherContainerName = "acl-watcher"

// Recommended Kubernetes Application Labels
// KubernetesAppNameLabel is the name of the application
KubernetesAppNameLabelKey = "app.kubernetes.io/name"
Expand Down Expand Up @@ -93,8 +97,7 @@ const (

Master = "master"

Replica = "replica"

Replica = "replica"
ReplicationReadyConditionType = "dragonflydb.io/replication-ready"

OperatorControlPlaneLabelKey = "control-plane"
Expand Down
31 changes: 31 additions & 0 deletions internal/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage
})

statefulset.Spec.Template.Spec.Containers[0].Args = append(statefulset.Spec.Template.Spec.Containers[0].Args, fmt.Sprintf("%s=%s/%s", AclFileArg, AclDir, AclFileName))

statefulset.Spec.Template.Spec.Containers = append(statefulset.Spec.Template.Spec.Containers, corev1.Container{
Name: AclWatcherContainerName,
Image: AclWatcherImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Env: []corev1.EnvVar{
{
Name: "ACL_DIR",
Value: AclDir,
},
{
Name: "ACL_FILE",
Value: fmt.Sprintf("%s/%s", AclDir, AclFileName),
},
{
Name: "REDIS_HOST",
Value: "127.0.0.1",
},
{
Name: "REDIS_PORT",
Value: fmt.Sprintf("%d", DragonflyAdminPort),
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: AclVolumeName,
MountPath: AclDir,
ReadOnly: true,
},
},
})
}

// Doc: https://www.dragonflydb.io/blog/a-preview-of-dragonfly-ssd-tiering
Expand Down