Skip to content

Commit a2b93b0

Browse files
feat: redis tls hot reload, merge master
Signed-off-by: Husni Alhamdani <[email protected]>
1 parent a560160 commit a2b93b0

File tree

112 files changed

+1629
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+1629
-174
lines changed

README.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,13 @@ Redis Operator requires a Kubernetes cluster of version `>=1.18.0`. If you have
5959

6060
## Image Compatibility
6161

62-
The following table shows the compatibility between the Operator Version, Redis Image, Sentinel Image, and Exporter Image:
63-
64-
| Operator Version | Redis Image | Sentinel Image | Exporter Image |
65-
| ---------------- | ----------- | -------------- | -------------- |
66-
| v0.19.x | > v7.0.12, >=v6.2.14 | > v7.0.12, >= v6.2.14 | v1.44.0 |
67-
| v0.18.x | v7.0.12 | v7.0.12 | v1.44.0 |
68-
| v0.17.0 | v7.0.12 | v7.0.12 | v1.44.0 |
69-
| v0.16.0 | v7.0.12 | v7.0.12 | v1.44.0 |
70-
| v0.15.1 | v7.0.12 | v7.0.12 | v1.44.0 |
71-
| v0.15.0 | v7.0.11 | v7.0.11 | v1.44.0 |
72-
| v0.14.0 | v7.0.7 | v7.0.7 | v1.44.0 |
73-
| v0.13.0 | v6.2.5 | nil | v1.44.0 |
62+
The operator supports Redis versions `>=6.x`. However, **it is strongly recommended to use the latest stable version** to ensure you have the latest security fixes and bug patches from upstream.
63+
64+
**Container Images:**
65+
- **Redis**: `quay.io/opstree/redis`
66+
- **Sentinel**: `quay.io/opstree/redis-sentinel`
67+
- **Exporter**: `quay.io/opstree/redis-exporter`
68+
7469

7570
## Quickstart
7671

api/common/v1beta2/common_types.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package v1beta2
22

33
import (
4+
"fmt"
5+
46
appsv1 "k8s.io/api/apps/v1"
57
corev1 "k8s.io/api/core/v1"
68
)
@@ -272,5 +274,27 @@ type InitContainer struct {
272274

273275
// +k8s:deepcopy-gen=true
274276
type ACLConfig struct {
277+
// Secret-based ACL configuration.
278+
// Adapts a Secret into a volume containing ACL rules.
279+
// The contents of the target Secret's Data field will be presented in a volume
280+
// as files using the keys in the Data field as the file names.
281+
// Secret volumes support ownership management and SELinux relabeling.
275282
Secret *corev1.SecretVolumeSource `json:"secret,omitempty"`
283+
// PersistentVolumeClaim-based ACL configuration
284+
// Specify the PVC name to mount ACL file from persistent storage
285+
// The operator will automatically mount /etc/redis/user.acl from the PVC
286+
PersistentVolumeClaim *string `json:"persistentVolumeClaim,omitempty"`
287+
}
288+
289+
// Validate checks that only one ACL source is specified
290+
func (a *ACLConfig) Validate() error {
291+
if a == nil {
292+
return nil
293+
}
294+
295+
if a.Secret != nil && a.PersistentVolumeClaim != nil {
296+
return fmt.Errorf("only one of 'secret' or 'persistentVolumeClaim' can be specified in ACL configuration")
297+
}
298+
299+
return nil
276300
}

api/common/v1beta2/common_types_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/assert"
7+
corev1 "k8s.io/api/core/v1"
78
"k8s.io/utils/ptr"
89
)
910

