Skip to content

Deployment Reconciliation

Reference documentation for the Deployment reconciliation logic that creates and updates an apps/v1 Deployment for each Memcached CR.

Source: internal/controller/deployment.go, internal/controller/memcached_controller.go

Overview

When a Memcached CR is created or updated, the reconciler ensures a matching Deployment exists in the same namespace with the same name. The Deployment is constructed from the CR spec using pure builder functions, then applied via controllerutil.CreateOrUpdate for idempotent create/update semantics. A controller owner reference on the Deployment enables automatic garbage collection when the Memcached CR is deleted.


Labels

labelsForMemcached(name string) returns the standard Kubernetes recommended labels applied to both the Deployment and its pod template:

go
func labelsForMemcached(name string) map[string]string {
    return map[string]string{
        "app.kubernetes.io/name":       "memcached",
        "app.kubernetes.io/instance":   name,
        "app.kubernetes.io/managed-by": "memcached-operator",
    }
}
Label KeyValuePurpose
app.kubernetes.io/namememcachedIdentifies the application
app.kubernetes.io/instance<cr-name>Distinguishes instances of the same application
app.kubernetes.io/managed-bymemcached-operatorIdentifies the managing controller

These labels are used as the Deployment's spec.selector.matchLabels and on the pod template metadata.labels, ensuring the Deployment manages the correct pods.


Memcached CLI Arguments

buildMemcachedArgs(config *MemcachedConfig, sasl *SASLSpec) translates the CRD's spec.memcached fields into memcached command-line flags. When sasl is non-nil and enabled, the -Y flag is appended pointing to the SASL password file mount path.

Flag Mapping

CRD FieldFlagDefaultExample Output
maxMemoryMB-m64["-m", "128"]
maxConnections-c1024["-c", "2048"]
threads-t4["-t", "8"]
maxItemSize-I"1m"["-I", "2m"]
verbosity-v00: none, 1: -v, 2: -vv
SASL enabled-Y/etc/memcached/sasl/password-file (see SASL Authentication)
extraArgs[]Appended after all flags

Default Arguments

When spec.memcached is nil or all fields are zero-valued, the produced argument list is:

text
["-m", "64", "-c", "1024", "-t", "4", "-I", "1m"]

Verbosity Handling

spec.memcached.verbosityFlags Appended
0 (default)(none)
1"-v"
2"-vv"

Argument Ordering

Arguments are appended in a fixed order:

  1. Standard flags (-m, -c, -t, -I)
  2. Verbosity (-v or -vv)
  3. SASL flag (-Y /etc/memcached/sasl/password-file) — only when SASL is enabled
  4. Extra arguments (spec.memcached.extraArgs)

Extra Arguments

spec.memcached.extraArgs are appended after all other flags, preserving order. This allows passing arbitrary memcached flags not covered by typed fields:

yaml
spec:
  memcached:
    maxMemoryMB: 128
    extraArgs: ["-o", "modern", "-B", "auto"]

Produces: ["-m", "128", "-c", "1024", "-t", "4", "-I", "1m", "-o", "modern", "-B", "auto"]


Deployment Construction

constructDeployment(mc *Memcached, dep *Deployment, secretHash, restartTrigger string) sets the desired state of the Deployment in-place. It is called within the controllerutil.CreateOrUpdate mutate function so that both creation and updates use identical logic. The secretHash and restartTrigger parameters are written as pod template annotations (memcached.c5c3.io/secret-hash and memcached.c5c3.io/restart-trigger respectively), causing Kubernetes to roll pods when these values change.

Spec Defaults

FieldSourceDefault
replicasspec.Replicas1
imagespec.Image"memcached:1.6"
argsspec.MemcachedSee default args
resourcesspec.Resources(empty)

Container Specification

The Deployment contains a single container:

