Skip to content

Commit 39d5b5b

Browse files
committed
CCS-4538: Provide an endpoint in Git2Pantheon that would handle cache clearing for both Drupal and Akamai.
1 parent a52e9e9 commit 39d5b5b

File tree

14 files changed

+304
-13
lines changed

14 files changed

+304
-13
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ flask run
4343
```
4444

4545
8. The swagger docs can be found at:
46-
http://localhost:5000/apidocs/
46+
http://localhost:5000/apidocs/
47+
48+
_Note_: _Please don't try to run cache clear API_
4749

git2pantheon/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from flasgger import Swagger
77
from .api.upload import api_blueprint, executor
88
import atexit
9+
from .helpers import EnvironmentVariablesHelper
910

1011

1112
def create_app():
@@ -15,11 +16,23 @@ def create_app():
1516
EXECUTOR_MAX_WORKERS="1",
1617
EXECUTOR_PROPAGATE_EXCEPTIONS=True
1718
)
19+
# check if required vars are available
20+
EnvironmentVariablesHelper.check_vars(['PANTHEON_SERVER', 'UPLOADER_PASSWORD','UPLOADER_USER'])
21+
try:
22+
EnvironmentVariablesHelper.check_vars(['AKAMAI_HOST', 'DRUPAL_HOST', 'AKAMAI_ACCESS_TOKEN',
23+
'AKAMAI_CLIENT_SECRET','AKAMAI_CLIENT_TOKEN'])
24+
except Exception as e:
25+
print(
26+
'Environment variable(s) for cache clear not present.'
27+
'Details={0}Please ignore if you are running on local server'.format(
28+
str(e)))
29+
1830
app.config.from_mapping(
1931
PANTHEON_SERVER=os.environ['PANTHEON_SERVER'],
2032
UPLOADER_PASSWORD=os.environ['UPLOADER_PASSWORD'],
2133
UPLOADER_USER=os.environ['UPLOADER_USER']
2234
)
35+
2336
gunicorn_error_logger = logging.getLogger('gunicorn.error')
2437
app.logger.handlers.extend(gunicorn_error_logger.handlers)
2538
logging.basicConfig(level=logging.DEBUG)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Clears the drupal and akamai cache
2+
---
3+
tags:
4+
- Clears the drupal and akamai cache.
5+
6+
parameters:
7+
- in: body
8+
name: body
9+
description: 'The structure containing list of URLs of modules and assemblies whose cache has to be cleared'
10+
required: true
11+
schema:
12+
$ref: '#/definitions/CacheClearData'
13+
14+
reponses:
15+
200:
16+
description: 'The status of upload corresponding to the key'
17+
schema:
18+
$ref: '#/definitions/UploaderKey'
19+
400:
20+
description: 'Invalid content error'
21+
schema:
22+
$ref: '#/definitions/Error'
23+
500:
24+
description: 'Internal server error'
25+
schema:
26+
$ref: '#/definitions/Error'
27+
28+
29+
definitions:
30+
CacheClearData:
31+
type: object
32+
properties:
33+
assemblies:
34+
type: array
35+
items:
36+
type: string
37+
modules:
38+
type: array
39+
items:
40+
type: string
41+
Error:
42+
type: object
43+
properties:
44+
code:
45+
type: string
46+
description: 'HTTP status code of the error'
47+
message:
48+
type: string
49+
description: 'Error message'
50+
details:
51+
type: string
52+
description: 'Error details'
53+

git2pantheon/api/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
from flask_executor import Executor
33
from flask_cors import CORS
44
from git2pantheon.utils import ApiError
5+
from git2pantheon.clients.akamai.akamai_rest_client import AkamaiCachePurgeClient
6+
from git2pantheon.clients.drupal.drupal_rest_client import DrupalClient
7+
import os
58

69
executor = Executor()
10+
akamai_purge_client = AkamaiCachePurgeClient(host=os.getenv('AKAMAI_HOST'),
11+
client_token=os.getenv('AKAMAI_CLIENT_TOKEN'),
12+
client_secret=os.getenv('AKAMAI_CLIENT_SECRET'),
13+
access_token=os.getenv('AKAMAI_ACCESS_TOKEN'))
14+
drupal_client = DrupalClient(os.getenv('DRUPAL_HOST'))
715
api_blueprint = Blueprint('api', __name__, url_prefix='/api/')
816
CORS(api_blueprint)
917

git2pantheon/api/upload.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@
1414
from pantheon_uploader import pantheon
1515
from . import api_blueprint
1616
from . import executor
17+
from . import drupal_client
18+
from . import akamai_purge_client
1719
from .. import utils
18-
from ..helpers import FileHelper, GitHelper, MessageHelper
20+
from ..helpers import FileHelper, GitHelper, MessageHelper, CacheObjectHelper
1921
from ..messaging import broker
2022
from ..models.request_models import RepoSchema
2123
from ..models.response_models import Status
2224
from ..utils import ApiError, get_docs_path_for
2325
from flask import current_app
26+
from decorest import HTTPErrorWrapper
2427

28+
ASSEMBLIES = "assemblies"
29+
MODULES = "modules"
2530
logger = logging.getLogger(__name__)
2631

2732

@@ -44,7 +49,7 @@ def push_repo():
4449

4550
def clone_repo(repo_name, repo_url, branch):
4651
try:
47-
52+
4853
MessageHelper.publish(repo_name + "-clone",
4954
json.dumps(dict(current_status="cloning", details="Cloning repo " + repo_name + "")))
5055
logger.info("Cloning repo=" + repo_url + " and branch=" + branch)
@@ -81,8 +86,8 @@ def upload_repo(cloned_repo, channel_name):
8186
try:
8287
pantheon.start_process(numeric_level=10, pw=current_app.config['UPLOADER_PASSWORD'],
8388
user=current_app.config['UPLOADER_USER'],
84-
server=current_app.config['PANTHEON_SERVER'], directory=cloned_repo.working_dir,
85-
use_broker=True, channel=channel_name, broker_host= os.getenv('REDIS_SERVICE') )
89+
server=current_app.config['PANTHEON_SERVER'], directory=cloned_repo.working_dir,
90+
use_broker=True, channel=channel_name, broker_host=os.getenv('REDIS_SERVICE'))
8691
except Exception as e:
8792
logger.error("Upload failed due to error=" + str(e))
8893
MessageHelper.publish(channel_name,
@@ -105,7 +110,7 @@ def info():
105110
def status():
106111
status_data, clone_data = get_upload_data()
107112
current_status = get_current_status(clone_data, status_data)
108-
logger.debug("current status="+current_status)
113+
logger.debug("current status=" + current_status)
109114
status_message = Status(clone_status=clone_data.get('current_status', ""),
110115
current_status=current_status,
111116
file_type=status_data.get('type_uploading', ""),
@@ -120,8 +125,8 @@ def status():
120125

121126

122127
def get_current_status(clone_data, status_data):
123-
logger.debug('upload status data='+json.dumps(status_data))
124-
logger.debug('clone status data='+json.dumps(clone_data))
128+
logger.debug('upload status data=' + json.dumps(status_data))
129+
logger.debug('clone status data=' + json.dumps(clone_data))
125130
return status_data.get('current_status') if status_data.get('current_status', "") != "" else clone_data[
126131
'current_status']
127132

@@ -147,7 +152,7 @@ def get_request_data():
147152

148153

149154
def reset_if_exists(repo_name):
150-
MessageHelper.publish(repo_name +"-clone", json.dumps(dict(current_status='')))
155+
MessageHelper.publish(repo_name + "-clone", json.dumps(dict(current_status='')))
151156
MessageHelper.publish(repo_name, json.dumps(dict(current_status='')))
152157

153158

@@ -157,7 +162,7 @@ def progress_update():
157162
status_data, clone_data = get_upload_data()
158163
# status_progress: UploadStatus = upload_status_from_dict(status_data)
159164
if "server" in status_data and status_data["server"]["response_code"] and not 200 <= int(status_data["server"][
160-
"response_code"]) <= 400:
165+
"response_code"]) <= 400:
161166
return jsonify(
162167
dict(
163168
server_status=status_data["server"]["response_code"],
@@ -265,3 +270,52 @@ def progress_update_resources():
265270
return jsonify(
266271
response_dict
267272
), 200
273+
274+
275+
@swag_from(get_docs_path_for('cache_clear_api.yaml'))
276+
@api_blueprint.route('/cache/clear', methods=['POST'])
277+
def clear_cache():
278+
data = get_request_data()
279+
cache_clear_result = {
280+
"drupal_result_assemblies":{},
281+
"drupal_result_modules": {}
282+
}
283+
284+
try:
285+
clear_drupal_cache(data, cache_clear_result)
286+
clear_akamai_cache(data,cache_clear_result)
287+
except Exception as e:
288+
logger.error("Exception occurred while trying to clear cache with error=" + str(e))
289+
raise ApiError("Upstream Server Error", 503, details=str(e))
290+
return jsonify(cache_clear_result)
291+
292+
293+
def drupal_cache_clear_bulk(cache_clear_result, cache_req_data):
294+
if "assemblies" in cache_req_data:
295+
cache_clear_result["drupal_result_assemblies"] = drupal_client.purge_cache_assembly("assemblies")
296+
297+
if "modules" in cache_req_data:
298+
cache_clear_result["drupal_result_modules"] = drupal_client.purge_cache_assembly("modules")
299+
300+
301+
def clear_drupal_cache(data, cache_clear_result, bulk_clear=False):
302+
cache_req_data = CacheObjectHelper.get_drupal_req_data(data)
303+
if bulk_clear:
304+
drupal_cache_clear_bulk(cache_clear_result, cache_req_data)
305+
return
306+
drupal_cache_clear_individual(cache_clear_result, cache_req_data)
307+
308+
309+
def drupal_cache_clear_individual(cache_clear_result, cache_req_data):
310+
if ASSEMBLIES in cache_req_data:
311+
for guid in cache_req_data[ASSEMBLIES]:
312+
cache_clear_result["drupal_result_assemblies"][str(guid)] = drupal_client.purge_cache_assembly(guid)
313+
if MODULES in cache_req_data:
314+
for guid in cache_req_data[MODULES]:
315+
cache_clear_result["drupal_result_modules"][str(guid)] = (drupal_client.purge_cache_module(guid))
316+
317+
318+
def clear_akamai_cache(data, cache_clear_result):
319+
cache_req_data = CacheObjectHelper.get_akamai_req_object(data)
320+
cache_clear_result['akamai_result'] = akamai_purge_client.purge(cache_req_data)
321+

git2pantheon/clients/__init__.py

Whitespace-only changes.

git2pantheon/clients/akamai/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import requests, logging, json
2+
from ..client import RestClient
3+
from akamai.edgegrid import EdgeGridAuth
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class AkamaiCachePurgeClient:
9+
def __init__(self,host, client_token, client_secret, access_token):
10+
self.session: requests.Session = requests.Session()
11+
self.session.auth = EdgeGridAuth(client_token=client_token, client_secret=client_secret,
12+
access_token=access_token)
13+
self.host = host
14+
self.akamai_rest_client = RestClient(auth_session=self.session, verbose=True,
15+
base_url=self.host)
16+
17+
def purge(self, purge_obj, action='delete'):
18+
logger.info('Adding %s request to the queue for %s' % (action, json.dumps(purge_obj)))
19+
return self.akamai_rest_client.post('/ccu/v3/delete/url', json.dumps(purge_obj))

git2pantheon/clients/client.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from urllib import parse
2+
3+
import json
4+
import logging
5+
import requests
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class RestClient:
11+
def __init__(self, auth_session, verbose, base_url):
12+
self.auth_session: requests.Session = auth_session
13+
self.verbose = verbose
14+
self.base_url = base_url
15+
self.errors = self.init_errors_dict()
16+
17+
def join_path(self, path):
18+
return parse.urljoin(self.base_url, path)
19+
20+
def get(self, endpoint, params=None):
21+
response = self.auth_session.get(self.join_path(endpoint), params=params)
22+
self.process_response(endpoint, response)
23+
return response.json()
24+
25+
def log_verbose(self, endpoint, response):
26+
if self.verbose:
27+
logger.info(
28+
'status=' + str(response.status_code) + ' for endpoint=' + endpoint +
29+
' with content type=' + response.headers['content-type']
30+
)
31+
logger.info("response body=" + json.dumps(response.json(), indent=2))
32+
33+
def post(self, endpoint, body, params=None):
34+
headers = {"content-type": "application/json"}
35+
response = self.auth_session.post(self.join_path(endpoint), data=body, headers=headers, params=params)
36+
self.process_response(endpoint, response)
37+
return response.json()
38+
39+
def process_response(self, endpoint, response):
40+
self.check_error(response, endpoint)
41+
self.log_verbose(endpoint, response)
42+
43+
@staticmethod
44+
def init_errors_dict():
45+
return {
46+
404: "Call to {URI} failed with a 404 result\n with details: {details}\n",
47+
403: "Call to {URI} failed with a 403 result\n with details: {details}\n",
48+
401: "Call to {URI} failed with a 401 result\n with details: {details}\n",
49+
400: "Call to {URI} failed with a 400 result\n with details: {details}\n"
50+
}
51+
52+
def check_error(self, response, endpoint):
53+
if not 200 >= response.status_code >= 400:
54+
return
55+
message = self.errors.get(response.status_code)
56+
if message:
57+
raise Exception(message.format(URI=self.join_path(endpoint), details=response.json()))

git2pantheon/clients/drupal/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)