Skip to content

Commit aa94403

Browse files
committed
Add TLS support for memcached
Ability to configure memcached with TLS endpoints. Jira: OSPRH-3568
1 parent 99face7 commit aa94403

File tree

14 files changed

+437
-52
lines changed

14 files changed

+437
-52
lines changed

apis/bases/memcached.openstack.org_memcacheds.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ spec:
5353
description: Size of the memcached cluster
5454
format: int32
5555
type: integer
56+
tls:
57+
description: TLS settings for memcached service
58+
properties:
59+
caBundleSecretName:
60+
description: CaBundleSecretName - holding the CA certs in a pre-created
61+
bundle file
62+
type: string
63+
secretName:
64+
description: SecretName - holding the cert, key for the service
65+
type: string
66+
type: object
5667
required:
5768
- containerImage
5869
type: object
@@ -102,6 +113,11 @@ spec:
102113
- type
103114
type: object
104115
type: array
116+
hash:
117+
additionalProperties:
118+
type: string
119+
description: Map of hashes to track input changes
120+
type: object
105121
readyCount:
106122
description: ReadyCount of Memcached instances
107123
format: int32

apis/memcached/v1beta1/memcached_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1beta1
1818

1919
import (
2020
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
21+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
2122
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2324
)
@@ -39,10 +40,18 @@ type MemcachedSpec struct {
3940
// +kubebuilder:default=1
4041
// Size of the memcached cluster
4142
Replicas *int32 `json:"replicas"`
43+
44+
// +kubebuilder:validation:Optional
45+
// +operator-sdk:csv:customresourcedefinitions:type=spec
46+
// TLS settings for memcached service
47+
TLS tls.SimpleService `json:"tls,omitempty"`
4248
}
4349

