Projet

Général

Profil

0001-add-caching-to-health-API-26836.patch

Frédéric Péters, 29 novembre 2018 08:13

Télécharger (12,4 ko)

Voir les différences:

Subject: [PATCH 1/2] add caching to health API (#26836)

 hobo/environment/models.py | 19 ++++++++++++
 hobo/rest/__init__.py      |  0
 hobo/rest/serializers.py   | 27 -----------------
 hobo/rest/urls.py          | 22 --------------
 hobo/rest/views.py         | 46 -----------------------------
 hobo/urls.py               |  5 ++--
 hobo/views.py              |  8 ++++-
 tests/test_health_api.py   | 60 +++++++++++++++++++++++++++-----------
 8 files changed, 71 insertions(+), 116 deletions(-)
 delete mode 100644 hobo/rest/__init__.py
 delete mode 100644 hobo/rest/serializers.py
 delete mode 100644 hobo/rest/urls.py
 delete mode 100644 hobo/rest/views.py
hobo/environment/models.py
1 1
import datetime
2 2
import json
3
import random
3 4
import requests
4 5
import socket
5 6

  
6 7
from django.conf import settings
8
from django.core.cache import cache
7 9
from django.db import models
8 10
from django.utils.crypto import get_random_string
9 11
from django.utils.encoding import force_text
......
189 191
        r = requests.get(self.get_admin_zones()[0].href, verify=False)
190 192
        return bool(r.status_code is 200)
191 193

  
194
    def get_health_dict(self):
195
        properties = [
196
            ('is_resolvable', 120),
197
            ('has_valid_certificate', 3600),
198
            ('is_running', 60),
199
            ('is_operational', 60),
200
        ]
201
        result = {}
202
        for name, cache_duration in properties:
203
            cache_key = '%s_%s' % (self.slug, name)
204
            value = cache.get(cache_key)
205
            if value is None:
206
                value = getattr(self, name)()
207
                cache.set(cache_key, value, cache_duration * (0.5 + random.random()))
208
            result[name] = value
209
        return result
210

  
192 211

  
193 212
class Authentic(ServiceBase):
194 213
    use_as_idp_for_self = models.BooleanField(
hobo/rest/serializers.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2018  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from rest_framework import serializers
18

  
19

  
20
class HealthBaseSerializer(serializers.Serializer):
21
    title = serializers.CharField()
22
    slug = serializers.SlugField()
23
    base_url = serializers.CharField()
24
    is_resolvable = serializers.BooleanField()
25
    has_valid_certificate = serializers.BooleanField()
26
    is_running = serializers.BooleanField()
27
    is_operational = serializers.BooleanField()
hobo/rest/urls.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2018  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18
from .views import HealthList
19

  
20
urlpatterns = [
21
    url(r'^health/$', HealthList.as_view(), name='api-health-list'),
22
]
hobo/rest/views.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2018  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from rest_framework import generics
18
from rest_framework import permissions
19
from rest_framework.response import Response
20

  
21
from hobo.environment.utils import get_installed_services
22
from hobo.rest.serializers import HealthBaseSerializer
23

  
24

  
25
class HealthList(generics.ListAPIView):
26
    serializer_class = HealthBaseSerializer
27
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
28

  
29
    def get_queryset(self):
30
        return [x for x in get_installed_services() if not x.secondary]
31

  
32
    def list(self, request, *args, **kwargs):
33
        """
34
        Custom dictionary object
35
        """
36
        self.object_list = self.filter_queryset(self.get_queryset())
37

  
38
        # Switch between paginated or standard style responses
39
        page = self.paginate_queryset(self.object_list)
40
        if page is not None:
41
            serializer = self.get_pagination_serializer(page)
42
        else:
43
            serializer = self.get_serializer(self.object_list, many=True)
44

  
45
        response = {d['slug']: d for d in serializer.data}
46
        return Response({'data': response})
hobo/urls.py
4 4
from django.contrib import admin
5 5
admin.autodiscover()
6 6

  
7
from .views import admin_required, login, login_local, logout, home, menu_json, hobo
7
from .views import admin_required, login, login_local, logout, home, health_json, menu_json, hobo
8 8
from .urls_utils import decorated_includes
9 9
from .environment.urls import urlpatterns as environment_urls
10 10
from .profile.urls import urlpatterns as profile_urls
11 11
from .theme.urls import urlpatterns as theme_urls
12 12
from .emails.urls import urlpatterns as emails_urls
13
from .rest.urls import urlpatterns as rest_urls
14 13

  
15 14
urlpatterns = [
16 15
    url(r'^$', home, name='home'),
......
21 20
    url(r'^theme/', decorated_includes(admin_required,
22 21
                                             include(theme_urls))),
23 22
    url(r'^emails/', decorated_includes(admin_required, include(emails_urls))),
24
    url(r'^api/', include(rest_urls)),
23
    url(r'^api/health/$', health_json, name='health-json'),
25 24
    url(r'^menu.json$', menu_json, name='menu_json'),
26 25
    url(r'^hobos.json$', hobo),
27 26
    url(r'^admin/', include(admin.site.urls)),
hobo/views.py
2 2
import urllib
3 3

  
4 4
from django.conf import settings
5
from django.http import HttpResponse, HttpResponseRedirect
5
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
6 6
from django.utils.translation import ugettext as _
7 7
from django.views.generic.base import TemplateView
8 8
from django.views.generic import edit
......
119 119
    return HttpResponseRedirect(request.GET.get('next')
120 120
                                or request.build_absolute_uri('/'))
121 121

  
122

  
123
def health_json(request):
124
    data = {x.slug: x.get_health_dict() for x in get_installed_services() if not x.secondary}
125
    return JsonResponse({'data': data})
126

  
127

  
122 128
@admin_required
123 129
def menu_json(request):
124 130
    response = HttpResponse(content_type='application/json')
tests/test_health_api.py
4 4
import requests
5 5
import socket
6 6

  
7
from django.core.cache import cache
7 8
from django.utils import timezone
8 9

  
10
from httmock import urlmatch, remember_called, HTTMock
11

  
9 12
from hobo.environment.models import Authentic, Combo
10 13

  
11 14
pytestmark = pytest.mark.django_db
......
22 25
    c.save()
23 26

  
24 27

  
25
def test_response(app, services, monkeypatch):
28
def test_response(app, admin_user, services, monkeypatch):
29
    cache.clear()
26 30
    monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
27 31
    monkeypatch.setattr(requests, 'get', lambda x, verify: MagicMock(status_code=200))
28 32
    response = app.get('/api/health/')
......
32 36
    assert 'jazz' in content['data'].keys()
33 37

  
34 38

  
35
def test_is_resolvable(app, services, monkeypatch):
39
def test_is_resolvable(app, admin_user, services, monkeypatch):
40
    cache.clear()
36 41
    def gethostname(netloc):
37 42
        if netloc == "jazz.example.publik":
38 43
            return '176.31.123.109'
......
48 53
    assert jazz['is_resolvable']
49 54

  
50 55

  
51
def test_is_running(app, services, monkeypatch):
52
    def get(url, verify):
53
        if url == 'https://jazz.example.publik/manage/':
54
            return MagicMock(status_code=200)
55
        else:
56
            return MagicMock(status_code=404)
56
def test_is_running(app, admin_user, services, monkeypatch):
57
    cache.clear()
57 58
    monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
58
    monkeypatch.setattr(requests, 'get', get)
59
    response = app.get('/api/health/')
60
    content = json.loads(response.content)
61
    blues = content['data']['blues']
62
    jazz = content['data']['jazz']
63
    assert not blues['is_running']
64
    assert jazz['is_running']
59

  
60
    @urlmatch(netloc='jazz.example.publik')
61
    @remember_called
62
    def jazz_mock(url, request):
63
        return {'status_code': 200}
64

  
65
    @urlmatch(netloc='blues.example.publik')
66
    @remember_called
67
    def blues_mock(url, request):
68
        return {'status_code': 404}
69

  
70
    with HTTMock(blues_mock, jazz_mock) as mock:
71
        response = app.get('/api/health/')
72
        content = json.loads(response.content)
73
        blues = content['data']['blues']
74
        jazz = content['data']['jazz']
75
        assert not blues['is_running']
76
        assert jazz['is_running']
77
        assert blues_mock.call['count'] == 2
78
        assert jazz_mock.call['count'] == 2
79

  
80
        # check it gets results from cache
81
        response = app.get('/api/health/')
82
        content = json.loads(response.content)
83
        blues = content['data']['blues']
84
        jazz = content['data']['jazz']
85
        assert not blues['is_running']
86
        assert jazz['is_running']
87
        assert blues_mock.call['count'] == 2
88
        assert jazz_mock.call['count'] == 2
65 89

  
66 90

  
67
def test_is_operational(app, services, monkeypatch):
91
def test_is_operational(app, admin_user, services, monkeypatch):
92
    cache.clear()
68 93
    monkeypatch.setattr(socket, 'gethostbyname', lambda x: '176.31.123.109')
69 94
    monkeypatch.setattr(requests, 'get', lambda x, verify: MagicMock(status_code=200))
70 95
    response = app.get('/api/health/')
......
75 100
    assert not jazz['is_operational']
76 101

  
77 102

  
78
def test_has_valid_certificate(app, services, monkeypatch):
103
def test_has_valid_certificate(app, admin_user, services, monkeypatch):
104
    cache.clear()
79 105
    def get(url, verify):
80 106
        if 'blues.example.publik' in url or not verify:
81 107
            return MagicMock(status_code=200)
82
-