PropertyValue
namememcached
imageFrom spec.Image (default memcached:1.6)
argsBuilt by buildMemcachedArgs
resourcesFrom spec.Resources (empty if nil)
portsmemcached: 11211/TCP
volumeMountsSASL credentials mount (when enabled, see SASL Authentication)

Container Port

go
corev1.ContainerPort{
    Name:          "memcached",
    ContainerPort: 11211,
    Protocol:      corev1.ProtocolTCP,
}

The named port memcached is referenced by health probes using intstr.FromString("memcached").

Health Probes

Both probes use TCP socket checks on the named port memcached (11211):

ProbeTypePortInitialDelayPeriod
livenessProbeTCP socketmemcached10s10s
readinessProbeTCP socketmemcached5s5s

The readiness probe gates traffic to the pod. The liveness probe restarts the container if memcached becomes unresponsive.

Deployment Strategy

go
appsv1.DeploymentStrategy{
    Type: appsv1.RollingUpdateDeploymentStrategyType,
    RollingUpdate: &appsv1.RollingUpdateDeployment{
        MaxSurge:       intstr.FromInt32(1),
        MaxUnavailable: intstr.FromInt32(0),
    },
}
ParameterValueEffect
maxSurge1One extra pod is created before terminating old pods
maxUnavailable0No existing pods are terminated until new pods ready

This ensures zero-downtime rolling updates for cache availability.


SASL Authentication

When spec.security.sasl.enabled is true, the operator configures memcached for SASL authentication by mounting a credentials Secret and adding the -Y flag to the container arguments.

Configuration

yaml
spec:
  security:
    sasl:
      enabled: true
      credentialsSecretRef:
        name: memcached-sasl-credentials

The referenced Secret must contain a password-file key with the SASL password file content (username:password pairs in memcached's expected format).

Helper Functions

buildSASLVolume(mc *Memcached) *corev1.Volume — Returns a Volume named sasl-credentials that references the Secret from spec.security.sasl.credentialsSecretRef.name, or nil when SASL is not enabled (security is nil, SASL is nil, or enabled is false).

buildSASLVolumeMount(mc *Memcached) *corev1.VolumeMount — Returns a read-only VolumeMount named sasl-credentials at /etc/memcached/sasl/, or nil when SASL is not enabled.

Volume and Mount Details

PropertyValue
Volume namesasl-credentials
Volume sourceSecret (from credentialsSecretRef)
Mount path/etc/memcached/sasl/
Read-onlytrue
Secret keypassword-file

Container Args

When SASL is enabled, buildMemcachedArgs appends -Y /etc/memcached/sasl/password-file after verbosity flags and before extraArgs:

text
["-m", "64", "-c", "1024", "-t", "4", "-I", "1m", "-Y", "/etc/memcached/sasl/password-file"]

Integration with constructDeployment

The SASL volume mount is added only to the memcached container. When monitoring is enabled, the exporter sidecar does not receive the SASL volume mount. SASL coexists with all other features:

FeatureInteraction
Pod security contextSASL volume/mount added alongside pod-level security settings
Container security contextSASL mount present on the same container with security context
Monitoring sidecarExporter container does not get the SASL volume mount
Graceful shutdownLifecycle preStop hook coexists with SASL volume mount
Extra args-Y flag appears before extraArgs in argument list

Disabled Behavior

When SASL is not enabled (spec.security is nil, spec.security.sasl is nil, or spec.security.sasl.enabled is false):

  • No -Y flag in container args
  • No sasl-credentials volume on the pod
  • No SASL volume mount on any container
  • Existing Memcached instances continue to work unchanged

RBAC

The operator's ClusterRole includes get, list, watch permissions for core/v1 Secrets to support reading the SASL credentials Secret. This is generated from the RBAC marker on the controller:

go
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch

Reconciliation Method

reconcileDeployment(ctx, mc *Memcached) ([]string, error) on MemcachedReconciler ensures the Deployment matches the desired state. It fetches referenced Secrets, computes a hash for rolling-update annotations, reads the restart-trigger annotation from the CR, and passes everything to constructDeployment. It returns the names of any missing Secrets for use by status reconciliation.

go
func (r *MemcachedReconciler) reconcileDeployment(ctx context.Context, mc *memcachedv1alpha1.Memcached) ([]string, error) {
    found, missing := fetchReferencedSecrets(ctx, r.Client, mc)
    secretHash := computeSecretHash(found...)
    restartTrigger := mc.Annotations[AnnotationRestartTrigger]

    dep := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      mc.Name,
            Namespace: mc.Namespace,
        },
    }

    _, err := r.reconcileResource(ctx, mc, dep, func() error {
        constructDeployment(mc, dep, secretHash, restartTrigger)
        return nil
    }, "Deployment")
    return missing, err
}

