diff --git a/README.md b/README.md index e4477f13b..a5624c22e 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,28 @@ By default, no pod annotations will be applied to Redis nor Sentinel pods. In order to apply custom pod Annotations, you can provide the `podAnnotations` option inside redis/sentinel spec. An example can be found in the [custom annotations example file](example/redisfailover/custom-annotations.yaml). +### Control of label propagation. +By default the operator will propagate all labels on the CRD down to the resources that it creates. This can be problematic if the +labels on the CRD are not fully under your own control (for example: being deployed by a gitops operator) +as a change to a labels value can fail on immutable resources such as PodDisruptionBudgets. To control what labels the operator propagates +to resource is creates you can modify the labelWhitelist option in the spec. + +By default specifying no whitelist or an empty whitelist will cause all labels to still be copied as not to break backwards compatibility. + +Items in the array should be regular expressions, see [here](example/redisfailover/control-label-propagation.yaml) as an example of how they can be used and +[here](https://github.com/google/re2/wiki/Syntax) for a syntax reference. + +The whitelist can also be used as a form of blacklist by specifying a regular expression that will not match any label. + +NOTE: The operator will always add the labels it requires for operation to resources. These are the following: +``` +app.kubernetes.io/component +app.kubernetes.io/managed-by +app.kubernetes.io/name +app.kubernetes.io/part-of +redisfailovers.databases.spotahome.com/name +``` + ## Connection to the created Redis Failovers In order to connect to the redis-failover and use it, a [Sentinel-ready](https://redis.io/topics/sentinel-clients) library has to be used. This will connect through the Sentinel service to the Redis node working as a master. diff --git a/api/redisfailover/v1/types.go b/api/redisfailover/v1/types.go index ed12ee4d4..019f5cc01 100644 --- a/api/redisfailover/v1/types.go +++ b/api/redisfailover/v1/types.go @@ -20,6 +20,7 @@ type RedisFailoverSpec struct { Redis RedisSettings `json:"redis,omitempty"` Sentinel SentinelSettings `json:"sentinel,omitempty"` Auth AuthSettings `json:"auth,omitempty"` + LabelWhitelist []string `json:"labelWhitelist,omitempty"` } // RedisSettings defines the specification of the redis cluster diff --git a/api/redisfailover/v1/zz_generated.deepcopy.go b/api/redisfailover/v1/zz_generated.deepcopy.go index 9e464f819..71a148637 100644 --- a/api/redisfailover/v1/zz_generated.deepcopy.go +++ b/api/redisfailover/v1/zz_generated.deepcopy.go @@ -123,6 +123,11 @@ func (in *RedisFailoverSpec) DeepCopyInto(out *RedisFailoverSpec) { in.Redis.DeepCopyInto(&out.Redis) in.Sentinel.DeepCopyInto(&out.Sentinel) out.Auth = in.Auth + if in.LabelWhitelist != nil { + in, out := &in.LabelWhitelist, &out.LabelWhitelist + *out = make([]string, len(*in)) + copy(*out, *in) + } return } @@ -170,6 +175,11 @@ func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { (*in).DeepCopyInto(*out) } } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]core_v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]core_v1.Toleration, len(*in)) @@ -177,6 +187,20 @@ func (in *RedisSettings) DeepCopyInto(out *RedisSettings) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } @@ -256,6 +280,11 @@ func (in *SentinelSettings) DeepCopyInto(out *SentinelSettings) { (*in).DeepCopyInto(*out) } } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]core_v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.Tolerations != nil { in, out := &in.Tolerations, &out.Tolerations *out = make([]core_v1.Toleration, len(*in)) @@ -263,6 +292,20 @@ func (in *SentinelSettings) DeepCopyInto(out *SentinelSettings) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/example/redisfailover/control-label-propagation.yaml b/example/redisfailover/control-label-propagation.yaml new file mode 100644 index 000000000..a9f67b12e --- /dev/null +++ b/example/redisfailover/control-label-propagation.yaml @@ -0,0 +1,29 @@ +apiVersion: databases.spotahome.com/v1 +kind: RedisFailover +metadata: + name: redisfailover2 + labels: + # These two labels will be propagated. + app.example.com/label1: value + app.example.com/label2: value + # This one wont be, as there is a non-empty whitelist and the regexp doesnt match it. + anotherlabel: value +spec: + sentinel: + replicas: 3 + resources: + requests: + cpu: 100m + limits: + memory: 100Mi + redis: + replicas: 3 + resources: + requests: + cpu: 100m + memory: 100Mi + limits: + cpu: 400m + memory: 500Mi + labelWhitelist: + - ^app.example.com.* diff --git a/mocks/service/redis/Client.go b/mocks/service/redis/Client.go index 582761e7e..9b26c5f9c 100644 --- a/mocks/service/redis/Client.go +++ b/mocks/service/redis/Client.go @@ -114,27 +114,6 @@ func (_m *Client) IsMaster(ip string, password string) (bool, error) { return r0, r1 } -// SlaveIsReady provides a mock function with given fields: ip, password -func (_m *Client) SlaveIsReady(ip string, password string) (bool, error) { - ret := _m.Called(ip, password) - - var r0 bool - if rf, ok := ret.Get(0).(func(string, string) bool); ok { - r0 = rf(ip, password) - } else { - r0 = ret.Get(0).(bool) - } - - var r1 error - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(ip, password) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // MakeMaster provides a mock function with given fields: ip, password func (_m *Client) MakeMaster(ip string, password string) error { ret := _m.Called(ip, password) @@ -218,3 +197,24 @@ func (_m *Client) SetCustomSentinelConfig(ip string, configs []string) error { return r0 } + +// SlaveIsReady provides a mock function with given fields: ip, password +func (_m *Client) SlaveIsReady(ip string, password string) (bool, error) { + ret := _m.Called(ip, password) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string) bool); ok { + r0 = rf(ip, password) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(ip, password) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/operator/redisfailover/handler.go b/operator/redisfailover/handler.go index 10d0855df..91d64ee27 100644 --- a/operator/redisfailover/handler.go +++ b/operator/redisfailover/handler.go @@ -3,6 +3,7 @@ package redisfailover import ( "context" "fmt" + "regexp" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -102,7 +103,27 @@ func (r *RedisFailoverHandler) getLabels(rf *redisfailoverv1.RedisFailover) map[ dynLabels := map[string]string{ rfLabelNameKey: rf.Name, } - return util.MergeLabels(defaultLabels, dynLabels, rf.Labels) + + // Filter the labels based on the whitelist + filteredCustomLabels := make(map[string]string) + if rf.Spec.LabelWhitelist != nil && len(rf.Spec.LabelWhitelist) != 0 { + for _, regex := range rf.Spec.LabelWhitelist { + compiledRegexp, err := regexp.Compile(regex) + if err != nil { + r.logger.Errorf("Unable to compile label whitelist regex '%s', ignoring it.", regex) + continue + } + for labelKey, labelValue := range rf.Labels { + if match := compiledRegexp.MatchString(labelKey); match { + filteredCustomLabels[labelKey] = labelValue + } + } + } + } else { + // If no whitelist is specified then don't filter the labels. + filteredCustomLabels = rf.Labels + } + return util.MergeLabels(defaultLabels, dynLabels, filteredCustomLabels) } func (w *RedisFailoverHandler) createOwnerReferences(rf *redisfailoverv1.RedisFailover) []metav1.OwnerReference {