4450
// MemcachedStatus defines the observed state of Memcached
4551
type MemcachedStatus struct {
52+
// Map of hashes to track input changes
53+
Hash map[string]string `json:"hash,omitempty"`
54+
4655
// ReadyCount of Memcached instances
4756
ReadyCount int32 `json:"readyCount,omitempty"`
4857

apis/memcached/v1beta1/zz_generated.deepcopy.go

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

config/crd/bases/memcached.openstack.org_memcacheds.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ spec:
5353
description: Size of the memcached cluster
5454
format: int32
5555
type: integer
56+
tls:
57+
description: TLS settings for memcached service
58+
properties:
59+
caBundleSecretName:
60+
description: CaBundleSecretName - holding the CA certs in a pre-created
61+
bundle file
62+
type: string
63+
secretName:
64+
description: SecretName - holding the cert, key for the service
65+
type: string
66+
type: object
5667
required:
5768
- containerImage
5869
type: object
@@ -102,6 +113,11 @@ spec:
102113
- type
103114
type: object
104115
type: array
116+
hash:
117+
additionalProperties:
118+
type: string
119+
description: Map of hashes to track input changes
120+
type: object
105121
readyCount:
106122
description: ReadyCount of Memcached instances
107123
format: int32
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: memcached.openstack.org/v1beta1
2+
kind: Memcached
3+
metadata:
4+
name: memcached
5+
spec:
6+
replicas: 1
7+
tls:
8+
secretName: memcached-tls
9+
caBundleSecretName: memcached-tls

controllers/memcached/memcached_controller.go

Lines changed: 158 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import (
2121
"fmt"
2222
"time"
2323

24+
"github.com/openstack-k8s-operators/lib-common/modules/common"
2425
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2526
configmap "github.com/openstack-k8s-operators/lib-common/modules/common/configmap"
2627
common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
2728
commonservice "github.com/openstack-k8s-operators/lib-common/modules/common/service"
2829
commonstatefulset "github.com/openstack-k8s-operators/lib-common/modules/common/statefulset"
30+
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
2931

3032
env "github.com/openstack-k8s-operators/lib-common/modules/common/env"
3133
helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
@@ -34,22 +36,43 @@ import (
3436
corev1 "k8s.io/api/core/v1"
3537
rbacv1 "k8s.io/api/rbac/v1"
3638
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
39+
"k8s.io/apimachinery/pkg/fields"
40+
"k8s.io/apimachinery/pkg/types"
3741
"k8s.io/client-go/kubernetes"
42+
"k8s.io/client-go/rest"
43+
"sigs.k8s.io/controller-runtime/pkg/builder"
44+
"sigs.k8s.io/controller-runtime/pkg/handler"
45+
"sigs.k8s.io/controller-runtime/pkg/predicate"
3846

3947
"github.com/go-logr/logr"
4048
"k8s.io/apimachinery/pkg/runtime"
4149
ctrl "sigs.k8s.io/controller-runtime"
4250
"sigs.k8s.io/controller-runtime/pkg/client"
4351
"sigs.k8s.io/controller-runtime/pkg/log"
52+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4453

4554
memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1"
4655
memcached "github.com/openstack-k8s-operators/infra-operator/pkg/memcached"
4756
)
4857

58+
// fields to index to reconcile on CR change
59+
const (
60+
serviceSecretNameField = ".spec.tls.genericService.SecretName"
61+
caSecretNameField = ".spec.tls.ca.caBundleSecretName"
62+
)
63+
64+
var (
65+
allWatchFields = []string{
66+
serviceSecretNameField,
67+
caSecretNameField,
68+
}
69+
)
70+
4971
// Reconciler reconciles a Memcached object
5072
type Reconciler struct {
5173
client.Client
5274
Kclient kubernetes.Interface
75+
config *rest.Config
5376
Scheme *runtime.Scheme
5477
}
5578

@@ -135,6 +158,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
135158
instance.Status.Conditions = condition.Conditions{}
136159
// initialize conditions used later as Status=Unknown
137160
cl := condition.CreateList(
161+
// TLS cert secrets
162+
condition.UnknownCondition(condition.TLSInputReadyCondition, condition.InitReason, condition.InputReadyInitMessage),
138163
// endpoint for adoption redirect
139164
condition.UnknownCondition(condition.ExposeServiceReadyCondition, condition.InitReason, condition.ExposeServiceReadyInitMessage),
140165
// configmap generation
@@ -185,9 +210,41 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
185210
return rbacResult, nil
186211
}
187212

213+
// Hash of all resources that may cause a service restart
214+
inputHashEnv := make(map[string]env.Setter)
215+
216+
// Check and hash inputs
217+
var certHash, caHash string
218+
specTLS := &instance.Spec.TLS
219+
if specTLS.Enabled() {
220+
certHash, _, err = specTLS.GenericService.ValidateCertSecret(ctx, helper, instance.Namespace)
221+
}
222+
if err == nil && specTLS.Enabled() && specTLS.Ca.CaBundleSecretName != "" {
223+
caName := types.NamespacedName{
224+
Name: specTLS.Ca.CaBundleSecretName,
225+
Namespace: instance.Namespace,
226+
}
227+
caHash, _, err = tls.ValidateCACertSecret(ctx, helper.GetClient(), caName)
228+
}
229+
if err != nil {
230+
instance.Status.Conditions.Set(condition.FalseCondition(
231+
condition.TLSInputReadyCondition,
232+
condition.ErrorReason,
233+
condition.SeverityWarning,
234+
condition.TLSInputErrorMessage,
235+
err.Error()))
236+
return ctrl.Result{}, fmt.Errorf("error calculating input hash: %w", err)
237+
}
238+
if certHash != "" {
239+
inputHashEnv["Cert"] = env.SetValue(certHash)
240+
}
241+
if caHash != "" {
242+
inputHashEnv["CA"] = env.SetValue(caHash)
243+
}
244+
instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage)
245+
188246
// Memcached config maps
189-
configMapVars := make(map[string]env.Setter)
190-
err = r.generateConfigMaps(ctx, helper, instance, &configMapVars)
247+
err = r.generateConfigMaps(ctx, helper, instance, &inputHashEnv)
191248
if err != nil {
192249
instance.Status.Conditions.Set(condition.FalseCondition(
193250
condition.ServiceConfigReadyCondition,
@@ -199,6 +256,27 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
199256
}
200257
instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
201258

259+
//
260+
// create hash over all the different input resources to identify if any those changed
261+
// and a restart/recreate is required.
262+
//
263+
hashOfHashes, err := util.HashOfInputHashes(inputHashEnv)
264+
if err != nil {
265+
return ctrl.Result{}, err
266+
}
267+
if hashMap, changed := util.SetHash(instance.Status.Hash, common.InputHashName, hashOfHashes); changed {
268+
// Hash changed and instance status should be updated (which will be done by main defer func),
269+
// so update all the input hashes and return to reconcile again
270+
instance.Status.Hash = hashMap
271+
for k, s := range inputHashEnv {
272+
var envVar corev1.EnvVar
273+
s(&envVar)
274+
instance.Status.Hash[k] = envVar.Value
275+
}
276+
Log.Info(fmt.Sprintf("Input hash changed %s", hashOfHashes), "instance", instance)
277+
return ctrl.Result{}, nil
278+
}
279+
202280
// Service to expose Memcached pods
203281
commonsvc, err := commonservice.NewService(memcached.HeadlessService(instance), time.Duration(5)*time.Second, nil)
204282
if err != nil {
@@ -247,17 +325,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
247325
return ctrl.Result{}, nil
248326
}
249327

250-
// generateConfigMaps returns the config map resource for a galera instance
328+
// generateConfigMaps returns the config map resource for a memcached instance
251329
func (r *Reconciler) generateConfigMaps(
252330
ctx context.Context,
253331
h *helper.Helper,
254332
instance *memcachedv1.Memcached,
255333
envVars *map[string]env.Setter,
256334
) error {
257-
Log := r.GetLogger(ctx)
335+
Log := h.GetLogger()
258336

259-
templateParameters := make(map[string]interface{})
260337
customData := make(map[string]string)
338+
var memcachedTLSConfig string
339+
if instance.Spec.TLS.Enabled() {
340+
memcachedTLSConfig = "-Z " +
341+
"-o ssl_chain_cert=/etc/pki/tls/certs/memcached.crt " +
342+
"-o ssl_key=/etc/pki/tls/private/memcached.key " +
343+
"-o ssl_ca_cert=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
344+
} else {
345+
memcachedTLSConfig = ""
346+
}
347+
templateParameters := map[string]interface{}{
348+
"memcachedTLSConfig": memcachedTLSConfig,
349+
}
261350

262351
cms := []util.Template{
263352
// ConfigMap
@@ -283,16 +372,80 @@ func (r *Reconciler) generateConfigMaps(
283372

284373
// SetupWithManager sets up the controller with the Manager.
285374
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
375+
r.config = mgr.GetConfig()
376+
377+
// Various CR fields need to be indexed to filter watch events
378+
// for the secret changes we want to be notified of
379+
// index caBundleSecretName
380+
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &memcachedv1.Memcached{}, caSecretNameField, func(rawObj client.Object) []string {
381+
// Extract the secret name from the spec, if one is provided
382+
cr := rawObj.(*memcachedv1.Memcached)
383+
tls := &cr.Spec.TLS
384+
if tls.Ca.CaBundleSecretName != "" {
385+
return []string{tls.Ca.CaBundleSecretName}
386+
}
387+
return nil
388+
}); err != nil {
389+
return err
390+
}
391+
// index secretName
392+
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &memcachedv1.Memcached{}, serviceSecretNameField, func(rawObj client.Object) []string {
393+
// Extract the secret name from the spec, if one is provided
394+
cr := rawObj.(*memcachedv1.Memcached)
395+
tls := &cr.Spec.TLS
396+
if tls.Enabled() {
397+
return []string{*tls.GenericService.SecretName}
398+
}
399+
return nil
400+
}); err != nil {
401+
return err
402+
}
403+
286404
return ctrl.NewControllerManagedBy(mgr).
287405
For(&memcachedv1.Memcached{}).
288406
Owns(&appsv1.StatefulSet{}).
289407
Owns(&corev1.Service{}).
290408
Owns(&corev1.ServiceAccount{}).
291409
Owns(&rbacv1.Role{}).
292410
Owns(&rbacv1.RoleBinding{}).
411+
Watches(
412+
&corev1.Secret{},
413+
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
414+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
415+
).
293416
Complete(r)
294417
}
295418

419+
// findObjectsForSrc - returns a reconcile request if the object is referenced by a Memcached CR
420+
func (r *Reconciler) findObjectsForSrc(_ context.Context, src client.Object) []reconcile.Request {
421+
requests := []reconcile.Request{}
422+
423+
for _, field := range allWatchFields {
424+
crList := &memcachedv1.MemcachedList{}
425+
listOps := &client.ListOptions{
426+
FieldSelector: fields.OneTermEqualSelector(field, src.GetName()),
427+
Namespace: src.GetNamespace(),
428+
}
429+
err := r.List(context.TODO(), crList, listOps)
430+
if err != nil {
431+
return []reconcile.Request{}
432+
}
433+
434+
for _, item := range crList.Items {
435+
requests = append(requests,
436+
reconcile.Request{
437+
NamespacedName: types.NamespacedName{
438+
Name: item.GetName(),
439+
Namespace: item.GetNamespace(),
440+
},
441+
},
442+
)
443+
}
444+
}
445+
446+
return requests
447+
}
448+
296449
// GetServerLists returns list of memcached server without/with inet prefix
297450
func (r *Reconciler) GetServerLists(
298451
instance *memcachedv1.Memcached,

0 commit comments

Comments
 (0)