CreateOrUpdate Behavior

controllerutil.CreateOrUpdate performs a server-side get-or-create:

ScenarioMutate Function CalledAPI Operationresult Value
Deployment does not existYesCreatecontrollerutil.OperationResultCreated
Deployment exists, spec differsYesUpdatecontrollerutil.OperationResultUpdated
Deployment exists, spec matchesYes(no-op)controllerutil.OperationResultNone

The mutate function runs before every create or update, ensuring the Deployment always reflects the current CR spec. External drift (manual edits) is corrected on the next reconciliation cycle.

Owner Reference

controllerutil.SetControllerReference adds an owner reference to the Deployment's metadata:

FieldValue
apiVersionmemcached.c5c3.io/v1alpha1
kindMemcached
name<cr-name>
uid<cr-uid>
controllertrue
blockOwnerDeletiontrue

This enables:

  • Garbage collection: Deleting the Memcached CR automatically deletes the owned Deployment via Kubernetes' owner reference cascade.
  • Watch filtering: The Owns(&appsv1.Deployment{}) watch on the controller maps Deployment events back to the owning Memcached CR for reconciliation.

Error Handling

Error ScenarioBehavior
API server unreachableError returned, controller-runtime requeues with backoff
Deployment create/update failsError wrapped with context, returned for requeue
Owner reference conflictError from SetControllerReference, returned for requeue

Errors are wrapped with fmt.Errorf("reconciling Deployment: %w", err) to provide context in logs while preserving the original error for apierrors.IsXxx() checks upstream.


Reconcile Integration

The Reconcile method calls reconcileDeployment after fetching the Memcached CR:

go
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    memcached := &memcachedv1alpha1.Memcached{}
    if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
        if apierrors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    missingSecrets, err := r.reconcileDeployment(ctx, memcached)
    if err != nil {
        return ctrl.Result{}, err
    }

    // ... reconcileService, reconcilePDB, reconcileServiceMonitor, reconcileNetworkPolicy ...

    if err := r.reconcileStatus(ctx, memcached, missingSecrets); err != nil {
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}
ScenarioReturn ValueEffect
CR not found (deleted)ctrl.Result{}, nilNo requeue; owner ref cascade handles Deployment cleanup
CR fetch failsctrl.Result{}, errRequeue with exponential backoff
Deployment reconcile succeedsctrl.Result{}, nilNo requeue; missing Secrets forwarded to status
Deployment reconcile failsctrl.Result{}, errRequeue with exponential backoff

Reconciliation Flow