@@ -84,3 +85,150 @@ func TestKubernetesConfig_ShouldCreateAdditionalService(t *testing.T) {
8485
})
8586
}
8687
}
88+
89+
func TestACLConfig_PersistentVolumeClaim(t *testing.T) {
90+
tests := []struct {
91+
name string
92+
aclConfig *ACLConfig
93+
expectedPVCName *string
94+
expectedSecretConfigured bool
95+
}{
96+
{
97+
name: "nil ACL config",
98+
aclConfig: nil,
99+
expectedPVCName: nil,
100+
expectedSecretConfigured: false,
101+
},
102+
{
103+
name: "empty ACL config",
104+
aclConfig: &ACLConfig{},
105+
expectedPVCName: nil,
106+
expectedSecretConfigured: false,
107+
},
108+
{
109+
name: "ACL with secret only",
110+
aclConfig: &ACLConfig{
111+
Secret: &corev1.SecretVolumeSource{
112+
SecretName: "redis-acl-secret",
113+
},
114+
},
115+
expectedPVCName: nil,
116+
expectedSecretConfigured: true,
117+
},
118+
{
119+
name: "ACL with PVC only",
120+
aclConfig: &ACLConfig{
121+
PersistentVolumeClaim: ptr.To("redis-acl-pvc"),
122+
},
123+
expectedPVCName: ptr.To("redis-acl-pvc"),
124+
expectedSecretConfigured: false,
125+
},
126+
}
127+
128+
for _, tt := range tests {
129+
t.Run(tt.name, func(t *testing.T) {
130+
if tt.aclConfig == nil {
131+
assert.Nil(t, tt.aclConfig)
132+
return
133+
}
134+
135+
// Verify PVC configuration
136+
if tt.expectedPVCName != nil {
137+
assert.NotNil(t, tt.aclConfig.PersistentVolumeClaim)
138+
assert.Equal(t, *tt.expectedPVCName, *tt.aclConfig.PersistentVolumeClaim)
139+
} else {
140+
assert.Nil(t, tt.aclConfig.PersistentVolumeClaim)
141+
}
142+
143+
// Verify Secret configuration
144+
if tt.expectedSecretConfigured {
145+
assert.NotNil(t, tt.aclConfig.Secret)
146+
} else {
147+
assert.Nil(t, tt.aclConfig.Secret)
148+
}
149+
})
150+
}
151+
}
152+
153+
func TestACLConfig_BothSourcesConfigured(t *testing.T) {
154+
// Test scenario where both Secret and PVC are configured
155+
// Validation should fail
156+
aclConfig := &ACLConfig{
157+
Secret: &corev1.SecretVolumeSource{
158+
SecretName: "redis-acl-secret",
159+
},
160+
PersistentVolumeClaim: ptr.To("redis-acl-pvc"),
161+
}
162+
163+
// Both should be settable (struct level)
164+
assert.NotNil(t, aclConfig.Secret)
165+
assert.NotNil(t, aclConfig.PersistentVolumeClaim)
166+
assert.Equal(t, "redis-acl-secret", aclConfig.Secret.SecretName)
167+
assert.Equal(t, "redis-acl-pvc", *aclConfig.PersistentVolumeClaim)
168+
169+
// But validation should fail
170+
err := aclConfig.Validate()
171+
assert.Error(t, err)
172+
assert.Contains(t, err.Error(), "only one of 'secret' or 'persistentVolumeClaim' can be specified")
173+
}
174+
175+
func TestACLConfig_Validate(t *testing.T) {
176+
tests := []struct {
177+
name string
178+
aclConfig *ACLConfig
179+
wantErr bool
180+
errMsg string
181+
}{
182+
{
183+
name: "nil config is valid",
184+
aclConfig: nil,
185+
wantErr: false,
186+
},
187+
{
188+
name: "empty config is valid",
189+
aclConfig: &ACLConfig{},
190+
wantErr: false,
191+
},
192+
{
193+
name: "only secret is valid",
194+
aclConfig: &ACLConfig{
195+
Secret: &corev1.SecretVolumeSource{
196+
SecretName: "redis-acl-secret",
197+
},
198+
},
199+
wantErr: false,
200+
},
201+
{
202+
name: "only PVC is valid",
203+
aclConfig: &ACLConfig{
204+
PersistentVolumeClaim: ptr.To("redis-acl-pvc"),
205+
},
206+
wantErr: false,
207+
},
208+
{
209+
name: "both secret and PVC is invalid",
210+
aclConfig: &ACLConfig{
211+
Secret: &corev1.SecretVolumeSource{
212+
SecretName: "redis-acl-secret",
213+
},
214+
PersistentVolumeClaim: ptr.To("redis-acl-pvc"),
215+
},
216+
wantErr: true,
217+
errMsg: "only one of 'secret' or 'persistentVolumeClaim' can be specified",
218+
},
219+
}
220+
221+
for _, tt := range tests {
222+
t.Run(tt.name, func(t *testing.T) {
223+
err := tt.aclConfig.Validate()
224+
if tt.wantErr {
225+
assert.Error(t, err)
226+
if tt.errMsg != "" {
227+
assert.Contains(t, err.Error(), tt.errMsg)
228+
}
229+
} else {
230+
assert.NoError(t, err)
231+
}
232+
})
233+
}
234+
}

api/common/v1beta2/zz_generated.deepcopy.go

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

api/redis/v1beta2/redis_webhook.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,80 @@ limitations under the License.
1717
package v1beta2
1818

1919
import (
20+
apierrors "k8s.io/apimachinery/pkg/api/errors"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"k8s.io/apimachinery/pkg/util/validation/field"
2024
ctrl "sigs.k8s.io/controller-runtime"
25+
logf "sigs.k8s.io/controller-runtime/pkg/log"
26+
"sigs.k8s.io/controller-runtime/pkg/webhook"
27+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2128
)
2229

