Switch Memcached to use StatefulSet

While Memcached is not stateful, it is very useful to have a stable DNS
which will allow us to simplify the Mcrouter resource configuration.

Change-Id: I0776875ed24c8c8681fe4efd334a7e911c0ae4ce
This commit is contained in:
Mohammed Naser 2020-04-18 18:00:06 -04:00
parent f6ca81190c
commit 8bd3e49038
8 changed files with 152 additions and 59 deletions

View File

@ -10,6 +10,7 @@ rules:
- apps - apps
resources: resources:
- deployments - deployments
- statefulsets
verbs: verbs:
- create - create
- delete - delete

View File

@ -32,13 +32,22 @@ def create_or_resume(name, spec, **_):
start the service up for the first time. start the service up for the first time.
""" """
utils.create_or_update('memcached/deployment.yml.j2', utils.create_or_update('memcached/statefulset.yml.j2',
name=name, spec=spec)
utils.create_or_update('memcached/service.yml.j2',
name=name, spec=spec)
utils.create_or_update('memcached/mcrouter.yml.j2',
name=name, spec=spec) name=name, spec=spec)
utils.create_or_update('memcached/podmonitor.yml.j2', utils.create_or_update('memcached/podmonitor.yml.j2',
name=name, spec=spec) name=name, spec=spec)
utils.create_or_update('memcached/prometheusrule.yml.j2', utils.create_or_update('memcached/prometheusrule.yml.j2',
name=name, spec=spec) name=name, spec=spec)
# NOTE(mnaser): We should remove this once all deployments are no longer
# using Deployment for Memcached.
utils.ensure_absent('memcached/deployment.yml.j2',
name=name, spec=spec)
@kopf.on.update('infrastructure.vexxhost.cloud', 'v1alpha1', 'memcacheds') @kopf.on.update('infrastructure.vexxhost.cloud', 'v1alpha1', 'memcacheds')
def update(name, spec, **_): def update(name, spec, **_):
@ -48,26 +57,5 @@ def update(name, spec, **_):
changes that happen within it. changes that happen within it.
""" """
utils.create_or_update('memcached/deployment.yml.j2', utils.create_or_update('memcached/statefulset.yml.j2',
name=name, spec=spec) name=name, spec=spec)
@kopf.on.event('apps', 'v1', 'deployments', labels={
'app.kubernetes.io/managed-by': 'openstack-operator',
'app.kubernetes.io/name': 'memcached',
})
def deployment_event(namespace, meta, spec, **_):
"""Create and re-sync Mcrouter instances
This function takes care of watching for the readyReplicas on the
Deployments for Memcached to both update and synchronize the Mcrouter.
"""
name = meta.get('labels', {}).get('app.kubernetes.io/instance')
selector = spec.get('selector', {}).get('matchLabels', {})
servers = utils.get_ready_pod_ips(namespace, selector)
memcacheds = ["%s:11211" % s for s in servers]
utils.create_or_update('memcached/mcrouter.yml.j2',
name=name, servers=memcacheds,
spec=spec.get('template', {}).get('spec', {}))

View File