text
  Memcached CR created/updated


  ┌─────────────────────────────┐
  │  Reconcile                  │
  │  1. Fetch Memcached CR      │
  │  2. If NotFound → return    │
  │  3. If error → requeue      │
  └────────────┬────────────────┘


  ┌─────────────────────────────┐
  │  reconcileDeployment        │
  │  → returns missingSecrets   │
  │                             │
  │  1. fetchReferencedSecrets  │
  │  2. computeSecretHash       │
  │  3. Read restart-trigger    │
  │                             │
  │  CreateOrUpdate:            │
  │    ┌──────────────────────┐ │
  │    │ Mutate function      │ │
  │    │  constructDeployment │ │
  │    │  (secretHash,        │ │
  │    │   restartTrigger)    │ │
  │    │  SetControllerRef    │ │
  │    └──────────────────────┘ │
  │                             │
  │  Deployment                 │
  │  ├─ Name: <cr-name>        │
  │  ├─ Namespace: <cr-ns>     │
  │  ├─ Replicas: spec/default │
  │  ├─ Image: spec/default    │
  │  ├─ Args: buildMemcachedArgs│
  │  ├─ Port: 11211/TCP        │
  │  ├─ Probes: TCP socket     │
  │  ├─ Strategy: RollingUpdate│
  │  ├─ Volumes: SASL (if on)  │
  │  ├─ VolumeMounts: SASL     │
  │  ├─ PodAnnotations:        │
  │  │  ├─ secret-hash         │
  │  │  └─ restart-trigger     │
  │  └─ OwnerRef → Memcached CR│
  └─────────────────────────────┘

Deployment Manifest Example

A Memcached CR with custom settings:

yaml
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
  name: my-cache
  namespace: default
spec:
  replicas: 3
  image: "memcached:1.6.29"
  resources:
    requests:
      cpu: "100m"
      memory: "128Mi"
    limits:
      cpu: "500m"
      memory: "256Mi"
  memcached:
    maxMemoryMB: 128
    maxConnections: 2048
    threads: 8
    maxItemSize: "2m"
    verbosity: 1
    extraArgs: ["-o", "modern"]

Produces a Deployment with:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-cache
  namespace: default
  labels:
    app.kubernetes.io/name: memcached
    app.kubernetes.io/instance: my-cache
    app.kubernetes.io/managed-by: memcached-operator
  ownerReferences:
    - apiVersion: memcached.c5c3.io/v1alpha1
      kind: Memcached
      name: my-cache
      controller: true
      blockOwnerDeletion: true
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: memcached
      app.kubernetes.io/instance: my-cache
      app.kubernetes.io/managed-by: memcached-operator
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: memcached
        app.kubernetes.io/instance: my-cache
        app.kubernetes.io/managed-by: memcached-operator
    spec:
      containers:
        - name: memcached
          image: "memcached:1.6.29"
          args:
            - "-m"
            - "128"
            - "-c"
            - "2048"
            - "-t"
            - "8"
            - "-I"
            - "2m"
            - "-v"
            - "-o"
            - "modern"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          ports:
            - name: memcached
              containerPort: 11211
              protocol: TCP
          livenessProbe:
            tcpSocket:
              port: memcached
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            tcpSocket:
              port: memcached
            initialDelaySeconds: 5
            periodSeconds: 5

SASL-Enabled CR Example

yaml
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
  name: my-cache
  namespace: default
spec:
  replicas: 3
  image: "memcached:1.6.29"
  security:
    sasl:
      enabled: true
      credentialsSecretRef:
        name: memcached-sasl-credentials

Produces a Deployment with SASL volume and mount on the memcached container:

yaml
spec:
  template:
    spec:
      volumes:
        - name: sasl-credentials
          secret:
            secretName: memcached-sasl-credentials
      containers:
        - name: memcached
          image: "memcached:1.6.29"
          args:
            - "-m"
            - "64"
            - "-c"
            - "1024"
            - "-t"
            - "4"
            - "-I"
            - "1m"
            - "-Y"
            - "/etc/memcached/sasl/password-file"
          volumeMounts:
            - name: sasl-credentials
              mountPath: /etc/memcached/sasl
              readOnly: true

Minimal CR Example

yaml
apiVersion: memcached.c5c3.io/v1alpha1
kind: Memcached
metadata:
  name: my-cache
spec: {}

Produces a Deployment with 1 replica, image memcached:1.6, args ["-m", "64", "-c", "1024", "-t", "4", "-I", "1m"], no resource limits, and the same labels, probes, strategy, and owner reference.