30+
const (
31+
webhookPath = "/validate-redis-redis-opstreelabs-in-v1beta2-redis"
32+
)
33+
34+
// log is for logging in this package.
35+
var redislog = logf.Log.WithName("redis-v1beta2-validation")
36+
37+
// +kubebuilder:webhook:path=/validate-redis-redis-opstreelabs-in-v1beta2-redis,mutating=false,failurePolicy=fail,sideEffects=None,groups=redis.redis.opstreelabs.in,resources=redis,verbs=create;update,versions=v1beta2,name=validate-redis.redis.opstreelabs.in,admissionReviewVersions=v1
38+
2339
func (r *Redis) SetupWebhookWithManager(mgr ctrl.Manager) error {
2440
return ctrl.NewWebhookManagedBy(mgr).
2541
For(r).
2642
Complete()
2743
}
2844

29-
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
45+
var _ webhook.Validator = &Redis{}
46+
47+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
48+
func (r *Redis) ValidateCreate() (admission.Warnings, error) {
49+
redislog.Info("validate create", "name", r.Name)
50+
51+
return r.validate(nil)
52+
}
53+
54+
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
55+
func (r *Redis) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
56+
redislog.Info("validate update", "name", r.Name)
57+
58+
return r.validate(old.(*Redis))
59+
}
60+
61+
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
62+
func (r *Redis) ValidateDelete() (admission.Warnings, error) {
63+
redislog.Info("validate delete", "name", r.Name)
64+
65+
return nil, nil
66+
}
67+
68+
// validate validates the Redis CR
69+
func (r *Redis) validate(_ *Redis) (admission.Warnings, error) {
70+
var errors field.ErrorList
71+
72+
// Validate ACL configuration
73+
if r.Spec.ACL != nil {
74+
if err := r.Spec.ACL.Validate(); err != nil {
75+
errors = append(errors, field.Invalid(
76+
field.NewPath("spec").Child("acl"),
77+
r.Spec.ACL,
78+
err.Error(),
79+
))
80+
}
81+
}
82+
83+
if len(errors) == 0 {
84+
return nil, nil
85+
}
86+
87+
return nil, apierrors.NewInvalid(
88+
schema.GroupKind{Group: "redis.redis.opstreelabs.in", Kind: "Redis"},
89+
r.Name,
90+
errors,
91+
)
92+
}
93+
94+
func (r *Redis) WebhookPath() string {
95+
return webhookPath
96+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package v1beta2_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
8+
common "github.com/OT-CONTAINER-KIT/redis-operator/api/common/v1beta2"
9+
v1beta2 "github.com/OT-CONTAINER-KIT/redis-operator/api/redis/v1beta2"
10+
"github.com/OT-CONTAINER-KIT/redis-operator/internal/testutil/webhook"
11+
"github.com/stretchr/testify/require"
12+
admissionv1beta1 "k8s.io/api/admission/v1beta1"
13+
corev1 "k8s.io/api/core/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/types"
16+
"k8s.io/utils/ptr"
17+
)
18+
19+
func TestRedisWebhook(t *testing.T) {
20+
cases := []webhook.ValidationWebhookTestCase{
21+
{
22+
Name: "failed-create-v1beta2-redis-acl-both-sources",
23+
Operation: admissionv1beta1.Create,
24+
Object: func(t *testing.T, uid string) []byte {
25+
t.Helper()
26+
redis := mkRedis(uid)
27+
redis.Spec.ACL = mkACLWithBothSources()
28+
return marshal(t, redis)
29+
},
30+
Check: webhook.ValidationWebhookFailed("only one of 'secret' or 'persistentVolumeClaim' can be specified"),
31+
},
32+
}
33+
34+
gvk := metav1.GroupVersionKind{
35+
Group: "redis.redis.opstreelabs.in",
36+
Version: "v1beta2",
37+
Kind: "Redis",
38+
}
39+
40+
redis := &v1beta2.Redis{}
41+
webhook.RunValidationWebhookTests(t, gvk, redis, cases...)
42+
}
43+
44+
func mkRedis(uid string) *v1beta2.Redis {
45+
return &v1beta2.Redis{
46+
ObjectMeta: metav1.ObjectMeta{
47+
Name: fmt.Sprintf("test-%s", uid),
48+
UID: types.UID(fmt.Sprintf("test-%s", uid)),
49+
},
50+
Spec: v1beta2.RedisSpec{},
51+
}
52+
}
53+
54+
func marshal(t *testing.T, obj interface{}) []byte {
55+
t.Helper()
56+
bytes, err := json.Marshal(obj)
57+
require.NoError(t, err)
58+
return bytes
59+
}
60+
61+
func mkACLWithBothSources() *common.ACLConfig {
62+
return &common.ACLConfig{
63+
Secret: &corev1.SecretVolumeSource{
64+
SecretName: "test-secret",
65+
},
66+
PersistentVolumeClaim: ptr.To("test-pvc"),
67+
}
68+
}

api/redis/v1beta2/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)