-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add option to manually set maintenance mode
Use `python -m pyramid_heroku.maintenance` to manually set Heroku maintenance mode. Also, some cleanup and refactor.
- Loading branch information
1 parent
19f76c8
commit 198be5f
Showing
9 changed files
with
438 additions
and
277 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
"""Heroku API client.""" | ||
|
||
from requests import Response | ||
from requests import Session | ||
from typing import Optional | ||
|
||
import os | ||
|
||
|
||
class Heroku(object): | ||
|
||
api_endpoint = "https://api.heroku.com" | ||
|
||
def __init__(self, app_name: str, ini_file: str) -> None: | ||
""" | ||
:param app_name: Name of Heroku app or id. | ||
:param ini_file: development.ini or production.ini filename. | ||
""" | ||
self._formation = None | ||
self.app_name = app_name | ||
self.ini_file = ini_file | ||
|
||
headers = { | ||
"Authorization": f"Bearer {self.auth_key}", | ||
"Accept": "application/vnd.heroku+json; version=3", | ||
"Content-Type": "application/json", | ||
} | ||
self.session = Session() | ||
self.session.headers.update(headers) | ||
|
||
@property | ||
def auth_key(self) -> Optional[str]: | ||
"""Heroku API secret. | ||
https://devcenter.heroku.com/articles/platform-api-quickstart#authentication. | ||
""" | ||
return os.environ.get("MIGRATE_API_SECRET_HEROKU") | ||
|
||
def scale_down(self): | ||
"""Scale all app workers to 0.""" | ||
updates = [dict(type=t, quantity=0) for t in self.formation.keys()] | ||
res = self.session.patch( | ||
f"{self.api_endpoint}/apps/{self.app_name}/formation", | ||
json=dict(updates=updates), | ||
) | ||
self.parse_response(res) | ||
print("Scaled down to:") | ||
for x in res.json(): | ||
print(f'{x["type"]}={x["quantity"]}') | ||
|
||
def scale_up(self): | ||
"""Scale app back to initial state.""" | ||
updates = [dict(type=t, quantity=s) for t, s in self.formation.items()] | ||
res = self.session.patch( | ||
f"{self.api_endpoint}/apps/{self.app_name}/formation", | ||
json=dict(updates=updates), | ||
) | ||
self.parse_response(res) | ||
print("Scaled up to:") | ||
for x in res.json(): | ||
print(f'{x["type"]}={x["quantity"]}') | ||
|
||
@property | ||
def formation(self): | ||
"""Get current app status and configuration. | ||
:return: Heroku app status as dict. | ||
""" | ||
if not self._formation: | ||
res = self.session.get( | ||
f"{self.api_endpoint}/apps/{self.app_name}/formation" | ||
) | ||
self.parse_response(res) | ||
self._formation = {x["type"]: x["quantity"] for x in res.json()} | ||
return self._formation | ||
|
||
def get_maintenance(self) -> bool: | ||
res = self.session.get(f"{self.api_endpoint}/apps/{self.app_name}") | ||
self.parse_response(res) | ||
return res.json()["maintenance"] | ||
|
||
def set_maintenance(self, state: bool) -> None: | ||
res = self.session.patch( | ||
f"{self.api_endpoint}/apps/{self.app_name}", json=dict(maintenance=state) | ||
) | ||
if self.parse_response(res): | ||
print("Maintenance {}".format("enabled" if state else "disabled")) | ||
|
||
def parse_response(self, res: Response) -> Optional[bool]: | ||
""" | ||
Parses Heroku API response. | ||
:param res: requests object | ||
:return: true if request succeeded | ||
""" | ||
if res.status_code != 200: | ||
print(res.json()) | ||
res.raise_for_status() | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
"""Enable or disable Heroku app maintenance mode.""" | ||
|
||
from enum import Enum | ||
from pyramid_heroku.heroku import Heroku | ||
|
||
import argparse | ||
|
||
|
||
class Mode(Enum): | ||
"""Heroku maintenance modes.""" | ||
|
||
on = "ON" | ||
off = "OFF" | ||
|
||
|
||
def main() -> None: | ||
parser = argparse.ArgumentParser( | ||
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
usage=( | ||
"usage: maintenance.py [-h] [app_name] [ini_file] [on|off]" | ||
"\nexample: python -m pyramid_heroku.maintenance on my_app etc/production.ini" | ||
), | ||
) | ||
parser.add_argument("mode", choices=[x.name for x in Mode], help="Maintenance mode") | ||
parser.add_argument("app_name", help="Heroku app name") | ||
parser.add_argument( | ||
"ini_file", | ||
nargs="?", | ||
default="etc/production.ini", | ||
help="Path to Pyramid configuration file ", | ||
) | ||
|
||
options = parser.parse_args() | ||
|
||
h = Heroku(options.app_name, options.ini_file) | ||
|
||
if Mode[options.mode] == Mode.on: | ||
h.set_maintenance(True) | ||
else: | ||
h.set_maintenance(False) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.