├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── samples │ ├── kustomization.yaml │ ├── v1alpha1_dragonfly.yaml │ ├── pvc.yaml │ └── v1alpha1_dragonfly_existing_pvc.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_dragonflies.yaml │ │ └── webhook_in_dragonflies.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml └── rbac │ ├── service_account.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── dragonfly_viewer_role.yaml │ ├── kustomization.yaml │ ├── dragonfly_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── charts └── dragonfly-operator │ ├── dashboards │ └── grafana-dashboard.json │ ├── .helmignore │ ├── templates │ ├── tests │ │ └── test-connection.yaml │ ├── serviceaccount.yaml │ ├── rolebindings.yaml │ ├── service.yaml │ ├── roles.yaml │ ├── clusterrolebindings.yaml │ ├── servicemonitor.yaml │ ├── grafanadashboards.yaml │ ├── clusterroles.yaml │ ├── _helpers.tpl │ └── deployment.yaml │ ├── Chart.yaml │ └── values.yaml ├── monitoring ├── promServiceAccount.yaml ├── prometheus-service.yaml ├── promClusterBinding.yaml ├── prometheus-config.yaml ├── podMonitor.yaml ├── promClusterRole.yaml ├── podMonitorGuide.md └── grafana-guide.md ├── .dockerignore ├── hack ├── kind-config.yaml ├── boilerplate.go.txt └── print-roles.go ├── .gitignore ├── .github ├── dependabot.yaml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── ci.yml └── images │ └── logo-full.svg ├── PROJECT ├── internal ├── resources │ ├── version.go │ ├── image_test.go │ ├── resources_test.go │ └── const.go └── controller │ ├── base_controller.go │ ├── dragonfly_pod_lifecycle_controller.go │ ├── util.go │ └── dragonfly_controller.go ├── Dockerfile ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── dragonfly_types.go │ └── zz_generated.deepcopy.go ├── CONTRIBUTING.md ├── e2e ├── suite_test.go ├── dragonfly_pod_lifecycle_controller_test.go └── util.go ├── dragonfly-with-tls.md ├── go.mod ├── s3.md ├── README.md ├── cmd └── main.go ├── Makefile └── LICENSE /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/dashboards/grafana-dashboard.json: -------------------------------------------------------------------------------- 1 | ../../../monitoring/grafana-dashboard.json -------------------------------------------------------------------------------- /monitoring/promServiceAccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: prometheus 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - v1alpha1_dragonfly.yaml 4 | #+kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /hack/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | - role: worker 6 | labels: 7 | database: dragonfly 8 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: docker.dragonflydb.io/dragonflydb/operator 8 | newTag: v1.3.1 9 | -------------------------------------------------------------------------------- /monitoring/prometheus-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus-svc 5 | spec: 6 | selector: 7 | prometheus: prometheus 8 | type: ClusterIP 9 | ports: 10 | - name: web 11 | port: 9090 12 | protocol: TCP 13 | targetPort: web 14 | 15 | -------------------------------------------------------------------------------- /monitoring/promClusterBinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: prometheus 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: prometheus 9 | subjects: 10 | - kind: ServiceAccount 11 | name: prometheus 12 | namespace: default 13 | -------------------------------------------------------------------------------- /monitoring/prometheus-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: Prometheus 3 | metadata: 4 | name: prometheus 5 | spec: 6 | serviceAccountName: prometheus 7 | podMonitorSelector: 8 | matchLabels: 9 | group: dragonfly 10 | app: dragonfly-sample 11 | resources: 12 | requests: 13 | memory: 400Mi 14 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_dragonflies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: dragonflies.dragonflydb.io 8 | -------------------------------------------------------------------------------- /monitoring/podMonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: PodMonitor 3 | metadata: 4 | name: dragonfly-sample 5 | labels: 6 | group: dragonfly 7 | app: dragonfly-sample 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: dragonfly-sample 12 | podTargetLabels: 13 | - app 14 | podMetricsEndpoints: 15 | - port: admin 16 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /monitoring/promClusterRole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: prometheus 5 | rules: 6 | - apiGroups: [""] 7 | resources: 8 | - nodes 9 | - nodes/metrics 10 | - services 11 | - endpoints 12 | - pods 13 | verbs: ["get", "list", "watch"] 14 | - apiGroups: 15 | - networking.k8s.io 16 | resources: 17 | - ingresses 18 | verbs: ["get", "list", "watch"] 19 | - nonResourceURLs: ["/metrics"] 20 | verbs: ["get"] 21 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_dragonflies.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: dragonflies.dragonflydb.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | Dockerfile.cross 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(actions):" 9 | labels: 10 | - "dependencies" 11 | - "github-actions" 12 | - package-ecosystem: "gomod" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | commit-message: 17 | prefix: "chore(actions):" 18 | labels: 19 | - "dependencies" 20 | - "go" 21 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/samples/v1alpha1_dragonfly.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dragonflydb.io/v1alpha1 2 | kind: Dragonfly 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: dragonfly 6 | app.kubernetes.io/instance: dragonfly-sample 7 | app.kubernetes.io/part-of: dragonfly-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | name: dragonfly-sample 11 | spec: 12 | replicas: 2 13 | resources: 14 | requests: 15 | cpu: 500m 16 | memory: 500Mi 17 | limits: 18 | cpu: 600m 19 | memory: 750Mi 20 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "dragonfly-operator.fullname" . }}-test-connection" 5 | namespace: {{ include "dragonfly-operator.namespace" . }} 6 | labels: 7 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": test 10 | spec: 11 | containers: 12 | - name: wget 13 | image: busybox 14 | command: ['wget'] 15 | args: ['{{ include "dragonfly-operator.fullname" . }}:{{ .Values.service.port }}'] 16 | restartPolicy: Never 17 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "dragonfly-operator.serviceAccountName" . }} 6 | namespace: {{ include "dragonfly-operator.namespace" . }} 7 | labels: 8 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 9 | app.kubernetes.io/component: rbac 10 | {{- with .Values.serviceAccount.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 15 | {{- end }} -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: dragonflydb.io 6 | layout: 7 | - go.kubebuilder.io/v4-alpha 8 | projectName: dragonfly-operator 9 | repo: github.com/dragonflydb/dragonfly-operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: dragonflydb.io 16 | kind: Dragonfly 17 | path: github.com/dragonflydb/dragonfly-operator/api/v1alpha1 18 | version: v1alpha1 19 | version: "3" 20 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: dragonfly-operator 9 | app.kubernetes.io/part-of: dragonfly-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | app.kubernetes.io/part-of: dragonfly-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/rolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "dragonfly-operator.fullname" . }}-leader-election-rolebinding 5 | labels: 6 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: rbac 8 | namespace: {{ include "dragonfly-operator.namespace" . }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: {{ include "dragonfly-operator.fullname" . }}-leader-election-role 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "dragonfly-operator.serviceAccountName" . }} 16 | namespace: {{ include "dragonfly-operator.namespace" . }} -------------------------------------------------------------------------------- /internal/resources/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resources 18 | 19 | const ( 20 | Version = "v1.35.0" 21 | ) 22 | -------------------------------------------------------------------------------- /config/rbac/dragonfly_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view dragonflies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dragonfly-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | app.kubernetes.io/part-of: dragonfly-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dragonfly-viewer-role 13 | rules: 14 | - apiGroups: 15 | - dragonflydb.io 16 | resources: 17 | - dragonflies 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - dragonflydb.io 24 | resources: 25 | - dragonflies/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /config/samples/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dragonflydb.io/v1alpha1 2 | kind: Dragonfly 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: dragonfly 6 | app.kubernetes.io/instance: dragonfly-sample 7 | app.kubernetes.io/part-of: dragonfly-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | name: dragonfly-sample 11 | spec: 12 | replicas: 1 13 | resources: 14 | requests: 15 | cpu: 500m 16 | memory: 500Mi 17 | limits: 18 | cpu: 600m 19 | memory: 750Mi 20 | snapshot: 21 | cron: "*/5 * * * *" 22 | enableOnMasterOnly: false 23 | persistentVolumeClaimSpec: 24 | accessModes: 25 | - ReadWriteOnce 26 | resources: 27 | requests: 28 | storage: 2Gi 29 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "dragonfly-operator.controllerServiceName" . }} 5 | labels: 6 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | namespace: {{ include "dragonfly-operator.namespace" . }} 9 | spec: 10 | type: {{ .Values.service.type }} 11 | ports: 12 | - port: {{ .Values.service.port }} 13 | targetPort: https 14 | protocol: TCP 15 | name: https 16 | - port: {{ .Values.service.metricsPort }} 17 | targetPort: metrics 18 | protocol: TCP 19 | name: metrics 20 | selector: 21 | {{- include "dragonfly-operator.selectorLabels" . | nindent 4 }} 22 | control-plane: controller-manager 23 | -------------------------------------------------------------------------------- /config/samples/v1alpha1_dragonfly_existing_pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dragonflydb.io/v1alpha1 2 | kind: Dragonfly 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: dragonfly 6 | app.kubernetes.io/instance: dragonfly-existing-pvc 7 | app.kubernetes.io/part-of: dragonfly-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | name: dragonfly-existing-pvc 11 | spec: 12 | replicas: 2 13 | snapshot: 14 | # Use an existing PVC instead of creating a new one 15 | existingPersistentVolumeClaimName: dragonfly-snapshots-pvc 16 | cron: "0 */6 * * *" # Snapshot every 6 hours 17 | resources: 18 | requests: 19 | cpu: 500m 20 | memory: 500Mi 21 | limits: 22 | cpu: 600m 23 | memory: 750Mi 24 | 25 | -------------------------------------------------------------------------------- /config/rbac/dragonfly_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit dragonflies. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: dragonfly-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | app.kubernetes.io/part-of: dragonfly-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: dragonfly-editor-role 13 | rules: 14 | - apiGroups: 15 | - dragonflydb.io 16 | resources: 17 | - dragonflies 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - dragonflydb.io 28 | resources: 29 | - dragonflies/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: dragonfly-operator 12 | app.kubernetes.io/part-of: dragonfly-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | app.kubernetes.io/part-of: dragonfly-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/roles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "dragonfly-operator.fullname" . }}-leader-election-role 6 | labels: 7 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 8 | app.kubernetes.io/component: rbac 9 | namespace: {{ include "dragonfly-operator.namespace" . }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - create 20 | - update 21 | - patch 22 | - delete 23 | - apiGroups: 24 | - coordination.k8s.io 25 | resources: 26 | - leases 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - create 32 | - update 33 | - patch 34 | - delete 35 | - apiGroups: 36 | - "" 37 | resources: 38 | - events 39 | verbs: 40 | - create 41 | - patch 42 | 43 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/dragonflydb.io_dragonflies.yaml 6 | #+kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_dragonflies.yaml 12 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_dragonflies.yaml 17 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: dragonfly-operator 3 | description: A Helm chart for dragonfly-operator 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: v1.3.1 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "v1.3.1" 25 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - pods 18 | - services 19 | verbs: 20 | - create 21 | - delete 22 | - get 23 | - list 24 | - patch 25 | - update 26 | - watch 27 | - apiGroups: 28 | - apps 29 | resources: 30 | - statefulsets 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | - apiGroups: 40 | - dragonflydb.io 41 | resources: 42 | - dragonflies 43 | verbs: 44 | - create 45 | - delete 46 | - get 47 | - list 48 | - patch 49 | - update 50 | - watch 51 | - apiGroups: 52 | - dragonflydb.io 53 | resources: 54 | - dragonflies/finalizers 55 | verbs: 56 | - update 57 | - apiGroups: 58 | - dragonflydb.io 59 | resources: 60 | - dragonflies/status 61 | verbs: 62 | - get 63 | - patch 64 | - update 65 | - apiGroups: 66 | - policy 67 | resources: 68 | - poddisruptionbudgets 69 | verbs: 70 | - create 71 | - delete 72 | - get 73 | - list 74 | - patch 75 | - update 76 | - watch 77 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/clusterrolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ include "dragonfly-operator.fullname" . }}-manager-clusterrolebinding 5 | labels: 6 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: rbac 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: {{ include "dragonfly-operator.fullname" . }}-manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "dragonfly-operator.serviceAccountName" . }} 15 | namespace: {{ include "dragonfly-operator.namespace" . }} 16 | --- 17 | 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | kind: ClusterRoleBinding 20 | metadata: 21 | name: {{ include "dragonfly-operator.fullname" . }}-proxy-clusterrolebinding 22 | labels: 23 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 24 | app.kubernetes.io/component: kube-rbac-proxy 25 | roleRef: 26 | apiGroup: rbac.authorization.k8s.io 27 | kind: ClusterRole 28 | name: {{ include "dragonfly-operator.fullname" . }}-proxy-role 29 | subjects: 30 | - kind: ServiceAccount 31 | name: {{ include "dragonfly-operator.serviceAccountName" . }} 32 | namespace: {{ include "dragonfly-operator.namespace" . }} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=dragonflydb.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "dragonflydb.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 8 | app.kubernetes.io/component: controller-manager-metrics 9 | {{- if .Values.serviceMonitor.labels }} 10 | {{- toYaml .Values.serviceMonitor.labels | nindent 4}} 11 | {{- end }} 12 | name: {{ include "dragonfly-operator.fullname" . }}-controller-manager-metrics 13 | spec: 14 | endpoints: 15 | - targetPort: {{ .Values.service.metricsPort }} 16 | {{- if .Values.serviceMonitor.interval }} 17 | interval: {{ .Values.serviceMonitor.interval }} 18 | {{- end }} 19 | {{- if .Values.serviceMonitor.path }} 20 | path: {{ .Values.serviceMonitor.path }} 21 | {{- end }} 22 | {{- if .Values.serviceMonitor.timeout }} 23 | scrapeTimeout: {{ .Values.serviceMonitor.timeout }} 24 | {{- end }} 25 | {{- if .Values.serviceMonitor.relabelings }} 26 | relabelings: 27 | {{- toYaml .Values.serviceMonitor.relabelings | nindent 4 }} 28 | {{- end }} 29 | {{- if .Values.serviceMonitor.metricRelabelings }} 30 | metricRelabelings: 31 | {{- toYaml .Values.serviceMonitor.metricRelabelings | nindent 4 }} 32 | {{- end }} 33 | jobLabel: {{ template "dragonfly-operator.fullname" . }} 34 | namespaceSelector: 35 | matchNames: 36 | - {{ include "dragonfly-operator.namespace" . }} 37 | selector: 38 | matchLabels: 39 | {{- include "dragonfly-operator.selectorLabels" . | nindent 6 }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /internal/resources/image_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | resourcesv1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 8 | "github.com/stretchr/testify/assert" 9 | appsv1 "k8s.io/api/apps/v1" 10 | ) 11 | 12 | func TestGenerateDragonflyResources_ImageResolution(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | crdImage string 16 | defaultImage string 17 | expectedImage string 18 | }{ 19 | { 20 | name: "CRD image takes precedence", 21 | crdImage: "crd-image:v1", 22 | defaultImage: "default-image:v1", 23 | expectedImage: "crd-image:v1", 24 | }, 25 | { 26 | name: "Default image used when CRD image is empty", 27 | crdImage: "", 28 | defaultImage: "default-image:v1", 29 | expectedImage: "default-image:v1", 30 | }, 31 | { 32 | name: "Hardcoded default used when both are empty", 33 | crdImage: "", 34 | defaultImage: "", 35 | expectedImage: fmt.Sprintf("%s:%s", DragonflyImage, Version), 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | df := &resourcesv1.Dragonfly{ 42 | Spec: resourcesv1.DragonflySpec{ 43 | Image: tt.crdImage, 44 | }, 45 | } 46 | 47 | objs, err := GenerateDragonflyResources(df, tt.defaultImage) 48 | assert.NoError(t, err) 49 | 50 | var sts *appsv1.StatefulSet 51 | for _, obj := range objs { 52 | if s, ok := obj.(*appsv1.StatefulSet); ok { 53 | sts = s 54 | break 55 | } 56 | } 57 | 58 | assert.NotNil(t, sts) 59 | assert.Equal(t, tt.expectedImage, sts.Spec.Template.Spec.Containers[0].Image) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Running on the cluster 4 | 5 | 1. Build your image with `IMG` tag: 6 | 7 | ```sh 8 | export IMG=/dragonfly-operator:tag 9 | make docker-build 10 | ``` 11 | 12 | 2. Make the image available to the cluster: 13 | 14 | > **Note** 15 | > 16 | > If you are using `kind`, You can load the image instead of pushing to a registry by running 17 | > 18 | > ```sh 19 | > make docker-kind-load IMG=/dragonfly-operator:tag 20 | > ``` 21 | 22 | ```sh 23 | make docker-push 24 | ``` 25 | 26 | 2. Deploy the controller to the cluster with the image specified by `IMG`: 27 | 28 | ```sh 29 | make deploy 30 | ``` 31 | 32 | 3. Verify that the controller is running, and CRD's are installed: 33 | 34 | ```sh 35 | ➜ watch kubectl -n dragonfly-operator-system get pods 36 | NAME READY STATUS RESTARTS AGE 37 | dragonfly-operator-controller-manager-7b88f9d84b-qnj4c 2/2 Running 0 13m 38 | ➜ kubectl get crds 39 | NAME CREATED AT 40 | dragonflies.dragonflydb.io 2023-04-03T13:29:18Z 41 | ``` 42 | 43 | 3. Install a sample instance of Custom Resource: 44 | 45 | ```sh 46 | kubectl apply -f config/samples/v1alpha1_dragonfly.yaml 47 | 48 | ``` 49 | 50 | 4. Check the status of the instance: 51 | 52 | ```sh 53 | kubectl describe dragonfly dragonfly-sample 54 | ``` 55 | 56 | ## Uninstall CRDs 57 | 58 | To delete the CRDs from the cluster: 59 | 60 | ```sh 61 | make uninstall 62 | ``` 63 | 64 | ## Undeploy controller 65 | 66 | UnDeploy the controller from the cluster: 67 | 68 | ```sh 69 | make undeploy 70 | ``` 71 | -------------------------------------------------------------------------------- /internal/controller/base_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | 22 | dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 23 | "github.com/go-logr/logr" 24 | "k8s.io/apimachinery/pkg/runtime" 25 | "k8s.io/apimachinery/pkg/types" 26 | "k8s.io/client-go/tools/record" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | ) 29 | 30 | type Reconciler struct { 31 | Client client.Client 32 | Scheme *runtime.Scheme 33 | EventRecorder record.EventRecorder 34 | DefaultDragonflyImage string 35 | } 36 | 37 | func (r *Reconciler) getDragonflyInstance(ctx context.Context, namespacedName types.NamespacedName, log logr.Logger) (*DragonflyInstance, error) { 38 | // Retrieve the relevant Dragonfly object 39 | var df dfv1alpha1.Dragonfly 40 | err := r.Client.Get(ctx, namespacedName, &df) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return &DragonflyInstance{ 46 | df: &df, 47 | client: r.Client, 48 | log: log, 49 | scheme: r.Scheme, 50 | eventRecorder: r.EventRecorder, 51 | defaultDragonflyImage: r.DefaultDragonflyImage, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | affinity: 12 | nodeAffinity: 13 | requiredDuringSchedulingIgnoredDuringExecution: 14 | nodeSelectorTerms: 15 | - matchExpressions: 16 | - key: kubernetes.io/arch 17 | operator: In 18 | values: 19 | - amd64 20 | - arm64 21 | - ppc64le 22 | - s390x 23 | - key: kubernetes.io/os 24 | operator: In 25 | values: 26 | - linux 27 | containers: 28 | - name: kube-rbac-proxy 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - "ALL" 34 | image: quay.io/brancz/kube-rbac-proxy:v0.16.0 35 | args: 36 | - "--secure-listen-address=0.0.0.0:8443" 37 | - "--upstream=http://127.0.0.1:8080/" 38 | - "--logtostderr=true" 39 | - "--v=0" 40 | ports: 41 | - containerPort: 8443 42 | protocol: TCP 43 | name: https 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 128Mi 48 | requests: 49 | cpu: 5m 50 | memory: 64Mi 51 | - name: manager 52 | args: 53 | - "--health-probe-bind-address=:8081" 54 | - "--metrics-bind-address=127.0.0.1:8080" 55 | - "--leader-elect" 56 | -------------------------------------------------------------------------------- /internal/resources/resources_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMergeNamedSlices_Containers(t *testing.T) { 12 | base := []corev1.Container{ 13 | {Name: "main", Image: "main:1"}, 14 | {Name: "sidecar", Image: "sidecar:1"}, 15 | } 16 | override := []corev1.Container{ 17 | {Name: "main", Image: "main:custom"}, 18 | {Name: "metrics", Image: "metrics:1"}, 19 | } 20 | 21 | result := mergeNamedSlices(base, override, func(c corev1.Container) string { return c.Name }) 22 | 23 | assert.Len(t, result, 3) 24 | assert.Equal(t, "main:custom", result[0].Image) 25 | assert.Equal(t, "metrics:1", result[1].Image) 26 | assert.Equal(t, "sidecar:1", result[2].Image) 27 | } 28 | 29 | func TestMergeNamedSlices_Volumes(t *testing.T) { 30 | base := []corev1.Volume{ 31 | {Name: "config"}, 32 | {Name: "data"}, 33 | } 34 | override := []corev1.Volume{ 35 | {Name: "config"}, // override 36 | {Name: "logs"}, 37 | } 38 | 39 | result := mergeNamedSlices(base, override, func(v corev1.Volume) string { return v.Name }) 40 | 41 | assert.Len(t, result, 3) 42 | assert.Equal(t, "config", result[0].Name) 43 | assert.Equal(t, "logs", result[1].Name) 44 | assert.Equal(t, "data", result[2].Name) 45 | } 46 | 47 | func TestMergeNamedSlices_EmptyOverride(t *testing.T) { 48 | base := []corev1.Volume{ 49 | {Name: "default"}, 50 | } 51 | override := []corev1.Volume{} 52 | 53 | result := mergeNamedSlices(base, override, func(v corev1.Volume) string { return v.Name }) 54 | 55 | assert.Len(t, result, 1) 56 | assert.Equal(t, "default", result[0].Name) 57 | } 58 | 59 | func TestMergeNamedSlices_EmptyBase(t *testing.T) { 60 | base := []corev1.Container{} 61 | override := []corev1.Container{ 62 | {Name: "user", Image: "user:1"}, 63 | } 64 | 65 | result := mergeNamedSlices(base, override, func(c corev1.Container) string { return c.Name }) 66 | 67 | assert.Len(t, result, 1) 68 | assert.Equal(t, "user", result[0].Name) 69 | } 70 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/grafanadashboards.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.grafanaDashboard.enabled -}} 2 | {{- $files := .Files.Glob "dashboards/*.json" }} 3 | {{- if $files }} 4 | --- 5 | apiVersion: v1 6 | kind: ConfigMapList 7 | items: 8 | {{- range $path, $fileContents := $files }} 9 | {{- $dashboardName := regexReplaceAll "(^.*/)(.*)\\.json$" $path "${2}" | lower }} 10 | - apiVersion: v1 11 | kind: ConfigMap 12 | metadata: 13 | annotations: 14 | {{ $.Values.grafanaDashboard.annotations.name }}: {{ $.Values.grafanaDashboard.folder }} 15 | labels: 16 | {{ $.Values.grafanaDashboard.labels.name }}: {{ $dashboardName }} 17 | {{- include "dragonfly-operator.labels" $ | nindent 6 }} 18 | app.kubernetes.io/component: dashboard 19 | name: {{ printf "dashboard-dragonfly-operator-%s" $dashboardName | trunc 63 | trimSuffix "-" }} 20 | namespace: {{ include "dragonfly-operator.namespace" $ }} 21 | data: 22 | {{ $dashboardName }}.json: |- 23 | {{ $.Files.Get $path | indent 6}} 24 | {{- end }} 25 | {{ if $.Values.grafanaDashboard.grafanaOperator.enabled -}} 26 | {{- range $path, $fileContents := $files }} 27 | {{- $dashboardName := regexReplaceAll "(^.*/)(.*)\\.json$" $path "${2}" | lower }} 28 | --- 29 | apiVersion: grafana.integreatly.org/v1beta1 30 | kind: GrafanaDashboard 31 | metadata: 32 | labels: 33 | {{- include "dragonfly-operator.labels" $ | nindent 4 }} 34 | app.kubernetes.io/component: dashboard 35 | name: {{ printf "dragonfly-operator-%s" $dashboardName | trunc 63 | trimSuffix "-" }} 36 | namespace: {{ include "dragonfly-operator.namespace" $ }} 37 | spec: 38 | allowCrossNamespaceImport: {{ $.Values.grafanaDashboard.grafanaOperator.allowCrossNamespaceImport }} 39 | folder: {{ $.Values.grafanaDashboard.folder }} 40 | instanceSelector: 41 | matchLabels: 42 | {{- toYaml $.Values.grafanaDashboard.grafanaOperator.matchLabels | nindent 6 }} 43 | configMapRef: 44 | name: {{ printf "dashboard-dragonfly-operator-%s" $dashboardName | trunc 63 | trimSuffix "-" }} 45 | key: {{ $dashboardName }}.json 46 | {{- end }} 47 | {{- end }} 48 | {{- end }} 49 | {{- end }} 50 | -------------------------------------------------------------------------------- /monitoring/podMonitorGuide.md: -------------------------------------------------------------------------------- 1 | # PodMonitor 2 | 3 | When using Dragonfly operator, sometimes there may be a need to monitor the dragonfly instances. PodMonitors can be of great use here. In this doc, we will see how we can configure PodMonitor resource to monitor dragonfly instances. 4 | 5 | ## Install Prometheus Operator 6 | 7 | First make sure you have all the prometheus crds installed in your cluster. Here we are going to install prometheus operator. 8 | 9 | ```bash 10 | LATEST=$(curl -s https://api.github.com/repos/prometheus-operator/prometheus-operator/releases/latest | jq -cr .tag_name) 11 | curl -sL https://github.com/prometheus-operator/prometheus-operator/releases/download/${LATEST}/bundle.yaml | kubectl create -f - 12 | ``` 13 | 14 | ## Create prerequisite resources 15 | 16 | Now that we have installed the operator, we can create `prometheus` resources. If you have RBAC enabled, create necessary `serviceaccount`, `clusterrole` and `clusterrolebinding` resources first. 17 | 18 | ```bash 19 | kubectl apply -f monitoring/promServiceAccount.yaml 20 | kubectl apply -f monitoring/promClusterRole.yaml 21 | kubectl apply -f monitoring/promClusterBinding.yaml 22 | ``` 23 | 24 | This will allow prometheus to scrape data from dragonfly resources. Once we 25 | configured the RBAC, we can now create a PodMonitor resource. 26 | 27 | ```bash 28 | kubectl apply -f monitoring/podMonitor.yaml 29 | ``` 30 | 31 | Note that we must specify `app` label under the `matchLabels` (`selector`) field. It is used to target the desired dragonfly instances. The value of `app` label is the name of your dragonfly resource name (in this case, `dragonfly-sample`). 32 | 33 | Dragonfly resources expose a port named `admin` and you can use it as the endpoint for PodMonitor. 34 | 35 | ## Create Dragonfly resource 36 | 37 | We can now create dragonfly resources and Prometheus will automatically scrap and monitor the created resources. 38 | 39 | ```bash 40 | kubectl apply -f config/samples/v1alpha1_dragonfly.yaml 41 | ``` 42 | 43 | If you want to view or query scraped data in the localhost, run the below command: 44 | 45 | ```bash 46 | kubectl port-forward prometheus-prometheus-0 9090:9090 47 | ``` 48 | 49 | Now go to `localhost:9090`. You'll see the prometheus dashboard. 50 | -------------------------------------------------------------------------------- /e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 27 | "k8s.io/client-go/kubernetes" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | "k8s.io/client-go/tools/clientcmd" 31 | "k8s.io/client-go/util/homedir" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | logf "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | //+kubebuilder:scaffold:imports 36 | ) 37 | 38 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 39 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 40 | 41 | var cfg *rest.Config 42 | var k8sClient client.Client 43 | var clientset *kubernetes.Clientset 44 | 45 | func TestAPIs(t *testing.T) { 46 | RegisterFailHandler(Fail) 47 | 48 | RunSpecs(t, "Controller Suite") 49 | } 50 | 51 | var _ = BeforeSuite(func() { 52 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 53 | // This expects a test environment with operator installed 54 | var kubeconfig string 55 | var err error 56 | kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config") 57 | 58 | // use the current context in kubeconfig 59 | cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 60 | if err != nil { 61 | panic(err.Error()) 62 | } 63 | 64 | err = dfv1alpha1.AddToScheme(scheme.Scheme) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | // create the clientset 68 | clientset, err = kubernetes.NewForConfig(cfg) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 72 | Expect(err).NotTo(HaveOccurred()) 73 | }) 74 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/clusterroles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "dragonfly-operator.fullname" . }}-manager-role 6 | labels: 7 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 8 | app.kubernetes.io/component: rbac 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - events 14 | verbs: 15 | - create 16 | - patch 17 | - apiGroups: 18 | - "" 19 | resources: 20 | - pods 21 | verbs: 22 | - create 23 | - delete 24 | - get 25 | - list 26 | - patch 27 | - update 28 | - watch 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - services 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - apps 43 | resources: 44 | - statefulsets 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - policy 55 | resources: 56 | - poddisruptionbudgets 57 | verbs: 58 | - create 59 | - delete 60 | - get 61 | - list 62 | - patch 63 | - update 64 | - watch 65 | - apiGroups: 66 | - dragonflydb.io 67 | resources: 68 | - dragonflies 69 | verbs: 70 | - create 71 | - delete 72 | - get 73 | - list 74 | - patch 75 | - update 76 | - watch 77 | - apiGroups: 78 | - dragonflydb.io 79 | resources: 80 | - dragonflies/finalizers 81 | verbs: 82 | - update 83 | - apiGroups: 84 | - dragonflydb.io 85 | resources: 86 | - dragonflies/status 87 | verbs: 88 | - get 89 | - patch 90 | - update 91 | --- 92 | apiVersion: rbac.authorization.k8s.io/v1 93 | kind: ClusterRole 94 | metadata: 95 | name: {{ include "dragonfly-operator.fullname" . }}-metrics-reader 96 | labels: 97 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 98 | app.kubernetes.io/component: kube-rbac-proxy 99 | rules: 100 | - nonResourceURLs: 101 | - /metrics 102 | verbs: 103 | - get 104 | --- 105 | apiVersion: rbac.authorization.k8s.io/v1 106 | kind: ClusterRole 107 | metadata: 108 | name: {{ include "dragonfly-operator.fullname" . }}-proxy-role 109 | labels: 110 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 111 | app.kubernetes.io/component: kube-rbac-proxy 112 | rules: 113 | - apiGroups: 114 | - authentication.k8s.io 115 | resources: 116 | - tokenreviews 117 | verbs: 118 | - create 119 | - apiGroups: 120 | - authorization.k8s.io 121 | resources: 122 | - subjectaccessreviews 123 | verbs: 124 | - create 125 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "dragonfly-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "dragonfly-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "dragonfly-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "dragonfly-operator.labels" -}} 37 | helm.sh/chart: {{ include "dragonfly-operator.chart" . }} 38 | {{ include "dragonfly-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | app.kubernetes.io/created-by: {{ include "dragonfly-operator.name" . }} 44 | app.kubernetes.io/part-of: {{ include "dragonfly-operator.name" . }} 45 | {{- if .Values.additionalLabels }} 46 | {{ toYaml .Values.additionalLabels }} 47 | {{- end }} 48 | {{- end }} 49 | 50 | {{/* 51 | Selector labels 52 | */}} 53 | {{- define "dragonfly-operator.selectorLabels" -}} 54 | app.kubernetes.io/name: {{ include "dragonfly-operator.name" . }} 55 | app.kubernetes.io/instance: {{ .Release.Name }} 56 | 57 | {{- end }} 58 | 59 | {{/* 60 | Create the name of the service account to use 61 | */}} 62 | {{- define "dragonfly-operator.serviceAccountName" -}} 63 | {{- if .Values.serviceAccount.create }} 64 | {{- default (include "dragonfly-operator.fullname" .) .Values.serviceAccount.name }} 65 | {{- else }} 66 | {{- default "default" .Values.serviceAccount.name }} 67 | {{- end }} 68 | {{- end }} 69 | 70 | {{/* 71 | Limit controller service name to 63 characters by truncating fullname at 48 characters to comply with DNS naming spec 72 | Suffix + 48 char fullname = max 63 characters 73 | */}} 74 | {{- define "dragonfly-operator.controllerServiceName" -}} 75 | {{- printf "%s-controller-svc" (include "dragonfly-operator.fullname" . | trunc 48 | trimSuffix "-" ) -}} 76 | {{- end -}} 77 | 78 | {{/* 79 | Allow the release namespace to be overridden for multi-namespace deployments in combined charts 80 | */}} 81 | {{- define "dragonfly-operator.namespace" -}} 82 | {{- if .Values.namespaceOverride -}} 83 | {{- .Values.namespaceOverride -}} 84 | {{- else -}} 85 | {{- .Release.Namespace -}} 86 | {{- end -}} 87 | {{- end -}} 88 | -------------------------------------------------------------------------------- /dragonfly-with-tls.md: -------------------------------------------------------------------------------- 1 | # Dragonfly With Server TLS 2 | 3 | ## Generate a Cert with Cert Manager 4 | 5 | You can also skip this step and use the self-signed cert that is generated manually. The Operator expects 6 | a relevant secret to be present with `tls.crt`, `tls.key` keys. 7 | 8 | ### Install Cert Manager 9 | 10 | ```sh 11 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml 12 | ``` 13 | 14 | Create a TLS secret to be used 15 | 16 | ```sh 17 | kubectl apply -f - < 6379/TCP 23h 33 | service/grafana-ui ClusterIP 10.96.146.235 80/TCP 23h 34 | service/kubernetes ClusterIP 10.96.0.1 443/TCP 33d 35 | service/prometheus ClusterIP 10.96.93.14 9090/TCP 20h 36 | service/prometheus-operated ClusterIP None 9090/TCP 167m 37 | service/prometheus-operator ClusterIP None 8080/TCP 8d 38 | 39 | NAME READY UP-TO-DATE AVAILABLE AGE 40 | deployment.apps/grafana-ui 1/1 1 1 23h 41 | deployment.apps/prometheus-operator 1/1 1 1 8d 42 | 43 | NAME DESIRED CURRENT READY AGE 44 | replicaset.apps/grafana-ui-785f79fb65 1 1 1 23h 45 | replicaset.apps/prometheus-operator-744c6bb8f9 1 1 1 8d 46 | 47 | NAME READY AGE 48 | statefulset.apps/dragonfly-sample 2/2 23h 49 | statefulset.apps/prometheus-prometheus 1/1 167m 50 | ``` 51 | 52 | ## Step 2: Create Prometheus Service 53 | 54 | Create a prometheus Service to let Grafana access prometheus trough it. 55 | 56 | ``` 57 | kubectl apply -f monitoring/prometheus-service.yaml 58 | ``` 59 | 60 | Note that it is required to use `prometheus: prometheus` label as selector (if no label is given to the existing Prometheus object). The prometheus operator exposes 61 | a port named `web`, so you can use this port as targetPort in the service. 62 | 63 | ## Step 3: Create Grafana dashboard 64 | 65 | First, port-forward the grafana service so that you can access the UI from localhost. 66 | 67 | ``` 68 | kubectl port-forward services/grafana-ui 3000:80 69 | ``` 70 | 71 | Now go to `localhost:3000` and add a Prometheus data source. Use `http://prometheus-svc:9090` as the datasource url. After that, import `grafana-dashboard.json` to the dashboard. -------------------------------------------------------------------------------- /hack/print-roles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/redis/go-redis/v9" 14 | "github.com/redis/go-redis/v9/maintnotifications" 15 | corev1 "k8s.io/api/core/v1" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/rest" 19 | "k8s.io/client-go/tools/clientcmd" 20 | "k8s.io/client-go/tools/portforward" 21 | "k8s.io/client-go/transport/spdy" 22 | "k8s.io/client-go/util/homedir" 23 | ) 24 | 25 | func main() { 26 | var kubeconfig *string 27 | if home := homedir.HomeDir(); home != "" { 28 | kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") 29 | } else { 30 | kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") 31 | } 32 | flag.Parse() 33 | 34 | // use the current context in kubeconfig 35 | config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) 36 | if err != nil { 37 | panic(err.Error()) 38 | } 39 | 40 | // create the clientset 41 | clientset, err := kubernetes.NewForConfig(config) 42 | if err != nil { 43 | panic(err.Error()) 44 | } 45 | 46 | dbClient := flag.Arg(0) 47 | 48 | if dbClient == "" { 49 | panic("No dragonfly object specified") 50 | } 51 | 52 | pods, err := clientset.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{ 53 | LabelSelector: fmt.Sprintf("app=%s", dbClient), 54 | }) 55 | if err != nil { 56 | panic(err.Error()) 57 | } 58 | 59 | startPort := 6379 60 | 61 | for i, pod := range pods.Items { 62 | err := portForward(context.Background(), clientset, config, pod, startPort+i) 63 | if err != nil { 64 | panic(err.Error()) 65 | } 66 | 67 | role, err := getRole(context.Background(), fmt.Sprintf("localhost:%d", startPort+i)) 68 | if err != nil { 69 | panic(err.Error()) 70 | } 71 | 72 | fmt.Printf("%s, %s, %s: %s\n", pod.Name, pod.Status.PodIP, pod.Labels["role"], role) 73 | } 74 | } 75 | 76 | func portForward(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod corev1.Pod, port int) error { 77 | url := clientset.CoreV1().RESTClient().Post(). 78 | Resource("pods"). 79 | Namespace(pod.Namespace). 80 | Name(pod.Name). 81 | SubResource("portforward"). 82 | URL() 83 | 84 | transport, upgrader, err := spdy.RoundTripperFor(config) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url) 90 | ports := []string{fmt.Sprintf("%d:%d", port, 9999)} 91 | readyChan := make(chan struct{}, 1) 92 | stopChan := make(chan struct{}, 1) 93 | 94 | fw, err := portforward.New(dialer, ports, stopChan, readyChan, io.Discard, os.Stderr) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | errChan := make(chan error, 1) 100 | go func() { errChan <- fw.ForwardPorts() }() 101 | 102 | select { 103 | case err = <-errChan: 104 | return errors.Wrap(err, "port forwarding failed") 105 | case <-fw.Ready: 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func getRole(ctx context.Context, url string) (string, error) { 112 | redisClient := redis.NewClient(&redis.Options{ 113 | Addr: url, 114 | MaintNotificationsConfig: &maintnotifications.Config{ 115 | Mode: maintnotifications.ModeDisabled, 116 | }, 117 | }) 118 | defer redisClient.Close() 119 | 120 | resp, err := redisClient.Info(ctx, "replication").Result() 121 | if err != nil { 122 | return "", err 123 | } 124 | 125 | return resp, nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/resources/const.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resources 18 | 19 | import "fmt" 20 | 21 | const ( 22 | // DragonflyPortName is the name of the port on which the Dragonfly instance listens 23 | DragonflyPortName = "redis" 24 | // DragonflyPort is the port on which Dragonfly listens 25 | DragonflyPort = 6379 26 | 27 | DragonflyAdminPortName = "admin" 28 | // DragonflyAdminPort is the admin port on which Dragonfly listens 29 | // IMPORTANT: This port should not be opened to non trusted networks. 30 | DragonflyAdminPort = 9999 31 | 32 | MemcachedPortName = "memcached" 33 | MemcachedPortArg = "--memcached_port" 34 | 35 | DragonflyContainerName = "dragonfly" 36 | 37 | AclVolumeName = "dragonfly-acl" 38 | AclDir = "/var/lib/dragonfly" 39 | AclFileName = "dragonfly.acl" 40 | AclFileArg = "--aclfile" 41 | 42 | SnapshotsVolumeName = "df" 43 | SnapshotsDir = "/dragonfly/snapshots" 44 | SnapshotsDirArg = "--dir" 45 | SnapshotsCronArg = "--snapshot_cron" 46 | 47 | TLSVolumeName = "dragonfly-tls" 48 | TLSDir = "/etc/dragonfly-tls" 49 | TLSCACertPathArg = "--tls_ca_cert_file" 50 | TLSCACertDir = "/etc/dragonfly/tls" 51 | TLSCACertFileName = "ca.crt" 52 | TLSCACertVolumeName = "client-ca-cert" 53 | TLSCertPathArg = "--tls_cert_file" 54 | TLSCertFileName = "tls.crt" 55 | TLSKeyPathArg = "--tls_key_file" 56 | TLSKeyFileName = "tls.key" 57 | TLSArg = "--tls" 58 | NoTLSOnAdminPortArg = "--no_tls_on_admin_port" 59 | 60 | // DragonflyOperatorName is the name of the operator 61 | DragonflyOperatorName = "dragonfly-operator" 62 | 63 | // DragonflyImage is the default image of the Dragonfly to use 64 | DragonflyImage = "docker.dragonflydb.io/dragonflydb/dragonfly" 65 | 66 | // Recommended Kubernetes Application Labels 67 | // KubernetesAppNameLabel is the name of the application 68 | KubernetesAppNameLabelKey = "app.kubernetes.io/name" 69 | KubernetesAppName = "dragonfly" 70 | 71 | // KubernetesAppVersionLabel is the version of the application 72 | KubernetesAppVersionLabelKey = "app.kubernetes.io/version" 73 | 74 | // KubernetesAppComponentLabel is the component of the application 75 | KubernetesAppComponentLabelKey = "app.kubernetes.io/component" 76 | KubernetesAppComponent = "dragonfly" 77 | 78 | KubernetesAppInstanceLabelKey = "app.kubernetes.io/instance" 79 | 80 | // KubernetesManagedByLabel is the tool being used to manage the operation of an application 81 | KubernetesManagedByLabelKey = "app.kubernetes.io/managed-by" 82 | 83 | // KubernetesPartOfLabel is the name of a higher level application this one is part of 84 | KubernetesPartOfLabelKey = "app.kubernetes.io/part-of" 85 | KubernetesPartOf = "dragonfly" 86 | 87 | MasterIpLabelKey = "master-ip" 88 | DragonflyNameLabelKey = "app" 89 | 90 | MasterIpAnnotationKey = "operator.dragonflydb.io/masterIP" 91 | 92 | RoleLabelKey = "role" 93 | 94 | Master = "master" 95 | 96 | Replica = "replica" 97 | ) 98 | 99 | var DefaultDragonflyArgs = []string{ 100 | "--alsologtostderr", 101 | "--break_replication_on_master_restart=true", 102 | "--primary_port_http_enabled=false", 103 | fmt.Sprintf("--admin_port=%d", DragonflyAdminPort), 104 | "--admin_nopass", 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dragonflydb/dragonfly-operator 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.3 7 | github.com/onsi/ginkgo/v2 v2.27.1 8 | github.com/onsi/gomega v1.38.2 9 | github.com/pkg/errors v0.9.1 10 | github.com/redis/go-redis/v9 v9.16.0 11 | github.com/stretchr/testify v1.11.1 12 | k8s.io/api v0.32.3 13 | k8s.io/apimachinery v0.32.3 14 | k8s.io/client-go v0.32.3 15 | sigs.k8s.io/controller-runtime v0.20.4 16 | ) 17 | 18 | require ( 19 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 25 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 26 | github.com/fsnotify/fsnotify v1.8.0 // indirect 27 | github.com/fxamacker/cbor/v2 v2.7.1 // indirect 28 | github.com/go-logr/zapr v1.3.0 // indirect 29 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 30 | github.com/go-openapi/jsonreference v0.21.0 // indirect 31 | github.com/go-openapi/swag v0.23.1 // indirect 32 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 33 | github.com/gogo/protobuf v1.3.2 // indirect 34 | github.com/golang/protobuf v1.5.4 // indirect 35 | github.com/google/btree v1.1.3 // indirect 36 | github.com/google/gnostic-models v0.6.9 // indirect 37 | github.com/google/go-cmp v0.7.0 // indirect 38 | github.com/google/gofuzz v1.2.0 // indirect 39 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 40 | github.com/google/uuid v1.6.0 // indirect 41 | github.com/gorilla/websocket v1.5.3 // indirect 42 | github.com/josharian/intern v1.0.0 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/klauspost/compress v1.18.0 // indirect 45 | github.com/mailru/easyjson v0.9.0 // indirect 46 | github.com/moby/spdystream v0.5.0 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 50 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 51 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 52 | github.com/prometheus/client_golang v1.21.1 // indirect 53 | github.com/prometheus/client_model v0.6.1 // indirect 54 | github.com/prometheus/common v0.63.0 // indirect 55 | github.com/prometheus/procfs v0.16.0 // indirect 56 | github.com/spf13/pflag v1.0.6 // indirect 57 | github.com/x448/float16 v0.8.4 // indirect 58 | go.uber.org/automaxprocs v1.6.0 // indirect 59 | go.uber.org/multierr v1.11.0 // indirect 60 | go.uber.org/zap v1.27.0 // indirect 61 | go.yaml.in/yaml/v3 v3.0.4 // indirect 62 | golang.org/x/mod v0.27.0 // indirect 63 | golang.org/x/net v0.43.0 // indirect 64 | golang.org/x/oauth2 v0.28.0 // indirect 65 | golang.org/x/sync v0.16.0 // indirect 66 | golang.org/x/sys v0.35.0 // indirect 67 | golang.org/x/term v0.34.0 // indirect 68 | golang.org/x/text v0.28.0 // indirect 69 | golang.org/x/time v0.11.0 // indirect 70 | golang.org/x/tools v0.36.0 // indirect 71 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 72 | google.golang.org/protobuf v1.36.7 // indirect 73 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 74 | gopkg.in/inf.v0 v0.9.1 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | k8s.io/apiextensions-apiserver v0.32.3 // indirect 77 | k8s.io/klog/v2 v2.130.1 // indirect 78 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 79 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 80 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 81 | sigs.k8s.io/randfill v1.0.0 // indirect 82 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 83 | sigs.k8s.io/yaml v1.4.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: dragonfly-operator 10 | app.kubernetes.io/part-of: dragonfly-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: dragonfly-operator 25 | app.kubernetes.io/part-of: dragonfly-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /s3.md: -------------------------------------------------------------------------------- 1 | # Configure Snapshots to S3 with the Dragonfly Operator 2 | 3 | In this guide, We will see how to configure the Dragonfly Instances to use S3 as a backup location with the 4 | Dragonfly Operator. While just having the AWS credentials in the environment (through a file or env) is enough 5 | to use S3, In this guide we will use [AWS IAM roles for service accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) to provide the credentials to the Dragonfly 6 | pod. 7 | 8 | As this mechanism uses OIDC (OpenID Connect) to authenticate the service account, we will also get the benefits of 9 | credentials isolation and automatic rotation of credentials. This way we can avoid having to pass long lived credentials. This is all done automatically by EKS. 10 | 11 | ## Create an EKS cluster 12 | 13 | ```bash 14 | eksctl create cluster --name df-storage --region us-east-1 15 | ``` 16 | 17 | ## Create and Associate IAM OIDC Provider for your cluster 18 | 19 | By following the [AWS documentation](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html), create and associate an IAM OIDC provider for your cluster. 20 | This is a required step for the next steps to work. 21 | 22 | ## Create an S3 bucket 23 | 24 | Now, we will create an S3 bucket to store the snapshots. This bucket can be created using the AWS console or using the AWS CLI. 25 | 26 | ```bash 27 | aws s3api create-bucket --bucket dragonfly-backup --region us-east-1 28 | ``` 29 | 30 | ## Create a policy to read a specific S3 bucket 31 | 32 | We will now create a policy that allows the Dragonfly Instance to read and write to the S3 bucket we created in the previous step. 33 | 34 | ```bash 35 | cat < policy.json 36 | { 37 | "Version": "2012-10-17", 38 | "Statement": [ 39 | { 40 | "Effect": "Allow", 41 | "Action": "s3:*", 42 | "Resource": [ 43 | "arn:aws:s3:::dragonfly-backup/*", 44 | "arn:aws:s3:::dragonfly-backup" 45 | ] 46 | } 47 | ] 48 | } 49 | EOF 50 | ``` 51 | 52 | ```bash 53 | aws iam create-policy --policy-name dragonfly-backup --policy-document file://policy.json 54 | ``` 55 | 56 | ## Associate the policy with a role 57 | 58 | Now, we will associate the policy we created in the previous step with a role. This role will be used by the service account called `dragonfly-backup` which will also be created in this step. 59 | 60 | ```bash 61 | eksctl create iamserviceaccount --name dragonfly-backup --namespace default --cluster df-storage --role-name dragonfly-backup --attach-policy-arn arn:aws:iam:::policy/dragonfly-backup --approve 62 | ``` 63 | 64 | ## Create a Dragonfly Instance with that service account 65 | 66 | Let's create a Dragonfly Instance with the service account we created in the previous step. We will also configure the snapshot location to be the S3 bucket we created in the previous steps. 67 | 68 | Important to note that this feature is only available from the `v1.12.0` version of the Dragonfly. Currently, We will 69 | use the weekly release of Dragonfly to use this feature. 70 | 71 | ```bash 72 | kubectl apply -f - < 80 | snapshot: 81 | dir: "s3://dragonfly-backup" 82 | EOF 83 | ``` 84 | 85 | ## Verify that the Dragonfly Instance is running 86 | 87 | ```bash 88 | kubectl describe dragonfly dragonfly-sample 89 | ``` 90 | 91 | ## Load Data and Terminate the Dragonfly Instance 92 | 93 | Now, we will load some data into the Dragonfly Instance and then terminate the Dragonfly Instance. 94 | 95 | ```bash 96 | kubectl run -it --rm --restart=Never redis-cli --image=redis:7.0.10 -- redis-cli -h dragonfly-sample.default SET 1 2 97 | ``` 98 | 99 | ```bash 100 | kubectl delete pod dragonfly-sample-0 101 | ``` 102 | 103 | ## Verification 104 | 105 | ### Verify that the backups are created in the S3 bucket 106 | 107 | ```bash 108 | aws s3 ls s3://dragonfly-backup 109 | ``` 110 | 111 | ### Verify that the data is automatically restored 112 | 113 | ```bash 114 | kubectl run -it --rm --restart=Never redis-cli --image=redis:7.0.10 -- redis-cli -h dragonfly-sample.default GET 1 115 | ``` 116 | 117 | As you can see, the data is automatically restored from the S3 bucket. This is because the Dragonfly instance is configured to use the S3 bucket as the snapshot location. 118 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "dragonfly-operator.fullname" . }} 5 | labels: 6 | {{- include "dragonfly-operator.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: controller 8 | control-plane: controller-manager 9 | {{- with .Values.deploymentLabels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.deploymentAnnotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | namespace: {{ include "dragonfly-operator.namespace" . }} 17 | spec: 18 | replicas: {{ .Values.replicaCount }} 19 | selector: 20 | matchLabels: 21 | {{- include "dragonfly-operator.selectorLabels" . | nindent 6 }} 22 | control-plane: controller-manager 23 | template: 24 | metadata: 25 | {{- with .Values.podAnnotations }} 26 | annotations: 27 | {{- toYaml . | nindent 8 }} 28 | {{- end }} 29 | labels: 30 | {{- include "dragonfly-operator.labels" . | nindent 8 }} 31 | control-plane: controller-manager 32 | {{- with .Values.podLabels }} 33 | {{- toYaml . | nindent 8 }} 34 | {{- end }} 35 | spec: 36 | {{- if .Values.manager.priorityClassName }} 37 | priorityClassName: {{ .Values.manager.priorityClassName }} 38 | {{- end }} 39 | {{- with .Values.imagePullSecrets }} 40 | imagePullSecrets: 41 | {{- toYaml . | nindent 8 }} 42 | {{- end }} 43 | serviceAccountName: {{ include "dragonfly-operator.serviceAccountName" . }} 44 | terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} 45 | securityContext: 46 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 47 | containers: 48 | {{- if .Values.rbacProxy.enabled }} 49 | - args: 50 | - --secure-listen-address=0.0.0.0:8443 51 | - --upstream=http://127.0.0.1:8080/ 52 | - --logtostderr=true 53 | - --v=0 54 | {{- with .Values.rbacProxy.extraArgs}} 55 | {{- toYaml . | nindent 12 }} 56 | {{- end }} 57 | image: "{{ .Values.rbacProxy.image.repository }}:{{ .Values.rbacProxy.image.tag }}" 58 | imagePullPolicy: {{ .Values.rbacProxy.image.pullPolicy }} 59 | name: kube-rbac-proxy 60 | securityContext: 61 | {{- toYaml .Values.rbacProxy.securityContext | nindent 12 }} 62 | ports: 63 | - containerPort: 8443 64 | name: https 65 | protocol: TCP 66 | resources: 67 | {{- toYaml .Values.rbacProxy.resources | nindent 12 }} 68 | {{- end }} 69 | - name: manager 70 | args: 71 | - --leader-elect 72 | {{- if .Values.dragonflyImage }} 73 | - --dragonfly-image={{ .Values.dragonflyImage }} 74 | {{- end }} 75 | {{- if .Values.rbacProxy.enabled }} 76 | - --metrics-bind-address=127.0.0.1:8080 77 | {{- end }} 78 | {{- with .Values.manager.extraArgs}} 79 | {{- toYaml . | nindent 12 }} 80 | {{- end }} 81 | command: 82 | - /manager 83 | securityContext: 84 | {{- toYaml .Values.manager.securityContext | nindent 12 }} 85 | image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}" 86 | imagePullPolicy: {{ .Values.manager.image.pullPolicy }} 87 | livenessProbe: 88 | {{- toYaml .Values.manager.livenessProbe | nindent 12 }} 89 | readinessProbe: 90 | {{- toYaml .Values.manager.readinessProbe | nindent 12 }} 91 | ports: 92 | - containerPort: 8080 93 | name: metrics 94 | protocol: TCP 95 | resources: 96 | {{- toYaml .Values.manager.resources | nindent 12 }} 97 | {{- with .Values.manager.volumeMounts }} 98 | volumeMounts: 99 | {{- toYaml . | nindent 12 }} 100 | {{- end }} 101 | {{- with .Values.manager.volumes }} 102 | volumes: 103 | {{- toYaml . | nindent 8 }} 104 | {{- end }} 105 | {{- with .Values.manager.nodeSelector }} 106 | nodeSelector: 107 | {{- toYaml . | nindent 8 }} 108 | {{- end }} 109 | {{- with .Values.manager.affinity }} 110 | affinity: 111 | {{- toYaml . | nindent 8 }} 112 | {{- end }} 113 | {{- with .Values.manager.tolerations }} 114 | tolerations: 115 | {{- toYaml . | nindent 8 }} 116 | {{- end }} 117 | {{- with .Values.manager.topologySpreadConstraints }} 118 | topologySpreadConstraints: 119 | {{- toYaml . | nindent 8 }} 120 | {{- end }} 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Dragonfly 5 | 6 |