@ -27,6 +27,7 @@ from pykube.objects import Deployment
from pykube.objects import NamespacedAPIObject from pykube.objects import NamespacedAPIObject
from pykube.objects import Pod from pykube.objects import Pod
from pykube.objects import Service from pykube.objects import Service
from pykube.objects import StatefulSet
class Mcrouter(NamespacedAPIObject): class Mcrouter(NamespacedAPIObject):
@ -61,6 +62,7 @@ MAPPING = {
}, },
"apps/v1": { "apps/v1": {
"Deployment": Deployment, "Deployment": Deployment,
"StatefulSet": StatefulSet,
}, },
"infrastructure.vexxhost.cloud/v1alpha1": { "infrastructure.vexxhost.cloud/v1alpha1": {
"Mcrouter": Mcrouter, "Mcrouter": Mcrouter,

View File

@ -21,7 +21,8 @@ spec:
pools: pools:
default: default:
servers: servers:
{{ servers | to_yaml | indent(8) }} - memcached-{{ name }}-0.memcached-{{ name }}:11211
- memcached-{{ name }}-1.memcached-{{ name }}:11211
route: PoolRoute|default route: PoolRoute|default
{% if 'nodeSelector' in spec %} {% if 'nodeSelector' in spec %}
nodeSelector: nodeSelector:

View File

@ -0,0 +1,27 @@
---
# Copyright 2020 VEXXHOST, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: v1
kind: Service
metadata:
name: memcached-{{ name }}
spec:
clusterIP: None
ports:
- name: memcached
port: 11211
targetPort: memcached
selector:
{{ labels("memcached", name) | indent(4) }}

View File

@ -0,0 +1,90 @@
---
# Copyright 2020 VEXXHOST, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: memcached-{{ name }}
labels:
{{ labels("memcached", name) | indent(4) }}
spec:
replicas: 2
serviceName: memcached-{{ name }}
selector:
matchLabels:
{{ labels("memcached", name) | indent(6) }}
template:
metadata:
labels:
{{ labels("memcached", name) | indent(8) }}
spec:
containers:
- name: memcached
image: vexxhost/memcached:latest
args: ["-m", "{{ (spec.megabytes / 2) | int }}"]
imagePullPolicy: Always
ports:
- name: memcached
containerPort: 11211
livenessProbe:
tcpSocket:
port: memcached
readinessProbe:
tcpSocket:
port: memcached
resources:
limits:
cpu: 50m
ephemeral-storage: 50M
memory: {{ (spec.megabytes / 2) | int + 64 }}M
requests:
cpu: 10m
ephemeral-storage: 50M
memory: {{ (spec.megabytes / 2) | int }}M
securityContext:
runAsUser: 1001
- name: exporter
image: vexxhost/memcached_exporter:latest
imagePullPolicy: Always
ports:
- name: metrics
containerPort: 9150
livenessProbe:
httpGet:
path: /metrics
port: metrics
readinessProbe:
httpGet:
path: /metrics
port: metrics
resources:
limits:
cpu: 100m
ephemeral-storage: 10M
memory: 64Mi
requests:
cpu: 50m
ephemeral-storage: 10M
memory: 32Mi
securityContext:
runAsUser: 1001
{% if 'nodeSelector' in spec %}
nodeSelector:
{{ spec.nodeSelector | to_yaml | indent(8) }}
{% endif %}
{% if 'tolerations' in spec %}
tolerations:
{{ spec.tolerations | to_yaml | indent(8) }}
{% endif %}

View File

@ -27,40 +27,13 @@ from oslotest import base
from openstack_operator import memcached from openstack_operator import memcached
class MemcachedListTestCase(base.BaseTestCase): class MemcachedOperatorTestCase(base.BaseTestCase):
"""Tests for determining server list.""" """Basic tests for the operator."""
@mock.patch.object(memcached.utils, 'get_ready_pod_ips')
@mock.patch.object(memcached.utils, 'create_or_update') @mock.patch.object(memcached.utils, 'create_or_update')
def test_with_no_ips(self, mock_create, mock_get_ready_pods): @mock.patch.object(memcached.utils, 'ensure_absent')
"""Test a deployment with no ready pods.""" def test_ensure_deployment_removal(self, mock_ensure_absent, _):
"""Test that we remove the old deployment"""
mock_get_ready_pods.return_value = [] memcached.create_or_resume("foo", {})
memcached.deployment_event("default", {}, {}) mock_ensure_absent.assert_called_once_with(
'memcached/deployment.yml.j2', name="foo", spec={})
mock_create.assert_called_once_with('memcached/mcrouter.yml.j2',
name=None, servers=[], spec={})
@mock.patch.object(memcached.utils, 'get_ready_pod_ips')
@mock.patch.object(memcached.utils, 'create_or_update')
def test_with_single_ip(self, mock_create, mock_get_ready_pods):
"""Test a deployment with a single ready pod."""
mock_get_ready_pods.return_value = ['1.1.1.1']
memcached.deployment_event("default", {}, {})
mock_create.assert_called_once_with(
'memcached/mcrouter.yml.j2', name=None,
servers=['1.1.1.1:11211'], spec={})
@mock.patch.object(memcached.utils, 'get_ready_pod_ips')
@mock.patch.object(memcached.utils, 'create_or_update')
def test_multiple_ips(self, mock_create, mock_get_ready_pods):
"""Test a deployment with a multiple ready pods."""
mock_get_ready_pods.return_value = ['1.1.1.1', '2.2.2.2']
memcached.deployment_event("default", {}, {})
mock_create.assert_called_once_with(
'memcached/mcrouter.yml.j2', name=None,
servers=['1.1.1.1:11211', '2.2.2.2:11211'], spec={})

View File

@ -77,6 +77,17 @@ def create_or_update(template, **kwargs):
resource.create() resource.create()
def ensure_absent(template, **kwargs):
"""Ensure a Kubernetes resource bound to a template is deleted
This function gets a template and makes sure that the object doesn't
exist on the remote cluster.
"""
resource = generate_object(template, **kwargs)
resource.delete()
def generate_yaml(template, **kwargs): def generate_yaml(template, **kwargs):
"""Generate dictionary from YAML template. """Generate dictionary from YAML template.