7 | 8 | Dragonfly Operator is a Kubernetes operator used to deploy and manage [Dragonfly](https://dragonflydb.io/) instances inside your Kubernetes clusters. 9 | Main features include: 10 | 11 | - Automatic failover 12 | - Scaling horizontally and vertically with custom rollout strategy 13 | - Authentication and server TLS 14 | - Automatic snapshots to PVCs and S3 15 | - Monitoring with Prometheus and Grafana 16 | - Comprehensive configuration options 17 | 18 | You can find more information about Dragonfly in the [official documentation](https://dragonflydb.io/docs/). 19 | There is also a dedicated [Dragonfly Operator section](https://www.dragonflydb.io/docs/managing-dragonfly/operator/installation) 20 | that contains more details and examples on how to use the operator. 21 | 22 | ## Installation 23 | 24 | Make sure to have your Kubernetes cluster up and running. Dragonfly Operator can be installed by running 25 | 26 | ```sh 27 | # Install the CRD and Operator 28 | kubectl apply -f https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/main/manifests/dragonfly-operator.yaml 29 | ``` 30 | 31 | By default, the operator will be installed in the `dragonfly-operator-system` namespace. 32 | 33 | ## Usage 34 | 35 | ### Creating a Dragonfly instance 36 | 37 | To create a sample Dragonfly instance, you can run the following command: 38 | 39 | ```sh 40 | kubectl apply -f https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/main/config/samples/v1alpha1_dragonfly.yaml 41 | ``` 42 | 43 | This will create a Dragonfly instance with 3 replicas. You can check the status of the instance by running 44 | 45 | ```sh 46 | kubectl describe dragonflies.dragonflydb.io dragonfly-sample 47 | ``` 48 | 49 | A service of the form `..svc.cluster.local` will be created, that selects the master instance. You can use this service to connect to the cluster. As pods are added/removed, the service will automatically update to point to the new master. 50 | 51 | #### Connecting with `redis-cli` 52 | 53 | To connect to the cluster using `redis-cli`, you can run: 54 | 55 | ```sh 56 | kubectl run -it --rm --restart=Never redis-cli --image=redis:7.0.10 -- redis-cli -h dragonfly-sample.default 57 | ``` 58 | 59 | This will create a temporary pod that runs `redis-cli` and connects to the cluster. After pressing `shift + R`, You can then run Redis commands as 60 | usual. For example, to set a key and get it back, you can run 61 | 62 | ```sh 63 | If you don't see a command prompt, try pressing enter. 64 | dragonfly-sample.default:6379> GET 1 65 | (nil) 66 | dragonfly-sample.default:6379> SET 1 2 67 | OK 68 | dragonfly-sample.default:6379> GET 1 69 | "2" 70 | dragonfly-sample.default:6379> exit 71 | pod "redis-cli" deleted 72 | ``` 73 | 74 | ### Scaling up/down the number of replicas 75 | 76 | To scale up/down the number of replicas, you can edit the `spec.replicas` field in the Dragonfly instance. For example, to scale up to 5 replicas, you can run 77 | 78 | ```sh 79 | kubectl patch dragonfly dragonfly-sample --type merge -p '{"spec":{"replicas":5}}' 80 | ``` 81 | 82 | ### Vertically scaling the instance 83 | 84 | To vertically scale the instance, you can edit the `spec.resources` field in the Dragonfly instance. For example, to increase the memory limit to 2GiB, you can run 85 | 86 | ```sh 87 | kubectl patch dragonfly dragonfly-sample --type merge -p '{"spec":{"resources":{"requests":{"memory":"1Gi"},"limits":{"memory":"2Gi"}}}}' 88 | ``` 89 | 90 | ### Configuring instance authentication 91 | 92 | To add authentication to the dragonfly pods, you either set the `DFLY_requirepass` environment variable, or add the `--requirepass` argument. 93 | 94 | ### Deleting a Dragonfly instance 95 | 96 | To delete a Dragonfly instance, you can run 97 | 98 | ```sh 99 | kubectl delete dragonfly dragonfly-sample 100 | ``` 101 | 102 | This will automatically delete all the resources (i.e pods and services) associated with the instance. 103 | 104 | ### Uninstalling the operator 105 | 106 | To uninstall the operator, you can run 107 | 108 | ```sh 109 | kubectl delete -f https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/main/manifests/dragonfly-operator.yaml 110 | ``` 111 | 112 | ### Note regarding ipv6 only support 113 | 114 | You need to add those `args` in the dragonfly instance declaration in order to bind on ipv6. 115 | 116 | ```sh 117 | ... 118 | - "--bind=::" 119 | - "--admin_bind=::" 120 | ... 121 | ``` 122 | 123 | ## License 124 | 125 | Copyright 2023 DragonflyDB authors. 126 | 127 | Licensed under the Apache License, Version 2.0 (the "License"); 128 | you may not use this file except in compliance with the License. 129 | You may obtain a copy of the License at 130 | 131 | http://www.apache.org/licenses/LICENSE-2.0 132 | 133 | Unless required by applicable law or agreed to in writing, software 134 | distributed under the License is distributed on an "AS IS" BASIS, 135 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136 | See the License for the specific language governing permissions and 137 | limitations under the License. 138 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: dragonfly-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: dragonfly-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patchesStrategicMerge: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | - manager_auth_proxy_patch.yaml 34 | 35 | 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | # Uncomment the following replacements to add the cert-manager CA injection annotations 48 | #replacements: 49 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 50 | # kind: Certificate 51 | # group: cert-manager.io 52 | # version: v1 53 | # name: serving-cert # this name should match the one in certificate.yaml 54 | # fieldPath: .metadata.namespace # namespace of the certificate CR 55 | # targets: 56 | # - select: 57 | # kind: ValidatingWebhookConfiguration 58 | # fieldPaths: 59 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 60 | # options: 61 | # delimiter: '/' 62 | # index: 0 63 | # create: true 64 | # - select: 65 | # kind: MutatingWebhookConfiguration 66 | # fieldPaths: 67 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 68 | # options: 69 | # delimiter: '/' 70 | # index: 0 71 | # create: true 72 | # - select: 73 | # kind: CustomResourceDefinition 74 | # fieldPaths: 75 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 76 | # options: 77 | # delimiter: '/' 78 | # index: 0 79 | # create: true 80 | # - source: 81 | # kind: Certificate 82 | # group: cert-manager.io 83 | # version: v1 84 | # name: serving-cert # this name should match the one in certificate.yaml 85 | # fieldPath: .metadata.name 86 | # targets: 87 | # - select: 88 | # kind: ValidatingWebhookConfiguration 89 | # fieldPaths: 90 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 91 | # options: 92 | # delimiter: '/' 93 | # index: 1 94 | # create: true 95 | # - select: 96 | # kind: MutatingWebhookConfiguration 97 | # fieldPaths: 98 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 99 | # options: 100 | # delimiter: '/' 101 | # index: 1 102 | # create: true 103 | # - select: 104 | # kind: CustomResourceDefinition 105 | # fieldPaths: 106 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 107 | # options: 108 | # delimiter: '/' 109 | # index: 1 110 | # create: true 111 | # - source: # Add cert-manager annotation to the webhook Service 112 | # kind: Service 113 | # version: v1 114 | # name: webhook-service 115 | # fieldPath: .metadata.name # namespace of the service 116 | # targets: 117 | # - select: 118 | # kind: Certificate 119 | # group: cert-manager.io 120 | # version: v1 121 | # fieldPaths: 122 | # - .spec.dnsNames.0 123 | # - .spec.dnsNames.1 124 | # options: 125 | # delimiter: '.' 126 | # index: 0 127 | # create: true 128 | # - source: 129 | # kind: Service 130 | # version: v1 131 | # name: webhook-service 132 | # fieldPath: .metadata.namespace # namespace of the service 133 | # targets: 134 | # - select: 135 | # kind: Certificate 136 | # group: cert-manager.io 137 | # version: v1 138 | # fieldPaths: 139 | # - .spec.dnsNames.0 140 | # - .spec.dnsNames.1 141 | # options: 142 | # delimiter: '.' 143 | # index: 1 144 | # create: true 145 | -------------------------------------------------------------------------------- /charts/dragonfly-operator/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for dragonfly-operator. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | ## Custom resource configuration 8 | crds: 9 | # -- Install and upgrade CRDs 10 | install: true 11 | # -- Keep CRDs on chart uninstall 12 | keep: true 13 | 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | namespaceOverride: "" 17 | 18 | # -- Default dragonfly image to use 19 | dragonflyImage: "" 20 | 21 | # -- Additional labels to add to all resources 22 | additionalLabels: {} 23 | # app: dragonfly-operator 24 | 25 | serviceAccount: 26 | # Specifies whether a service account should be created 27 | create: true 28 | # Automatically mount a ServiceAccount's API credentials? 29 | automount: true 30 | # Annotations to add to the service account 31 | annotations: {} 32 | # The name of the service account to use. 33 | # If not set and create is true, a name is generated using the fullname template 34 | name: dragonfly-operator-controller-manager 35 | 36 | podAnnotations: 37 | kubectl.kubernetes.io/default-container: manager 38 | 39 | podLabels: {} 40 | 41 | # Additional annotations to add to deployments 42 | deploymentAnnotations: {} 43 | 44 | # Additional labels to add to deployments 45 | deploymentLabels: {} 46 | 47 | podSecurityContext: 48 | runAsNonRoot: true 49 | 50 | service: 51 | type: ClusterIP 52 | port: 8443 53 | metricsPort: 8080 54 | 55 | terminationGracePeriodSeconds: 10 56 | 57 | rbacProxy: 58 | enabled: true 59 | image: 60 | repository: quay.io/brancz/kube-rbac-proxy 61 | pullPolicy: IfNotPresent 62 | # Overrides the image tag whose default is the chart appVersion. 63 | tag: v0.13.1 64 | 65 | extraArgs: {} 66 | 67 | resources: 68 | limits: 69 | cpu: 500m 70 | memory: 128Mi 71 | requests: 72 | cpu: 10m 73 | memory: 64Mi 74 | 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - ALL 80 | 81 | manager: 82 | image: 83 | repository: docker.dragonflydb.io/dragonflydb/operator 84 | pullPolicy: IfNotPresent 85 | # Overrides the image tag whose default is the chart appVersion. 86 | tag: "" 87 | 88 | extraArgs: {} 89 | 90 | resources: {} 91 | # limits: 92 | # cpu: 500m 93 | # memory: 128Mi 94 | # requests: 95 | # cpu: 10m 96 | # memory: 64Mi 97 | 98 | livenessProbe: 99 | httpGet: 100 | path: /healthz 101 | port: 8081 102 | initialDelaySeconds: 15 103 | periodSeconds: 20 104 | 105 | readinessProbe: 106 | httpGet: 107 | path: /readyz 108 | port: 8081 109 | initialDelaySeconds: 5 110 | periodSeconds: 10 111 | 112 | securityContext: 113 | allowPrivilegeEscalation: false 114 | capabilities: 115 | drop: 116 | - ALL 117 | 118 | # -- Priority class name for the operator pod 119 | priorityClassName: "" 120 | 121 | nodeSelector: {} 122 | 123 | tolerations: [] 124 | 125 | 126 | # -- Assign custom [TopologySpreadConstraints] rules to the application controller 127 | # @default -- `[]` (defaults to global.topologySpreadConstraints) 128 | ## Ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ 129 | ## If labelSelector is left out, it will default to the labelSelector configuration of the deployment 130 | topologySpreadConstraints: [] 131 | # - maxSkew: 1 132 | # topologyKey: topology.kubernetes.io/zone 133 | # whenUnsatisfiable: DoNotSchedule 134 | affinity: {} 135 | # nodeAffinity: 136 | # requiredDuringSchedulingIgnoredDuringExecution: 137 | # nodeSelectorTerms: 138 | # - matchExpressions: 139 | # - key: kubernetes.io/arch 140 | # operator: In 141 | # values: 142 | # - amd64 143 | # - arm64 144 | # - ppc64le 145 | # - s390x 146 | # - key: kubernetes.io/os 147 | # operator: In 148 | # values: 149 | # - linux 150 | 151 | serviceMonitor: 152 | # When set true then use a ServiceMonitor to configure scraping 153 | enabled: false 154 | # Set how frequently Prometheus should scrape 155 | interval: 30s 156 | # Set path to cloudwatch-exporter telemtery-path 157 | path: /metrics 158 | # Set labels for the ServiceMonitor, use this to define your scrape label for Prometheus Operator 159 | labels: {} 160 | # Set timeout for scrape 161 | timeout: 10s 162 | # Set relabelings for the ServiceMonitor, use to apply to samples before scraping 163 | relabelings: [] 164 | # Set metricRelabelings for the ServiceMonitor, use to apply to samples for ingestion 165 | metricRelabelings: [] 166 | # Example - note the Kubernetes convention of camelCase instead of Prometheus' snake_case 167 | # metricRelabelings: 168 | # - sourceLabels: [dbinstance_identifier] 169 | # action: replace 170 | # replacement: mydbname 171 | # targetLabel: dbname 172 | 173 | grafanaDashboard: 174 | enabled: false 175 | folder: database 176 | # -- Grafana dashboard configmap annotations. 177 | annotations: 178 | name: grafana_folder 179 | # -- Grafana dashboard configmap labels 180 | labels: 181 | name: grafana_dashboard 182 | grafanaOperator: 183 | enabled: false 184 | allowCrossNamespaceImport: true 185 | # -- Selected labels for Grafana instance 186 | matchLabels: 187 | dashboards: grafana 188 | -------------------------------------------------------------------------------- /internal/controller/dragonfly_pod_lifecycle_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | 24 | "github.com/dragonflydb/dragonfly-operator/internal/resources" 25 | corev1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/event" 30 | "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/predicate" 32 | ) 33 | 34 | type DfPodLifeCycleReconciler struct { 35 | Reconciler 36 | } 37 | 38 | // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete 39 | // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch 40 | 41 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 42 | // move the current state of the cluster closer to the desired state. 43 | // 44 | // This reconcile events focuses on configuring the given pods either as a `master` 45 | // or `replica` as they go through their lifecycle. This also focus on the failing 46 | // over to replica's part to make sure one `master` is always available. 47 | func (r *DfPodLifeCycleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 48 | log := log.FromContext(ctx) 49 | 50 | log.Info("received", "pod", req.NamespacedName) 51 | var pod corev1.Pod 52 | err := r.Client.Get(ctx, req.NamespacedName, &pod) 53 | if err != nil { 54 | return ctrl.Result{}, client.IgnoreNotFound(fmt.Errorf("failed to get pod: %w", err)) 55 | } 56 | 57 | dfName, err := getDragonflyName(&pod) 58 | if err != nil { 59 | log.Error(err, "failed to get Dragonfly name from pod labels") 60 | return ctrl.Result{}, nil 61 | } 62 | 63 | dfi, err := r.getDragonflyInstance(ctx, types.NamespacedName{ 64 | Name: dfName, 65 | Namespace: pod.Namespace, 66 | }, log) 67 | if err != nil { 68 | return ctrl.Result{}, client.IgnoreNotFound(fmt.Errorf("failed to get dragonfly instance: %w", err)) 69 | } 70 | 71 | podReady, readinessErr := dfi.isPodReady(ctx, &pod) 72 | if readinessErr != nil { 73 | return ctrl.Result{}, fmt.Errorf("failed to verify pod readiness: %w", readinessErr) 74 | } 75 | 76 | master, err := dfi.getMaster(ctx) 77 | if err != nil { 78 | if isMasterError(err) { 79 | log.Info("failed to get master pod", "error", err) 80 | 81 | if errors.Is(err, ErrIncorrectMasters) || errors.Is(err, ErrNoHealthyMaster) { 82 | if err = dfi.deleteMasterRoleLabel(ctx); err != nil { 83 | return ctrl.Result{}, fmt.Errorf("failed to delete master role label: %w", err) 84 | } 85 | } 86 | 87 | if podReady { 88 | master = &pod 89 | } else { 90 | if master, err = dfi.getHealthyPod(ctx); err != nil { 91 | log.Info("no healthy pod available to set up a master") 92 | return ctrl.Result{}, nil 93 | } 94 | } 95 | 96 | if err = dfi.configureReplication(ctx, master); err != nil { 97 | return ctrl.Result{}, fmt.Errorf("failed to configure replication: %w", err) 98 | } 99 | // re-evaluate readiness after replication changes. 100 | podReady, readinessErr = dfi.isPodReady(ctx, &pod) 101 | if readinessErr != nil { 102 | return ctrl.Result{}, fmt.Errorf("failed to verify pod readiness: %w", readinessErr) 103 | } 104 | } else { 105 | return ctrl.Result{}, fmt.Errorf("failed to get master pod: %w", err) 106 | } 107 | } 108 | 109 | if !podReady { 110 | return ctrl.Result{}, nil 111 | } 112 | 113 | if roleExists(&pod) { 114 | if dfi.getStatus().Phase != PhaseReady && dfi.getStatus().Phase != PhaseReadyOld { 115 | return ctrl.Result{}, nil 116 | } 117 | 118 | // is something wrong? check if all replicas have a matching role and revamp accordingly 119 | log.Info("non-deletion event for a pod with an existing role. checking if something is wrong", "pod", pod.Name, "role", pod.Labels[resources.RoleLabelKey]) 120 | 121 | if err = dfi.checkAndConfigureReplicas(ctx, master.Status.PodIP); err != nil { 122 | return ctrl.Result{}, fmt.Errorf("failed to check and configure replicas: %w", err) 123 | } 124 | 125 | r.EventRecorder.Event(dfi.df, corev1.EventTypeNormal, "Replication", "Checked and configured replication") 126 | } else { 127 | log.Info("pod does not have a role label", "pod", pod.Name) 128 | 129 | if err = dfi.configureReplica(ctx, &pod, master.Status.PodIP); err != nil { 130 | return ctrl.Result{}, fmt.Errorf("failed to configure pod as replica: %w", err) 131 | } 132 | 133 | r.EventRecorder.Event(dfi.df, corev1.EventTypeNormal, "Replication", "Configured a new replica") 134 | } 135 | 136 | return ctrl.Result{}, nil 137 | } 138 | 139 | // SetupWithManager sets up the controller with the Manager. 140 | func (r *DfPodLifeCycleReconciler) SetupWithManager(mgr ctrl.Manager) error { 141 | return ctrl.NewControllerManagedBy(mgr). 142 | WithEventFilter( 143 | predicate.Funcs{ 144 | UpdateFunc: func(e event.UpdateEvent) bool { 145 | return e.ObjectNew.GetLabels()[resources.KubernetesAppNameLabelKey] == resources.KubernetesAppName 146 | }, 147 | CreateFunc: func(e event.CreateEvent) bool { 148 | return e.Object.GetLabels()[resources.KubernetesAppNameLabelKey] == resources.KubernetesAppName 149 | }, 150 | DeleteFunc: func(e event.DeleteEvent) bool { 151 | return e.Object.GetLabels()[resources.KubernetesAppNameLabelKey] == resources.KubernetesAppName 152 | }, 153 | }). 154 | Named("DragonflyPodLifecycle"). 155 | For(&corev1.Pod{}). 156 | Complete(r) 157 | } 158 | -------------------------------------------------------------------------------- /internal/controller/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/dragonflydb/dragonfly-operator/internal/resources" 25 | appsv1 "k8s.io/api/apps/v1" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 31 | ) 32 | 33 | const ( 34 | PhaseResourcesCreated string = "ResourcesCreated" 35 | PhaseReady string = "Ready" 36 | PhaseRollingUpdate string = "RollingUpdate" 37 | PhaseConfiguring string = "Configuring" 38 | // PhaseReadyOld TODO: remove this in a future release. 39 | PhaseReadyOld string = "ready" 40 | ) 41 | 42 | var ( 43 | ErrNoMaster = errors.New("no master found") 44 | ErrNoHealthyMaster = errors.New("no healthy master found") 45 | ErrIncorrectMasters = errors.New("incorrect number of masters") 46 | ) 47 | 48 | // isPodOnLatestVersion returns true if the given pod is on the updated revision of the given StatefulSet. 49 | func isPodOnLatestVersion(pod *corev1.Pod, updateRevision string) bool { 50 | if podRevision, ok := pod.Labels[appsv1.StatefulSetRevisionLabel]; ok && podRevision == updateRevision { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | 57 | // getUpdatedReplica returns a replica pod that is on the latest version of the given StatefulSet. 58 | func getUpdatedReplica(replicas *corev1.PodList, updateRevision string) (*corev1.Pod, error) { 59 | // Iterate over the replicas and find a replica which is on the latest version 60 | for _, replica := range replicas.Items { 61 | if isPodOnLatestVersion(&replica, updateRevision) { 62 | return &replica, nil 63 | } 64 | } 65 | 66 | return nil, fmt.Errorf("no replica pod found on latest version") 67 | } 68 | 69 | // roleExists returns true if the pod has a role label. 70 | func roleExists(pod *corev1.Pod) bool { 71 | _, ok := pod.Labels[resources.RoleLabelKey] 72 | return ok 73 | } 74 | 75 | // isRunningAndReady returns true if the pod is running and ready 76 | func isHealthy(pod *corev1.Pod) bool { 77 | return isRunningAndReady(pod) && !isTerminating(pod) 78 | } 79 | 80 | // isRunningAndReady checks if the pod is running and ready 81 | func isRunningAndReady(pod *corev1.Pod) bool { 82 | return pod.Status.Phase == corev1.PodRunning && isReady(pod) 83 | } 84 | 85 | // isReady returns true if the pod and the dragonfly container are ready. 86 | func isReady(pod *corev1.Pod) bool { 87 | for _, c := range pod.Status.Conditions { 88 | if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue && pod.Status.PodIP != "" { 89 | return isDragonflyContainerReady(pod.Status.ContainerStatuses) 90 | } 91 | } 92 | 93 | return false 94 | } 95 | 96 | // isDragonflyContainerReady returns true if the dragonfly container is ready. 97 | func isDragonflyContainerReady(containerStatuses []corev1.ContainerStatus) bool { 98 | for _, cs := range containerStatuses { 99 | if cs.Name == resources.DragonflyContainerName { 100 | return cs.Ready 101 | } 102 | } 103 | 104 | return false 105 | } 106 | 107 | // isTerminating returns true if the pod is terminating. 108 | func isTerminating(pod *corev1.Pod) bool { 109 | if !pod.DeletionTimestamp.IsZero() { 110 | return true 111 | } 112 | 113 | for _, c := range pod.Status.Conditions { 114 | if c.Type == corev1.DisruptionTarget && c.Status == corev1.ConditionTrue { 115 | return true 116 | } 117 | } 118 | 119 | return false 120 | } 121 | 122 | // getGVK returns the GroupVersionKind of the given object. 123 | func getGVK(obj client.Object, scheme *runtime.Scheme) schema.GroupVersionKind { 124 | gvk, err := apiutil.GVKForObject(obj, scheme) 125 | if err != nil { 126 | return schema.GroupVersionKind{Group: "Unknown", Version: "Unknown", Kind: "Unknown"} 127 | } 128 | return gvk 129 | } 130 | 131 | // needRollingUpdate returns true if the given pods require a rolling update. 132 | func needRollingUpdate(pods *corev1.PodList, sts *appsv1.StatefulSet) bool { 133 | if sts.Status.UpdatedReplicas != sts.Status.Replicas { 134 | for _, pod := range pods.Items { 135 | if !isPodOnLatestVersion(&pod, sts.Status.UpdateRevision) { 136 | return true 137 | } 138 | } 139 | } 140 | 141 | return false 142 | } 143 | 144 | // getDragonflyName returns the dragonfly name from the pod labels 145 | func getDragonflyName(pod *corev1.Pod) (string, error) { 146 | if name, ok := pod.Labels[resources.DragonflyNameLabelKey]; ok { 147 | return name, nil 148 | } 149 | 150 | return "", fmt.Errorf("can't find the `%s` label", resources.DragonflyNameLabelKey) 151 | } 152 | 153 | // isMaster returns true if the pod is a master 154 | func isMaster(pod *corev1.Pod) bool { 155 | if role, ok := pod.Labels[resources.RoleLabelKey]; ok && role == resources.Master { 156 | return true 157 | } 158 | 159 | return false 160 | } 161 | 162 | // isReplica returns true if the pod is a replica 163 | func isReplica(pod *corev1.Pod) bool { 164 | if role, ok := pod.Labels[resources.RoleLabelKey]; ok && role == resources.Replica { 165 | return true 166 | } 167 | 168 | return false 169 | } 170 | 171 | // isMasterError returns true if the error is related to the master. 172 | func isMasterError(err error) bool { 173 | return errors.Is(err, ErrNoMaster) || 174 | errors.Is(err, ErrNoHealthyMaster) || 175 | errors.Is(err, ErrIncorrectMasters) 176 | } 177 | 178 | // sanitizeIp removes surrounding brackets from IPv6 addresses. 179 | func sanitizeIp(masterIp string) string { 180 | return strings.Trim(masterIp, "[]") 181 | } 182 | -------------------------------------------------------------------------------- /internal/controller/dragonfly_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 25 | appsv1 "k8s.io/api/apps/v1" 26 | corev1 "k8s.io/api/core/v1" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/builder" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/predicate" 32 | ) 33 | 34 | // DragonflyReconciler reconciles a Dragonfly object 35 | type DragonflyReconciler struct { 36 | Reconciler 37 | } 38 | 39 | //+kubebuilder:rbac:groups=dragonflydb.io,resources=dragonflies,verbs=get;list;watch;create;update;patch;delete 40 | //+kubebuilder:rbac:groups=dragonflydb.io,resources=dragonflies/status,verbs=get;update;patch 41 | //+kubebuilder:rbac:groups=dragonflydb.io,resources=dragonflies/finalizers,verbs=update 42 | //+kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete 43 | //+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete 44 | //+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete 45 | //+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete 46 | //+kubebuilder:rbac:groups="",resources=events,verbs=create;patch 47 | 48 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 49 | // move the current state of the cluster closer to the desired state. 50 | // 51 | // For more details, check Reconcile and its Result here: 52 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile 53 | func (r *DragonflyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 54 | log := log.FromContext(ctx) 55 | 56 | log.Info("reconciling dragonfly instance") 57 | 58 | dfi, err := r.getDragonflyInstance(ctx, req.NamespacedName, log) 59 | if err != nil { 60 | return ctrl.Result{}, client.IgnoreNotFound(fmt.Errorf("failed to get dragonfly instance: %w", err)) 61 | } 62 | 63 | if dfi.isTerminating() { 64 | // Ignore dragonfly instance that is being foreground deleted 65 | return ctrl.Result{}, nil 66 | } 67 | 68 | if err = dfi.reconcileResources(ctx); err != nil { 69 | return ctrl.Result{}, fmt.Errorf("failed to reconcile dragonfly resources: %w", err) 70 | } 71 | 72 | dfiStatus := dfi.getStatus() 73 | 74 | if dfiStatus.Phase == PhaseReady || dfiStatus.Phase == PhaseReadyOld { 75 | dfiStatus, err = dfi.detectRollingUpdate(ctx) 76 | if err != nil { 77 | return ctrl.Result{}, fmt.Errorf("failed to detect rolling update: %w", err) 78 | } 79 | } 80 | 81 | if dfiStatus.Phase == PhaseRollingUpdate || dfiStatus.IsRollingUpdate { 82 | log.Info("rolling out new version") 83 | 84 | statefulSet, err := dfi.getStatefulSet(ctx) 85 | if err != nil { 86 | return ctrl.Result{}, fmt.Errorf("failed to get statefulset: %w", err) 87 | } 88 | 89 | if result, err := dfi.allPodsHealthy(ctx, statefulSet.Status.UpdateRevision); !result.IsZero() || err != nil { 90 | return result, err 91 | } 92 | 93 | replicas, err := dfi.getReplicas(ctx) 94 | if err != nil { 95 | return ctrl.Result{}, fmt.Errorf("failed to get replicas: %w", err) 96 | } 97 | 98 | // We want to update the replicas first then the master 99 | // We want to have at most one updated replica in full sync phase at a time 100 | // if not, requeue 101 | if result, err := dfi.verifyUpdatedReplicas(ctx, replicas, statefulSet.Status.UpdateRevision); !result.IsZero() || err != nil { 102 | return result, err 103 | } 104 | 105 | // if we are here it means that all latest replicas are in stable sync 106 | // delete older version replicas 107 | if result, err := dfi.updatedReplicas(ctx, replicas, statefulSet.Status.UpdateRevision); !result.IsZero() || err != nil { 108 | return result, err 109 | } 110 | 111 | master, err := dfi.getMaster(ctx) 112 | if err != nil { 113 | return ctrl.Result{}, fmt.Errorf("failed to get master: %w", err) 114 | } 115 | 116 | if !isPodOnLatestVersion(master, statefulSet.Status.UpdateRevision) { 117 | if err = dfi.updatedMaster(ctx, master, replicas, statefulSet.Status.UpdateRevision); err != nil { 118 | return ctrl.Result{}, fmt.Errorf("failed to update master: %w", err) 119 | } 120 | 121 | return ctrl.Result{RequeueAfter: 5 * time.Second}, nil 122 | } else { 123 | if err = dfi.detectOldMasters(ctx, statefulSet.Status.UpdateRevision); err != nil { 124 | return ctrl.Result{}, fmt.Errorf("failed to detect old masters: %w", err) 125 | } 126 | } 127 | 128 | // If we are here all are on latest version 129 | dfiStatus.Phase = PhaseReady 130 | // TODO: remove this in a future release. 131 | dfiStatus.IsRollingUpdate = false 132 | if err = dfi.patchStatus(ctx, dfiStatus); err != nil { 133 | return ctrl.Result{}, fmt.Errorf("failed to update the dragonfly status: %w", err) 134 | } 135 | 136 | dfi.eventRecorder.Event(dfi.df, corev1.EventTypeNormal, "Rollout", "Completed") 137 | } 138 | 139 | return ctrl.Result{}, nil 140 | } 141 | 142 | // SetupWithManager sets up the controller with the Manager. 143 | func (r *DragonflyReconciler) SetupWithManager(mgr ctrl.Manager) error { 144 | return ctrl.NewControllerManagedBy(mgr). 145 | // Listen only to spec changes 146 | For(&dfv1alpha1.Dragonfly{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). 147 | Owns(&appsv1.StatefulSet{}, builder.MatchEveryOwner). 148 | Owns(&corev1.Service{}, builder.MatchEveryOwner). 149 | Named("Dragonfly"). 150 | Complete(r) 151 | } 152 | -------------------------------------------------------------------------------- /e2e/dragonfly_pod_lifecycle_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 24 | "github.com/dragonflydb/dragonfly-operator/internal/controller" 25 | "github.com/dragonflydb/dragonfly-operator/internal/resources" 26 | . "github.com/onsi/ginkgo/v2" 27 | . "github.com/onsi/gomega" 28 | appsv1 "k8s.io/api/apps/v1" 29 | corev1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/types" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | ) 34 | 35 | var _ = Describe("DF Pod Lifecycle Reconciler", Ordered, FlakeAttempts(3), func() { 36 | ctx := context.Background() 37 | podRoles := map[string][]string{ 38 | resources.Master: make([]string, 0), 39 | resources.Replica: make([]string, 0), 40 | } 41 | name := "health-test" 42 | namespace := "default" 43 | replicas := 4 44 | 45 | df := dfv1alpha1.Dragonfly{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: name, 48 | Namespace: namespace, 49 | }, 50 | Spec: dfv1alpha1.DragonflySpec{ 51 | Replicas: int32(replicas), 52 | }, 53 | } 54 | 55 | Context("Fail Over is working", func() { 56 | It("Initial Master is elected", func() { 57 | err := k8sClient.Create(ctx, &df) 58 | Expect(err).To(BeNil()) 59 | }) 60 | 61 | It("Check for resources, functional pods and status", func() { 62 | 63 | // Wait until Dragonfly object is marked initialized 64 | waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseResourcesCreated, 2*time.Minute) 65 | waitForStatefulSetReady(ctx, k8sClient, name, namespace, 2*time.Minute) 66 | 67 | // Check for service and statefulset 68 | var ss appsv1.StatefulSet 69 | err := k8sClient.Get(ctx, types.NamespacedName{ 70 | Name: name, 71 | Namespace: namespace, 72 | }, &ss) 73 | Expect(err).To(BeNil()) 74 | 75 | var svc corev1.Service 76 | err = k8sClient.Get(ctx, types.NamespacedName{ 77 | Name: name, 78 | Namespace: namespace, 79 | }, &svc) 80 | Expect(err).To(BeNil()) 81 | 82 | err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 1*time.Minute) 83 | Expect(err).To(BeNil()) 84 | 85 | // Check if there are relevant pods with expected roles 86 | var pods corev1.PodList 87 | err = k8sClient.List(ctx, &pods, client.InNamespace(namespace), client.MatchingLabels{ 88 | resources.DragonflyNameLabelKey: name, 89 | resources.KubernetesPartOfLabelKey: "dragonfly", 90 | }) 91 | Expect(err).To(BeNil()) 92 | 93 | // 4 pod replicas = 1 master + 3 replicas 94 | Expect(pods.Items).To(HaveLen(replicas)) 95 | 96 | // Get the pods along with their roles 97 | for _, pod := range pods.Items { 98 | role, ok := pod.Labels[resources.RoleLabelKey] 99 | // error if there is no label 100 | Expect(ok).To(BeTrue()) 101 | 102 | podRoles[role] = append(podRoles[role], pod.Name) 103 | } 104 | 105 | // One Master & Three Replicas 106 | Expect(podRoles[resources.Master]).To(HaveLen(1)) 107 | Expect(podRoles[resources.Replica]).To(HaveLen(replicas - 1)) 108 | }) 109 | 110 | It("Delete old master", func() { 111 | 112 | // Get & Delete the old master 113 | var pod corev1.Pod 114 | err := k8sClient.Get(ctx, types.NamespacedName{ 115 | Namespace: namespace, 116 | Name: podRoles[resources.Master][0], 117 | }, &pod) 118 | Expect(err).To(BeNil()) 119 | 120 | err = k8sClient.Delete(ctx, &pod) 121 | Expect(err).To(BeNil()) 122 | }) 123 | 124 | It("New master is elected", func() { 125 | 126 | // Wait until the loop is reconciled. This is needed as status is ready previously 127 | // and the test might move forward even before the reconcile loop is triggered 128 | time.Sleep(1 * time.Minute) 129 | 130 | err := waitForStatefulSetReady(ctx, k8sClient, name, namespace, 1*time.Minute) 131 | Expect(err).To(BeNil()) 132 | 133 | err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 1*time.Minute) 134 | Expect(err).To(BeNil()) 135 | 136 | // Check if there are relevant pods with expected roles 137 | var pods corev1.PodList 138 | err = k8sClient.List(ctx, &pods, client.InNamespace(namespace), client.MatchingLabels{ 139 | resources.DragonflyNameLabelKey: name, 140 | resources.KubernetesPartOfLabelKey: "dragonfly", 141 | }) 142 | Expect(err).To(BeNil()) 143 | 144 | // 4 pod replicas = 1 master + 3 replicas 145 | Expect(pods.Items).To(HaveLen(replicas)) 146 | 147 | // Get the pods along with their roles 148 | podRoles := make(map[string][]string) 149 | for _, pod := range pods.Items { 150 | role, ok := pod.Labels[resources.RoleLabelKey] 151 | // error if there is no label 152 | Expect(ok).To(BeTrue()) 153 | // verify the role to match the label 154 | podRoles[role] = append(podRoles[role], pod.Name) 155 | } 156 | 157 | // One Master & Three Replicas 158 | Expect(podRoles[resources.Master]).To(HaveLen(1)) 159 | Expect(podRoles[resources.Replica]).To(HaveLen(replicas - 1)) 160 | }) 161 | 162 | It("New pods are added as replica", func() { 163 | var pod corev1.Pod 164 | err := k8sClient.Get(ctx, types.NamespacedName{ 165 | Namespace: namespace, 166 | Name: podRoles[resources.Replica][0], 167 | }, &pod) 168 | Expect(err).To(BeNil()) 169 | 170 | err = k8sClient.Delete(ctx, &pod) 171 | Expect(err).To(BeNil()) 172 | 173 | // Wait until the loop is reconciled. This is needed as status is ready previously 174 | // and the test might move forward even before the reconcile loop is triggered 175 | time.Sleep(10 * time.Second) 176 | 177 | // Expect a new replica 178 | // Wait for Status to be ready 179 | err = waitForDragonflyPhase(ctx, k8sClient, name, namespace, controller.PhaseReady, 1*time.Minute) 180 | Expect(err).To(BeNil()) 181 | err = waitForStatefulSetReady(ctx, k8sClient, name, namespace, 1*time.Minute) 182 | Expect(err).To(BeNil()) 183 | 184 | // Check if there are relevant pods with expected roles 185 | var pods corev1.PodList 186 | err = k8sClient.List(ctx, &pods, client.InNamespace(namespace), client.MatchingLabels{ 187 | resources.DragonflyNameLabelKey: name, 188 | resources.KubernetesPartOfLabelKey: "dragonfly", 189 | }) 190 | Expect(err).To(BeNil()) 191 | 192 | // 4 pod replicas = 1 master + 3 replicas 193 | Expect(pods.Items).To(HaveLen(replicas)) 194 | 195 | // Get the pods along with their roles 196 | podRoles := make(map[string][]string) 197 | for _, pod := range pods.Items { 198 | role, ok := pod.Labels[resources.RoleLabelKey] 199 | // error if there is no label 200 | Expect(ok).To(BeTrue()) 201 | // verify the role to match the label 202 | podRoles[role] = append(podRoles[role], pod.Name) 203 | } 204 | 205 | // One Master & Three Replicas 206 | Expect(podRoles[resources.Master]).To(HaveLen(1)) 207 | Expect(podRoles[resources.Replica]).To(HaveLen(replicas - 1)) 208 | }) 209 | 210 | It("Cleanup", func() { 211 | var df dfv1alpha1.Dragonfly 212 | err := k8sClient.Get(ctx, types.NamespacedName{ 213 | Name: name, 214 | Namespace: namespace, 215 | }, &df) 216 | Expect(err).To(BeNil()) 217 | 218 | err = k8sClient.Delete(ctx, &df) 219 | Expect(err).To(BeNil()) 220 | }) 221 | }) 222 | 223 | }) 224 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 26 | // to ensure that exec-entrypoint and run can make use of them. 27 | v1 "k8s.io/api/core/v1" 28 | "k8s.io/client-go/kubernetes" 29 | typedv1core "k8s.io/client-go/kubernetes/typed/core/v1" 30 | _ "k8s.io/client-go/plugin/pkg/client/auth" 31 | "k8s.io/client-go/tools/record" 32 | 33 | "k8s.io/apimachinery/pkg/runtime" 34 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 35 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 36 | "k8s.io/client-go/rest" 37 | "k8s.io/client-go/tools/clientcmd" 38 | ctrl "sigs.k8s.io/controller-runtime" 39 | "sigs.k8s.io/controller-runtime/pkg/cache" 40 | "sigs.k8s.io/controller-runtime/pkg/healthz" 41 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 42 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 43 | "sigs.k8s.io/controller-runtime/pkg/webhook" 44 | 45 | dragonflydbiov1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" 46 | "github.com/dragonflydb/dragonfly-operator/internal/controller" 47 | //+kubebuilder:scaffold:imports 48 | ) 49 | 50 | var ( 51 | scheme = runtime.NewScheme() 52 | setupLog = ctrl.Log.WithName("setup") 53 | ) 54 | 55 | const ( 56 | // Version is the version of the operator. 57 | Version = "source" 58 | ) 59 | 60 | func init() { 61 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 62 | 63 | utilruntime.Must(dragonflydbiov1alpha1.AddToScheme(scheme)) 64 | //+kubebuilder:scaffold:scheme 65 | } 66 | 67 | func main() { 68 | var metricsAddr string 69 | var enableLeaderElection bool 70 | var probeAddr string 71 | var versionFlag bool 72 | var watchCurrentNamespace bool 73 | var dragonflyImage string 74 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 75 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 76 | flag.StringVar(&dragonflyImage, "dragonfly-image", "", "The default dragonfly image to use.") 77 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 78 | "Enable leader election for controller manager. "+ 79 | "Enabling this will ensure there is only one active controller manager.") 80 | flag.BoolVar(&versionFlag, "version", false, "Print version and exist") 81 | flag.BoolVar(&watchCurrentNamespace, "watch-current-namespace", false, "Watch only namespace where operator is deployed") 82 | 83 | opts := zap.Options{ 84 | Development: true, 85 | } 86 | opts.BindFlags(flag.CommandLine) 87 | flag.Parse() 88 | 89 | if versionFlag { 90 | fmt.Println(Version) 91 | os.Exit(0) 92 | } 93 | 94 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 95 | 96 | mgrOpts := ctrl.Options{ 97 | Scheme: scheme, 98 | Metrics: metricsserver.Options{ 99 | BindAddress: metricsAddr, 100 | }, 101 | WebhookServer: webhook.NewServer(webhook.Options{ 102 | Port: 9443, 103 | }), 104 | HealthProbeBindAddress: probeAddr, 105 | LeaderElection: enableLeaderElection, 106 | LeaderElectionID: "31079dea.dragonflydb.io", 107 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 108 | // when the Manager ends. This requires the binary to immediately end when the 109 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 110 | // speeds up voluntary leader transitions as the new leader don't have to wait 111 | // LeaseDuration time first. 112 | // 113 | // In the default scaffold provided, the program ends immediately after 114 | // the manager stops, so would be fine to enable this option. However, 115 | // if you are doing or is intended to do any operation such as perform cleanups 116 | // after the manager stops then its usage might be unsafe. 117 | // LeaderElectionReleaseOnCancel: true, 118 | } 119 | 120 | // Watch namespace taken from the environment variable WATCH_NAMESPACE. 121 | watchNamespaces := getWatchNamespaceFromEnvVariable() 122 | // Watching the current namespace takes precedence over watching the namespace taken from the environment variable WATCH_NAMESPACE. 123 | if watchCurrentNamespace { 124 | watchNamespaces = getCurrentNamespace() 125 | } 126 | 127 | addNamespacesToOpts(watchNamespaces, &mgrOpts) 128 | 129 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts) 130 | if err != nil { 131 | setupLog.Error(err, "unable to start manager") 132 | os.Exit(1) 133 | } 134 | 135 | clientset, err := kubernetes.NewForConfig(ctrl.GetConfigOrDie()) 136 | if err != nil { 137 | setupLog.Error(err, "unable to create a clientset") 138 | os.Exit(1) 139 | } 140 | 141 | eventBroadcaster := record.NewBroadcaster() 142 | eventBroadcaster.StartStructuredLogging(4) 143 | eventBroadcaster.StartRecordingToSink(&typedv1core.EventSinkImpl{Interface: clientset.CoreV1().Events("")}) 144 | eventRecorder := eventBroadcaster.NewRecorder(scheme, v1.EventSource{Component: "dragonfly-operator"}) 145 | 146 | defer eventBroadcaster.Shutdown() 147 | 148 | if err = (&controller.DragonflyReconciler{ 149 | Reconciler: controller.Reconciler{ 150 | Client: mgr.GetClient(), 151 | Scheme: mgr.GetScheme(), 152 | EventRecorder: eventRecorder, 153 | DefaultDragonflyImage: dragonflyImage, 154 | }, 155 | }).SetupWithManager(mgr); err != nil { 156 | setupLog.Error(err, "unable to create controller", "controller", "Dragonfly") 157 | os.Exit(1) 158 | } 159 | 160 | if err = (&controller.DfPodLifeCycleReconciler{ 161 | Reconciler: controller.Reconciler{ 162 | Client: mgr.GetClient(), 163 | Scheme: mgr.GetScheme(), 164 | EventRecorder: eventRecorder, 165 | DefaultDragonflyImage: dragonflyImage, 166 | }, 167 | }).SetupWithManager(mgr); err != nil { 168 | setupLog.Error(err, "unable to create controller", "controller", "Health") 169 | os.Exit(1) 170 | } 171 | 172 | //+kubebuilder:scaffold:builder 173 | 174 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 175 | setupLog.Error(err, "unable to set up health check") 176 | os.Exit(1) 177 | } 178 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 179 | setupLog.Error(err, "unable to set up ready check") 180 | os.Exit(1) 181 | } 182 | 183 | setupLog.Info("starting manager") 184 | 185 | if watchNamespaces != "" { 186 | setupLog.Info(fmt.Sprintf("Watch namespaces: %s", watchNamespaces)) 187 | } else { 188 | setupLog.Info(fmt.Sprintf("Watch all namespaces.")) 189 | } 190 | 191 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 192 | setupLog.Error(err, "problem running manager") 193 | os.Exit(1) 194 | } 195 | } 196 | 197 | func getWatchNamespaceFromEnvVariable() string { 198 | var watchNamespaceEnvVar = "WATCH_NAMESPACE" 199 | 200 | ns, found := os.LookupEnv(watchNamespaceEnvVar) 201 | if !found { 202 | return "" 203 | } 204 | return ns 205 | } 206 | 207 | func getCurrentNamespace() string { 208 | clientCfg, _ := clientcmd.NewDefaultClientConfigLoadingRules().Load() 209 | ns := clientCfg.Contexts[clientCfg.CurrentContext].Namespace 210 | if ns == "" { 211 | ns = "default" 212 | } 213 | return ns 214 | } 215 | 216 | func addNamespacesToOpts(namespaces string, ops *ctrl.Options) error { 217 | nsList := map[string]cache.Config{} 218 | if namespaces != "" { 219 | for _, value := range strings.Split(namespaces, ",") { 220 | nsList[value] = cache.Config{} 221 | } 222 | ops.NewCache = func(config *rest.Config, opts cache.Options) (cache.Cache, error) { 223 | opts.DefaultNamespaces = nsList 224 | return cache.New(config, opts) 225 | } 226 | 227 | } 228 | return nil 229 | } 230 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 5 | ENVTEST_K8S_VERSION = 1.30.0 6 | 7 | VERSION ?= $(shell git describe --tags --always --dirty) 8 | 9 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 10 | ifeq (,$(shell go env GOBIN)) 11 | GOBIN=$(shell go env GOPATH)/bin 12 | else 13 | GOBIN=$(shell go env GOBIN) 14 | endif 15 | 16 | # Setting SHELL to bash allows bash commands to be executed by recipes. 17 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 18 | SHELL = /usr/bin/env bash -o pipefail 19 | .SHELLFLAGS = -ec 20 | 21 | .PHONY: all 22 | all: build 23 | 24 | ##@ General 25 | 26 | # The help target prints out all targets with their descriptions organized 27 | # beneath their categories. The categories are represented by '##@' and the 28 | # target descriptions by '##'. The awk commands is responsible for reading the 29 | # entire set of makefiles included in this invocation, looking for lines of the 30 | # file as xyz: ## something, and then pretty-format the target and help. Then, 31 | # if there's a line with ##@ something, that gets pretty-printed as a category. 32 | # More info on the usage of ANSI control characters for terminal formatting: 33 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 34 | # More info on the awk command: 35 | # http://linuxcommand.org/lc3_adv_awk.php 36 | 37 | .PHONY: help 38 | help: ## Display this help. 39 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 40 | 41 | ##@ Development 42 | 43 | .PHONY: manifests 44 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 45 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 46 | 47 | .PHONY: generate 48 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 49 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 50 | 51 | .PHONY: fmt 52 | fmt: ## Run go fmt against code. 53 | go fmt ./... 54 | 55 | .PHONY: vet 56 | vet: ## Run go vet against code. 57 | go vet ./... 58 | 59 | .PHONY: test 60 | test: manifests generate fmt vet envtest ## Run tests. 61 | GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo@v2.27.1 62 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -timeout=12m -vv -r -p -coverprofile cover.out 63 | 64 | ##@ Build 65 | 66 | .PHONY: build 67 | build: manifests generate fmt vet ## Build manager binary. 68 | go build -o bin/dragonfly-operator -ldflags "-X main.version=$(VERSION)" cmd/main.go 69 | 70 | .PHONY: run 71 | run: manifests generate fmt vet ## Run a controller from your host. 72 | go run ./cmd/main.go 73 | 74 | # If you wish built the manager image targeting other platforms you can use the --platform flag. 75 | # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. 76 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 77 | .PHONY: docker-build 78 | docker-build: ## Build docker image with the manager. 79 | docker build -t ${IMG} . 80 | 81 | .PHONY: docker-push 82 | docker-push: ## Push docker image with the manager. 83 | docker push ${IMG} 84 | 85 | .PHONY: docker-kind-load 86 | docker-kind-load: ## Load docker image with the manager into kind cluster. 87 | kind load docker-image ${IMG} 88 | 89 | # PLATFORMS defines the target platforms for the manager image be build to provide support to multiple 90 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 91 | # - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ 92 | # - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 93 | # - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) 94 | # To properly provided solutions that supports more than one platform you should use this option. 95 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 96 | .PHONY: docker-buildx 97 | docker-buildx: test ## Build and push docker image for the manager for cross-platform support 98 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 99 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 100 | - docker buildx create --name project-v3-builder 101 | docker buildx use project-v3-builder 102 | - docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 103 | - docker buildx rm project-v3-builder 104 | rm Dockerfile.cross 105 | 106 | ##@ Deployment 107 | 108 | ifndef ignore-not-found 109 | ignore-not-found = false 110 | endif 111 | 112 | .PHONY: install 113 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 114 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 115 | 116 | .PHONY: generate-manifests 117 | generate-manifests: manifests kustomize ## Generate manifests e.g. CRD, RBAC etc. 118 | mkdir -p manifests 119 | $(KUSTOMIZE) build config/default > manifests/dragonfly-operator.yaml 120 | $(KUSTOMIZE) build config/crd > manifests/crd.yaml 121 | 122 | .PHONY: uninstall 123 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 124 | $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 125 | 126 | .PHONY: deploy 127 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 128 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 129 | $(KUSTOMIZE) build config/default | kubectl apply -f - 130 | 131 | .PHONY: undeploy 132 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 133 | $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - 134 | 135 | ##@ Build Dependencies 136 | 137 | ## Location to install dependencies to 138 | LOCALBIN ?= $(shell pwd)/bin 139 | $(LOCALBIN): 140 | mkdir -p $(LOCALBIN) 141 | 142 | ## Tool Binaries 143 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 144 | GINKGO ?= $(LOCALBIN)/ginkgo 145 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 146 | ENVTEST ?= $(LOCALBIN)/setup-envtest 147 | 148 | ## Tool Versions 149 | KUSTOMIZE_VERSION ?= v4.5.7 150 | CONTROLLER_TOOLS_VERSION ?= v0.17.2 151 | 152 | KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" 153 | .PHONY: kustomize 154 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. 155 | $(KUSTOMIZE): $(LOCALBIN) 156 | @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ 157 | echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ 158 | rm -rf $(LOCALBIN)/kustomize; \ 159 | fi 160 | test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } 161 | 162 | .PHONY: controller-gen 163 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 164 | $(CONTROLLER_GEN): $(LOCALBIN) 165 | test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ 166 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 167 | 168 | .PHONY: envtest 169 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 170 | $(ENVTEST): $(LOCALBIN) 171 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /api/v1alpha1/dragonfly_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // DragonflySpec defines the desired state of Dragonfly 28 | type DragonflySpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // Replicas is the total number of Dragonfly instances including the master 33 | Replicas int32 `json:"replicas,omitempty"` 34 | 35 | // Image is the Dragonfly image to use 36 | Image string `json:"image,omitempty"` 37 | 38 | // (Optional) imagePullPolicy to set to Dragonfly, default is Always 39 | // +optional 40 | // +kubebuilder:validation:Optional 41 | // +kubebuilder:default:="Always" 42 | ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` 43 | 44 | // (Optional) imagePullSecrets to set to Dragonfly 45 | // +optional 46 | // +kubebuilder:validation:Optional 47 | ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` 48 | 49 | // (Optional) Dragonfly container args to pass to the container 50 | // Refer to the Dragonfly documentation for the list of supported args 51 | // +optional 52 | // +kubebuilder:validation:Optional 53 | Args []string `json:"args,omitempty"` 54 | 55 | // (Optional) Acl file Secret to pass to the container 56 | // +optional 57 | // +kubebuilder:validation:Optional 58 | AclFromSecret *corev1.SecretKeySelector `json:"aclFromSecret,omitempty"` 59 | 60 | // (Optional) Annotations to add to the Dragonfly pods. 61 | // +optional 62 | // +kubebuilder:validation:Optional 63 | Annotations map[string]string `json:"annotations,omitempty"` 64 | 65 | // (Optional) Labels to add to the Dragonfly pods. 66 | // +optional 67 | // +kubebuilder:validation:Optional 68 | Labels map[string]string `json:"labels,omitempty"` 69 | 70 | // (Optional) Env variables to add to the Dragonfly pods. 71 | // +optional 72 | // +kubebuilder:validation:Optional 73 | Env []corev1.EnvVar `json:"env,omitempty"` 74 | 75 | // (Optional) Additional containers to add to dragonflycluster. Replace container on name collision. 76 | // +optional 77 | // +kubebuilder:validation:Optional 78 | AdditionalContainers []corev1.Container `json:"additionalContainers,omitempty"` 79 | 80 | // (Optional) Additional volumes to add to dragonflycluster. Replace volume on name collision. 81 | // +optional 82 | // +kubebuilder:validation:Optional 83 | AdditionalVolumes []corev1.Volume `json:"additionalVolumes,omitempty"` 84 | 85 | // (Optional) Dragonfly container resource limits. Any container limits 86 | // can be specified. 87 | // +optional 88 | // +kubebuilder:validation:Optional 89 | Resources *corev1.ResourceRequirements `json:"resources,omitempty"` 90 | 91 | // (Optional) Dragonfly pod affinity 92 | // +optional 93 | // +kubebuilder:validation:Optional 94 | Affinity *corev1.Affinity `json:"affinity,omitempty"` 95 | 96 | // (Optional) Dragonfly pod node selector 97 | // +optional 98 | // +kubebuilder:validation:Optional 99 | NodeSelector map[string]string `json:"nodeSelector,omitempty"` 100 | 101 | // (Optional) Dragonfly memcached port 102 | // +optional 103 | // +kubebuilder:validation:Optional 104 | MemcachedPort int32 `json:"memcachedPort,omitempty"` 105 | 106 | // (Optional) Dragonfly pod tolerations 107 | // +optional 108 | // +kubebuilder:validation:Optional 109 | Tolerations []corev1.Toleration `json:"tolerations,omitempty"` 110 | 111 | // (Optional) Dragonfly pod topologySpreadConstraints 112 | // +optional 113 | // +kubebuilder:validation:Optional 114 | TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` 115 | 116 | // (Optional) Dragonfly Authentication mechanism 117 | // +optional 118 | // +kubebuilder:validation:Optional 119 | Authentication *Authentication `json:"authentication,omitempty"` 120 | 121 | // (Optional) Dragonfly container security context 122 | // +optional 123 | // +kubebuilder:validation:Optional 124 | ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"` 125 | 126 | // (Optional) Dragonfly pod security context 127 | // +optional 128 | // +kubebuilder:validation:Optional 129 | PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` 130 | 131 | // (Optional) Dragonfly pod service account name 132 | // +optional 133 | // +kubebuilder:validation:Optional 134 | ServiceAccountName string `json:"serviceAccountName,omitempty"` 135 | 136 | // (Optional) Dragonfly pod priority class name 137 | // +optional 138 | // +kubebuilder:validation:Optional 139 | PriorityClassName string `json:"priorityClassName,omitempty"` 140 | 141 | // (Optional) Dragonfly TLS secret to used for TLS 142 | // Connections to Dragonfly. Dragonfly instance must 143 | // have access to this secret and be in the same namespace 144 | // +optional 145 | // +kubebuilder:validation:Optional 146 | TLSSecretRef *corev1.SecretReference `json:"tlsSecretRef,omitempty"` 147 | 148 | // (Optional) Dragonfly SSD Tiering configuration 149 | // +optional 150 | // +kubebuilder:validation:Optional 151 | Tiering *Tiering `json:"tiering,omitempty"` 152 | 153 | // (Optional) Dragonfly Snapshot configuration 154 | // +optional 155 | // +kubebuilder:validation:Optional 156 | Snapshot *Snapshot `json:"snapshot,omitempty"` 157 | 158 | // (Optional) Skip Assigning FileSystem Group. Required for platforms such as Openshift that require IDs to not be set, as it injects a fixed randomized ID per namespace into all pods. 159 | // +optional 160 | // +kubebuilder:validation:Optional 161 | SkipFSGroup bool `json:"skipFSGroup,omitempty"` 162 | 163 | // (Optional) Dragonfly Service configuration 164 | // +optional 165 | // +kubebuilder:validation:Optional 166 | ServiceSpec *ServiceSpec `json:"serviceSpec,omitempty"` 167 | 168 | // (Optional) Dragonfly pod init containers 169 | // +optional 170 | // +kubebuilder:validation:Optional 171 | InitContainers []corev1.Container `json:"initContainers,omitempty"` 172 | 173 | // (Optional) Dragonfly direct child resources additional annotations and labels 174 | // +optional 175 | // +kubebuilder:validation:Optional 176 | OwnedObjectsMetadata *OwnedObjectsMetadata `json:"ownedObjectsMetadata,omitempty"` 177 | } 178 | 179 | type OwnedObjectsMetadata struct { 180 | Annotations map[string]string `json:"annotations,omitempty"` 181 | Labels map[string]string `json:"labels,omitempty"` 182 | } 183 | 184 | type ServiceSpec struct { 185 | // (Optional) Dragonfly Service type 186 | // +optional 187 | // +kubebuilder:validation:Optional 188 | Type corev1.ServiceType `json:"type,omitempty"` 189 | 190 | // (Optional) Dragonfly Service name 191 | // +optional 192 | // +kubebuilder:validation:Optional 193 | Name string `json:"name,omitempty"` 194 | 195 | // (Optional) Dragonfly Service nodePort 196 | // +optional 197 | // +kubebuilder:validation:Optional 198 | NodePort int32 `json:"nodePort,omitempty"` 199 | 200 | // (Optional) Dragonfly Service Annotations 201 | // +optional 202 | // +kubebuilder:validation:Optional 203 | Annotations map[string]string `json:"annotations,omitempty"` 204 | 205 | // (Optional) Dragonfly Service Labels 206 | // +optional 207 | // +kubebuilder:validation:Optional 208 | Labels map[string]string `json:"labels,omitempty"` 209 | } 210 | 211 | type Tiering struct { 212 | // (Optional) Dragonfly PVC spec for cache tiering configuration 213 | // +optional 214 | // +kubebuilder:validation:Optional 215 | PersistentVolumeClaimSpec *corev1.PersistentVolumeClaimSpec `json:"persistentVolumeClaimSpec,omitempty"` 216 | } 217 | 218 | type Snapshot struct { 219 | // (Optional) The path to the snapshot directory 220 | // This can also be an S3 URI with the prefix `s3://` when 221 | // using S3 as the snapshot backend 222 | // +optional 223 | // +kubebuilder:validation:Optional 224 | Dir string `json:"dir,omitempty"` 225 | 226 | // (Optional) Dragonfly snapshot schedule 227 | // +optional 228 | // +kubebuilder:validation:Optional 229 | Cron string `json:"cron,omitempty"` 230 | 231 | // (Optional) Enable snapshot on master only 232 | // +optional 233 | // +kubebuilder:validation:Optional 234 | EnableOnMasterOnly bool `json:"enableOnMasterOnly,omitempty"` 235 | 236 | // (Optional) Dragonfly PVC spec 237 | // +optional 238 | // +kubebuilder:validation:Optional 239 | PersistentVolumeClaimSpec *corev1.PersistentVolumeClaimSpec `json:"persistentVolumeClaimSpec,omitempty"` 240 | 241 | // (Optional) Name of an existing PVC to use for Dragonfly snapshots 242 | // +optional 243 | // +kubebuilder:validation:Optional 244 | ExistingPersistentVolumeClaimName string `json:"existingPersistentVolumeClaimName,omitempty"` 245 | } 246 | 247 | type Authentication struct { 248 | // (Optional) Dragonfly Password from Secret as a reference to a specific key 249 | // +optional 250 | PasswordFromSecret *corev1.SecretKeySelector `json:"passwordFromSecret,omitempty"` 251 | 252 | // (Optional) If specified, the Dragonfly instance will check if the 253 | // client certificate is signed by this CA. Server TLS must be enabled for this. 254 | // +optional 255 | ClientCaCertSecret *corev1.SecretKeySelector `json:"clientCaCertSecret,omitempty"` 256 | } 257 | 258 | // DragonflyStatus defines the observed state of Dragonfly 259 | type DragonflyStatus struct { 260 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 261 | // Important: Run "make" to regenerate code after modifying this file 262 | 263 | // Status of the Dragonfly Instance 264 | // It can be one of the following: 265 | // - "ready": The Dragonfly instance is ready to serve requests 266 | // - "configuring-replication": The controller is updating the master of the Dragonfly instance 267 | // - "resources-created": The Dragonfly instance resources were created but not yet configured 268 | Phase string `json:"phase,omitempty"` 269 | 270 | // TODO: remove this in a future release. 271 | // IsRollingUpdate is true if the Dragonfly instance is being updated 272 | IsRollingUpdate bool `json:"isRollingUpdate,omitempty"` 273 | } 274 | 275 | //+kubebuilder:object:root=true 276 | //+kubebuilder:subresource:status 277 | //+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The current phase of the Dragonfly cluster" 278 | //+kubebuilder:printcolumn:name="Rolling Update",type="boolean",JSONPath=".status.isRollingUpdate",description="Indicates if a rolling update is in progress" 279 | //+kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas",description="Number of replicas" 280 | 281 | // Dragonfly is the Schema for the dragonflies API 282 | type Dragonfly struct { 283 | metav1.TypeMeta `json:",inline"` 284 | metav1.ObjectMeta `json:"metadata,omitempty"` 285 | 286 | Spec DragonflySpec `json:"spec,omitempty"` 287 | Status DragonflyStatus `json:"status,omitempty"` 288 | } 289 | 290 | //+kubebuilder:object:root=true 291 | 292 | // DragonflyList contains a list of Dragonfly 293 | type DragonflyList struct { 294 | metav1.TypeMeta `json:",inline"` 295 | metav1.ListMeta `json:"metadata,omitempty"` 296 | Items []Dragonfly `json:"items"` 297 | } 298 | 299 | func init() { 300 | SchemeBuilder.Register(&Dragonfly{}, &DragonflyList{}) 301 | } 302 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2023 DragonflyDB authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1alpha1 22 | 23 | import ( 24 | "k8s.io/api/core/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *Authentication) DeepCopyInto(out *Authentication) { 30 | *out = *in 31 | if in.PasswordFromSecret != nil { 32 | in, out := &in.PasswordFromSecret, &out.PasswordFromSecret 33 | *out = new(v1.SecretKeySelector) 34 | (*in).DeepCopyInto(*out) 35 | } 36 | if in.ClientCaCertSecret != nil { 37 | in, out := &in.ClientCaCertSecret, &out.ClientCaCertSecret 38 | *out = new(v1.SecretKeySelector) 39 | (*in).DeepCopyInto(*out) 40 | } 41 | } 42 | 43 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. 44 | func (in *Authentication) DeepCopy() *Authentication { 45 | if in == nil { 46 | return nil 47 | } 48 | out := new(Authentication) 49 | in.DeepCopyInto(out) 50 | return out 51 | } 52 | 53 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 54 | func (in *Dragonfly) DeepCopyInto(out *Dragonfly) { 55 | *out = *in 56 | out.TypeMeta = in.TypeMeta 57 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 58 | in.Spec.DeepCopyInto(&out.Spec) 59 | out.Status = in.Status 60 | } 61 | 62 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dragonfly. 63 | func (in *Dragonfly) DeepCopy() *Dragonfly { 64 | if in == nil { 65 | return nil 66 | } 67 | out := new(Dragonfly) 68 | in.DeepCopyInto(out) 69 | return out 70 | } 71 | 72 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 73 | func (in *Dragonfly) DeepCopyObject() runtime.Object { 74 | if c := in.DeepCopy(); c != nil { 75 | return c 76 | } 77 | return nil 78 | } 79 | 80 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 81 | func (in *DragonflyList) DeepCopyInto(out *DragonflyList) { 82 | *out = *in 83 | out.TypeMeta = in.TypeMeta 84 | in.ListMeta.DeepCopyInto(&out.ListMeta) 85 | if in.Items != nil { 86 | in, out := &in.Items, &out.Items 87 | *out = make([]Dragonfly, len(*in)) 88 | for i := range *in { 89 | (*in)[i].DeepCopyInto(&(*out)[i]) 90 | } 91 | } 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DragonflyList. 95 | func (in *DragonflyList) DeepCopy() *DragonflyList { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(DragonflyList) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 105 | func (in *DragonflyList) DeepCopyObject() runtime.Object { 106 | if c := in.DeepCopy(); c != nil { 107 | return c 108 | } 109 | return nil 110 | } 111 | 112 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 113 | func (in *DragonflySpec) DeepCopyInto(out *DragonflySpec) { 114 | *out = *in 115 | if in.ImagePullSecrets != nil { 116 | in, out := &in.ImagePullSecrets, &out.ImagePullSecrets 117 | *out = make([]v1.LocalObjectReference, len(*in)) 118 | copy(*out, *in) 119 | } 120 | if in.Args != nil { 121 | in, out := &in.Args, &out.Args 122 | *out = make([]string, len(*in)) 123 | copy(*out, *in) 124 | } 125 | if in.AclFromSecret != nil { 126 | in, out := &in.AclFromSecret, &out.AclFromSecret 127 | *out = new(v1.SecretKeySelector) 128 | (*in).DeepCopyInto(*out) 129 | } 130 | if in.Annotations != nil { 131 | in, out := &in.Annotations, &out.Annotations 132 | *out = make(map[string]string, len(*in)) 133 | for key, val := range *in { 134 | (*out)[key] = val 135 | } 136 | } 137 | if in.Labels != nil { 138 | in, out := &in.Labels, &out.Labels 139 | *out = make(map[string]string, len(*in)) 140 | for key, val := range *in { 141 | (*out)[key] = val 142 | } 143 | } 144 | if in.Env != nil { 145 | in, out := &in.Env, &out.Env 146 | *out = make([]v1.EnvVar, len(*in)) 147 | for i := range *in { 148 | (*in)[i].DeepCopyInto(&(*out)[i]) 149 | } 150 | } 151 | if in.AdditionalContainers != nil { 152 | in, out := &in.AdditionalContainers, &out.AdditionalContainers 153 | *out = make([]v1.Container, len(*in)) 154 | for i := range *in { 155 | (*in)[i].DeepCopyInto(&(*out)[i]) 156 | } 157 | } 158 | if in.AdditionalVolumes != nil { 159 | in, out := &in.AdditionalVolumes, &out.AdditionalVolumes 160 | *out = make([]v1.Volume, len(*in)) 161 | for i := range *in { 162 | (*in)[i].DeepCopyInto(&(*out)[i]) 163 | } 164 | } 165 | if in.Resources != nil { 166 | in, out := &in.Resources, &out.Resources 167 | *out = new(v1.ResourceRequirements) 168 | (*in).DeepCopyInto(*out) 169 | } 170 | if in.Affinity != nil { 171 | in, out := &in.Affinity, &out.Affinity 172 | *out = new(v1.Affinity) 173 | (*in).DeepCopyInto(*out) 174 | } 175 | if in.NodeSelector != nil { 176 | in, out := &in.NodeSelector, &out.NodeSelector 177 | *out = make(map[string]string, len(*in)) 178 | for key, val := range *in { 179 | (*out)[key] = val 180 | } 181 | } 182 | if in.Tolerations != nil { 183 | in, out := &in.Tolerations, &out.Tolerations 184 | *out = make([]v1.Toleration, len(*in)) 185 | for i := range *in { 186 | (*in)[i].DeepCopyInto(&(*out)[i]) 187 | } 188 | } 189 | if in.TopologySpreadConstraints != nil { 190 | in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints 191 | *out = make([]v1.TopologySpreadConstraint, len(*in)) 192 | for i := range *in { 193 | (*in)[i].DeepCopyInto(&(*out)[i]) 194 | } 195 | } 196 | if in.Authentication != nil { 197 | in, out := &in.Authentication, &out.Authentication 198 | *out = new(Authentication) 199 | (*in).DeepCopyInto(*out) 200 | } 201 | if in.ContainerSecurityContext != nil { 202 | in, out := &in.ContainerSecurityContext, &out.ContainerSecurityContext 203 | *out = new(v1.SecurityContext) 204 | (*in).DeepCopyInto(*out) 205 | } 206 | if in.PodSecurityContext != nil { 207 | in, out := &in.PodSecurityContext, &out.PodSecurityContext 208 | *out = new(v1.PodSecurityContext) 209 | (*in).DeepCopyInto(*out) 210 | } 211 | if in.TLSSecretRef != nil { 212 | in, out := &in.TLSSecretRef, &out.TLSSecretRef 213 | *out = new(v1.SecretReference) 214 | **out = **in 215 | } 216 | if in.Tiering != nil { 217 | in, out := &in.Tiering, &out.Tiering 218 | *out = new(Tiering) 219 | (*in).DeepCopyInto(*out) 220 | } 221 | if in.Snapshot != nil { 222 | in, out := &in.Snapshot, &out.Snapshot 223 | *out = new(Snapshot) 224 | (*in).DeepCopyInto(*out) 225 | } 226 | if in.ServiceSpec != nil { 227 | in, out := &in.ServiceSpec, &out.ServiceSpec 228 | *out = new(ServiceSpec) 229 | (*in).DeepCopyInto(*out) 230 | } 231 | if in.InitContainers != nil { 232 | in, out := &in.InitContainers, &out.InitContainers 233 | *out = make([]v1.Container, len(*in)) 234 | for i := range *in { 235 | (*in)[i].DeepCopyInto(&(*out)[i]) 236 | } 237 | } 238 | if in.OwnedObjectsMetadata != nil { 239 | in, out := &in.OwnedObjectsMetadata, &out.OwnedObjectsMetadata 240 | *out = new(OwnedObjectsMetadata) 241 | (*in).DeepCopyInto(*out) 242 | } 243 | } 244 | 245 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DragonflySpec. 246 | func (in *DragonflySpec) DeepCopy() *DragonflySpec { 247 | if in == nil { 248 | return nil 249 | } 250 | out := new(DragonflySpec) 251 | in.DeepCopyInto(out) 252 | return out 253 | } 254 | 255 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 256 | func (in *DragonflyStatus) DeepCopyInto(out *DragonflyStatus) { 257 | *out = *in 258 | } 259 | 260 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DragonflyStatus. 261 | func (in *DragonflyStatus) DeepCopy() *DragonflyStatus { 262 | if in == nil { 263 | return nil 264 | } 265 | out := new(DragonflyStatus) 266 | in.DeepCopyInto(out) 267 | return out 268 | } 269 | 270 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 271 | func (in *OwnedObjectsMetadata) DeepCopyInto(out *OwnedObjectsMetadata) { 272 | *out = *in 273 | if in.Annotations != nil { 274 | in, out := &in.Annotations, &out.Annotations 275 | *out = make(map[string]string, len(*in)) 276 | for key, val := range *in { 277 | (*out)[key] = val 278 | } 279 | } 280 | if in.Labels != nil { 281 | in, out := &in.Labels, &out.Labels 282 | *out = make(map[string]string, len(*in)) 283 | for key, val := range *in { 284 | (*out)[key] = val 285 | } 286 | } 287 | } 288 | 289 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnedObjectsMetadata. 290 | func (in *OwnedObjectsMetadata) DeepCopy() *OwnedObjectsMetadata { 291 | if in == nil { 292 | return nil 293 | } 294 | out := new(OwnedObjectsMetadata) 295 | in.DeepCopyInto(out) 296 | return out 297 | } 298 | 299 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 300 | func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { 301 | *out = *in 302 | if in.Annotations != nil { 303 | in, out := &in.Annotations, &out.Annotations 304 | *out = make(map[string]string, len(*in)) 305 | for key, val := range *in { 306 | (*out)[key] = val 307 | } 308 | } 309 | if in.Labels != nil { 310 | in, out := &in.Labels, &out.Labels 311 | *out = make(map[string]string, len(*in)) 312 | for key, val := range *in { 313 | (*out)[key] = val 314 | } 315 | } 316 | } 317 | 318 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. 319 | func (in *ServiceSpec) DeepCopy() *ServiceSpec { 320 | if in == nil { 321 | return nil 322 | } 323 | out := new(ServiceSpec) 324 | in.DeepCopyInto(out) 325 | return out 326 | } 327 | 328 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 329 | func (in *Snapshot) DeepCopyInto(out *Snapshot) { 330 | *out = *in 331 | if in.PersistentVolumeClaimSpec != nil { 332 | in, out := &in.PersistentVolumeClaimSpec, &out.PersistentVolumeClaimSpec 333 | *out = new(v1.PersistentVolumeClaimSpec) 334 | (*in).DeepCopyInto(*out) 335 | } 336 | } 337 | 338 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snapshot. 339 | func (in *Snapshot) DeepCopy() *Snapshot { 340 | if in == nil { 341 | return nil 342 | } 343 | out := new(Snapshot) 344 | in.DeepCopyInto(out) 345 | return out 346 | } 347 | 348 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 349 | func (in *Tiering) DeepCopyInto(out *Tiering) { 350 | *out = *in 351 | if in.PersistentVolumeClaimSpec != nil { 352 | in, out := &in.PersistentVolumeClaimSpec, &out.PersistentVolumeClaimSpec 353 | *out = new(v1.PersistentVolumeClaimSpec) 354 | (*in).DeepCopyInto(*out) 355 | } 356 | } 357 | 358 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tiering. 359 | func (in *Tiering) DeepCopy() *Tiering { 360 | if in == nil { 361 | return nil 362 | } 363 | out := new(Tiering) 364 | in.DeepCopyInto(out) 365 | return out 366 | } 367 | -------------------------------------------------------------------------------- /.github/images/logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /e2e/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 DragonflyDB authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "bufio" 21 | "context" 22 | "fmt" 23 | "io" 24 | "net" 25 | "net/http" 26 | "os" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | 33 | "github.com/dragonflydb/dragonfly-operator/internal/resources" 34 | "github.com/pkg/errors" 35 | "github.com/redis/go-redis/v9" 36 | "github.com/redis/go-redis/v9/maintnotifications" 37 | appsv1 "k8s.io/api/apps/v1" 38 | corev1 "k8s.io/api/core/v1" 39 | "k8s.io/apimachinery/pkg/types" 40 | "k8s.io/client-go/kubernetes" 41 | "k8s.io/client-go/rest" 42 | "k8s.io/client-go/tools/portforward" 43 | "k8s.io/client-go/transport/spdy" 44 | "sigs.k8s.io/controller-runtime/pkg/client" 45 | ) 46 | 47 | func parseTieredEntriesFromInfo(info string) (int64, error) { 48 | sc := bufio.NewScanner(strings.NewReader(info)) 49 | for sc.Scan() { 50 | line := strings.TrimSpace(sc.Text()) 51 | if line == "" || strings.HasPrefix(line, "#") { 52 | continue 53 | } 54 | // Handle "tiered_entries:" (with optional spaces) 55 | if strings.HasPrefix(line, "tiered_entries") { 56 | parts := strings.SplitN(line, ":", 2) 57 | if len(parts) == 2 { 58 | val := strings.TrimSpace(parts[1]) 59 | return strconv.ParseInt(val, 10, 64) 60 | } 61 | } 62 | } 63 | if err := sc.Err(); err != nil { 64 | return 0, err 65 | } 66 | return 0, fmt.Errorf("tiered_entries not found") 67 | } 68 | 69 | func waitForStatefulSetReady(ctx context.Context, c client.Client, name, namespace string, maxDuration time.Duration) error { 70 | ctx, cancel := context.WithTimeout(ctx, maxDuration) 71 | defer cancel() 72 | for { 73 | select { 74 | case <-ctx.Done(): 75 | return fmt.Errorf("timed out waiting for statefulset to be ready") 76 | default: 77 | // Check if the statefulset is ready 78 | ready, err := isStatefulSetReady(ctx, c, name, namespace) 79 | if err != nil { 80 | return err 81 | } 82 | if ready { 83 | return nil 84 | } 85 | } 86 | } 87 | } 88 | 89 | func isStatefulSetReady(ctx context.Context, c client.Client, name, namespace string) (bool, error) { 90 | var statefulSet appsv1.StatefulSet 91 | if err := c.Get(ctx, types.NamespacedName{ 92 | Name: name, 93 | Namespace: namespace, 94 | }, &statefulSet); err != nil { 95 | return false, nil 96 | } 97 | 98 | if statefulSet.Status.ReadyReplicas == *statefulSet.Spec.Replicas && statefulSet.Status.UpdatedReplicas == statefulSet.Status.Replicas { 99 | return true, nil 100 | } 101 | 102 | return false, nil 103 | } 104 | 105 | func checkAndK8sPortForwardRedis(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, stopChan chan struct{}, name, namespace, password string, port int) (*redis.Client, error) { 106 | pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ 107 | LabelSelector: fmt.Sprintf("app=%s", name), 108 | }) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if len(pods.Items) == 0 { 114 | return nil, fmt.Errorf("no pods found") 115 | } 116 | 117 | var master *corev1.Pod 118 | for _, pod := range pods.Items { 119 | if pod.Labels[resources.RoleLabelKey] == resources.Master { 120 | master = &pod 121 | break 122 | } 123 | } 124 | 125 | if master == nil { 126 | return nil, fmt.Errorf("no master pod found") 127 | } 128 | 129 | updatedMaster, err := clientset.CoreV1().Pods(master.Namespace).Get(ctx, master.Name, metav1.GetOptions{}) 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to get pod %s/%s: %w", master.Namespace, master.Name, err) 132 | } 133 | master = updatedMaster 134 | 135 | if master.Status.Phase != corev1.PodRunning { 136 | return nil, fmt.Errorf("pod %s/%s is not running (phase: %s)", master.Namespace, master.Name, master.Status.Phase) 137 | } 138 | 139 | // Verify pod has Ready condition 140 | hasReadyCondition := false 141 | for _, condition := range master.Status.Conditions { 142 | if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { 143 | hasReadyCondition = true 144 | break 145 | } 146 | } 147 | if !hasReadyCondition { 148 | return nil, fmt.Errorf("pod %s/%s is not ready (Ready condition not true)", master.Namespace, master.Name) 149 | } 150 | 151 | // Verify the Dragonfly container is ready 152 | containerReady := false 153 | for _, status := range master.Status.ContainerStatuses { 154 | if status.Name == resources.DragonflyContainerName { 155 | if status.Ready { 156 | containerReady = true 157 | break 158 | } 159 | } 160 | } 161 | if !containerReady { 162 | return nil, fmt.Errorf("container %s in pod %s/%s is not ready", resources.DragonflyContainerName, master.Namespace, master.Name) 163 | } 164 | 165 | fw, err := portForward(ctx, clientset, config, master, stopChan, port) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | redisOptions := &redis.Options{ 171 | Addr: fmt.Sprintf("localhost:%d", port), 172 | MaintNotificationsConfig: &maintnotifications.Config{ 173 | Mode: maintnotifications.ModeDisabled, 174 | }, 175 | } 176 | 177 | if password != "" { 178 | redisOptions.Password = password 179 | } 180 | 181 | redisClient := redis.NewClient(redisOptions) 182 | 183 | errChan := make(chan error, 1) 184 | go func() { errChan <- fw.ForwardPorts() }() 185 | 186 | select { 187 | case err = <-errChan: 188 | return nil, errors.Wrap(err, "unable to forward ports") 189 | case <-fw.Ready: 190 | } 191 | 192 | pingCtx, cancel := context.WithTimeout(ctx, 4*time.Second) 193 | defer cancel() 194 | 195 | err = redisClient.Ping(pingCtx).Err() 196 | if err != nil { 197 | return nil, fmt.Errorf("unable to ping instance: %w", err) 198 | } 199 | 200 | return redisClient, nil 201 | } 202 | 203 | type portForwardResult struct { 204 | LocalPort int 205 | Cleanup func() 206 | } 207 | 208 | // setupPortForwardWithCleanup sets up port forwarding with proper cleanup handling 209 | // it finds an available local port, forwards to the pod's remote port, and returns 210 | // the local port and a cleanup function. The cleanup function must be called to 211 | // ensure the port is released. 212 | func setupPortForwardWithCleanup(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *corev1.Pod, remotePort int, timeout time.Duration) (*portForwardResult, error) { 213 | // Fetch pod to get latest status 214 | updatedPod, err := clientset.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) 215 | if err != nil { 216 | return nil, fmt.Errorf("failed to get pod %s/%s: %w", pod.Namespace, pod.Name, err) 217 | } 218 | pod = updatedPod 219 | 220 | // Verify pod is running 221 | if pod.Status.Phase != corev1.PodRunning { 222 | return nil, fmt.Errorf("pod %s/%s is not running (phase: %s)", pod.Namespace, pod.Name, pod.Status.Phase) 223 | } 224 | 225 | // Verify pod has Ready condition 226 | hasReadyCondition := false 227 | for _, condition := range pod.Status.Conditions { 228 | if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { 229 | hasReadyCondition = true 230 | break 231 | } 232 | } 233 | if !hasReadyCondition { 234 | return nil, fmt.Errorf("pod %s/%s is not ready (Ready condition not true)", pod.Namespace, pod.Name) 235 | } 236 | 237 | // Verify the Dragonfly container is ready 238 | containerReady := false 239 | for _, status := range pod.Status.ContainerStatuses { 240 | if status.Name == resources.DragonflyContainerName { 241 | if status.Ready { 242 | containerReady = true 243 | break 244 | } 245 | } 246 | } 247 | if !containerReady { 248 | return nil, fmt.Errorf("container %s in pod %s/%s is not ready", resources.DragonflyContainerName, pod.Namespace, pod.Name) 249 | } 250 | 251 | localStopChan := make(chan struct{}, 1) 252 | portForwardDone := make(chan struct{}, 1) 253 | portForwardClosed := new(bool) 254 | 255 | // Port forward to pod 256 | url := clientset.CoreV1().RESTClient().Post(). 257 | Resource("pods"). 258 | Namespace(pod.Namespace). 259 | Name(pod.Name). 260 | SubResource("portforward"). 261 | URL() 262 | 263 | transport, upgrader, err := spdy.RoundTripperFor(config) 264 | if err != nil { 265 | return nil, err 266 | } 267 | 268 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url) 269 | 270 | // Find an available local port 271 | var localPort int 272 | maxPortAttempts := 10 273 | for i := 0; i < maxPortAttempts; i++ { 274 | listener, err := net.Listen("tcp", ":0") 275 | if err != nil { 276 | if i == maxPortAttempts-1 { 277 | return nil, fmt.Errorf("unable to find available port after %d attempts: %w", maxPortAttempts, err) 278 | } 279 | time.Sleep(100 * time.Millisecond) 280 | continue 281 | } 282 | localPort = listener.Addr().(*net.TCPAddr).Port 283 | listener.Close() 284 | time.Sleep(100 * time.Millisecond) 285 | break 286 | } 287 | 288 | ports := []string{fmt.Sprintf("%d:%d", localPort, remotePort)} 289 | readyChan := make(chan struct{}, 1) 290 | 291 | fw, err := portforward.New(dialer, ports, localStopChan, readyChan, io.Discard, os.Stderr) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | errChan := make(chan error, 1) 297 | go func() { 298 | defer close(portForwardDone) 299 | errChan <- fw.ForwardPorts() 300 | }() 301 | 302 | // Wait for port forward to be ready or error 303 | portForwardCtx, cancel := context.WithTimeout(ctx, timeout) 304 | defer cancel() 305 | 306 | select { 307 | case err = <-errChan: 308 | // Port forward failed, close and wait for cleanup 309 | if !*portForwardClosed && localStopChan != nil { 310 | close(localStopChan) 311 | *portForwardClosed = true 312 | } 313 | select { 314 | case <-portForwardDone: 315 | case <-time.After(2 * time.Second): 316 | } 317 | time.Sleep(500 * time.Millisecond) 318 | return nil, errors.Wrap(err, "unable to forward ports") 319 | case <-fw.Ready: 320 | // Port forward ready, wait a bit to establish connection 321 | select { 322 | case <-time.After(500 * time.Millisecond): 323 | case <-portForwardCtx.Done(): 324 | if !*portForwardClosed && localStopChan != nil { 325 | close(localStopChan) 326 | *portForwardClosed = true 327 | } 328 | select { 329 | case <-portForwardDone: 330 | case <-time.After(2 * time.Second): 331 | } 332 | time.Sleep(500 * time.Millisecond) 333 | return nil, portForwardCtx.Err() 334 | } 335 | case <-portForwardCtx.Done(): 336 | // Timeout waiting for port forward to be ready 337 | if !*portForwardClosed && localStopChan != nil { 338 | close(localStopChan) 339 | *portForwardClosed = true 340 | } 341 | select { 342 | case <-portForwardDone: 343 | case <-time.After(2 * time.Second): 344 | } 345 | time.Sleep(500 * time.Millisecond) 346 | return nil, fmt.Errorf("timeout waiting for port forward to be ready: %w", portForwardCtx.Err()) 347 | } 348 | 349 | cleanup := func() { 350 | if !*portForwardClosed && localStopChan != nil { 351 | close(localStopChan) 352 | *portForwardClosed = true 353 | } 354 | select { 355 | case <-portForwardDone: 356 | case <-time.After(3 * time.Second): 357 | } 358 | time.Sleep(1 * time.Second) 359 | } 360 | 361 | return &portForwardResult{ 362 | LocalPort: localPort, 363 | Cleanup: cleanup, 364 | }, nil 365 | } 366 | 367 | func portForward(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *corev1.Pod, stopChan chan struct{}, port int) (*portforward.PortForwarder, error) { 368 | url := clientset.CoreV1().RESTClient().Post(). 369 | Resource("pods"). 370 | Namespace(pod.Namespace). 371 | Name(pod.Name). 372 | SubResource("portforward"). 373 | URL() 374 | 375 | transport, upgrader, err := spdy.RoundTripperFor(config) 376 | if err != nil { 377 | return nil, err 378 | } 379 | 380 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url) 381 | ports := []string{fmt.Sprintf("%d:%d", port, resources.DragonflyPort)} 382 | readyChan := make(chan struct{}, 1) 383 | 384 | fw, err := portforward.New(dialer, ports, stopChan, readyChan, io.Discard, os.Stderr) 385 | if err != nil { 386 | return nil, err 387 | } 388 | return fw, err 389 | } 390 | 391 | // checkPersistenceInfo checks the persistence info from a pod's admin port 392 | // Uses port forwarding to access the pod's admin port 393 | // ensures container is Ready before attempting connection 394 | func checkPersistenceInfo(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *corev1.Pod, stopChan chan struct{}) (loading string, loadState string, err error) { 395 | // Retry with different ports if we get port conflicts 396 | maxRetries := 10 397 | for attempt := 0; attempt < maxRetries; attempt++ { 398 | loading, loadState, err = tryCheckPersistenceInfo(ctx, clientset, config, pod, stopChan) 399 | if err == nil { 400 | return loading, loadState, nil 401 | } 402 | 403 | // Check if error is due to container not being ready 404 | errStr := err.Error() 405 | isNotReadyError := strings.Contains(errStr, "is not ready") || 406 | strings.Contains(errStr, "not running") || 407 | strings.Contains(errStr, "Ready condition not true") 408 | 409 | // Retry on port conflicts or container not ready 410 | isRetryableError := strings.Contains(errStr, "address already in use") || 411 | strings.Contains(errStr, "bind") || 412 | strings.Contains(errStr, "unable to forward ports") || 413 | isNotReadyError 414 | 415 | if !isRetryableError { 416 | return "", "", err 417 | } 418 | 419 | if attempt < maxRetries-1 { 420 | // Exponential backoff, wait longer if container is not ready 421 | backoff := time.Duration(attempt+1) * 200 * time.Millisecond 422 | if isNotReadyError { 423 | backoff = time.Duration(attempt+1) * 1 * time.Second 424 | } 425 | cleanupWait := 2 * time.Second 426 | time.Sleep(backoff + cleanupWait) 427 | } 428 | } 429 | return "", "", err 430 | } 431 | 432 | func tryCheckPersistenceInfo(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *corev1.Pod, stopChan chan struct{}) (loading string, loadState string, err error) { 433 | // Setup port forwarding with proper cleanup 434 | pfResult, err := setupPortForwardWithCleanup(ctx, clientset, config, pod, resources.DragonflyAdminPort, 30*time.Second) 435 | if err != nil { 436 | return "", "", err 437 | } 438 | defer pfResult.Cleanup() 439 | 440 | adminClient := redis.NewClient(&redis.Options{ 441 | Addr: fmt.Sprintf("localhost:%d", pfResult.LocalPort), 442 | DialTimeout: 15 * time.Second, 443 | ReadTimeout: 10 * time.Second, 444 | WriteTimeout: 10 * time.Second, 445 | ContextTimeoutEnabled: true, 446 | }) 447 | defer adminClient.Close() 448 | 449 | infoCtx, cancel := context.WithTimeout(ctx, 15*time.Second) 450 | defer cancel() 451 | 452 | info, err := adminClient.Info(infoCtx, "persistence").Result() 453 | if err != nil { 454 | return "", "", fmt.Errorf("unable to get persistence info: %w", err) 455 | } 456 | 457 | // Parse info output 458 | sc := bufio.NewScanner(strings.NewReader(info)) 459 | for sc.Scan() { 460 | line := strings.TrimSpace(sc.Text()) 461 | if line == "" || strings.HasPrefix(line, "#") { 462 | continue 463 | } 464 | parts := strings.SplitN(line, ":", 2) 465 | if len(parts) == 2 { 466 | key := strings.TrimSpace(parts[0]) 467 | value := strings.TrimSpace(parts[1]) 468 | if key == "loading" { 469 | loading = value 470 | } 471 | if key == "load_state" { 472 | loadState = value 473 | } 474 | } 475 | } 476 | 477 | return loading, loadState, sc.Err() 478 | } 479 | --------------------------------------------------------------------------------