Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DH-227] in preparation for running git precommits #6347

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions .github/scripts/determine-hub-deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@
If no hubs need deploying, nothing will be emitted.
"""

import argparse
import os


def main(args):
hubs = []

# Deploy all hubs by getting deployment names from the dirs in deployments/
if "GITHUB_PR_LABEL_JUPYTERHUB_DEPLOYMENT" in os.environ.keys() or \
"GITHUB_PR_LABEL_HUB_IMAGES" in os.environ.keys():
if (
"GITHUB_PR_LABEL_JUPYTERHUB_DEPLOYMENT" in os.environ.keys()
or "GITHUB_PR_LABEL_HUB_IMAGES" in os.environ.keys()
):
for deployment in next(os.walk(args.deployments))[1]:
if deployment not in args.ignore:
hubs.append(deployment)

# Deploy only the modified/flagged hubs by PR labels
else:
hub_labels = [
k.lower() for k in os.environ.keys()
if k.startswith("GITHUB_PR_LABEL_HUB_")
k.lower() for k in os.environ.keys() if k.startswith("GITHUB_PR_LABEL_HUB_")
]
hubs = [x.split("_")[-1] for x in hub_labels]
hubs = [x for x in hubs if x not in args.ignore]
Expand All @@ -39,6 +42,7 @@ def main(args):
continue
print(h)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Get hubs that need to be deployed from environment variables."
Expand All @@ -47,21 +51,18 @@ def main(args):
"--deployments",
"-d",
default="deployments",
help="The directory to search for deployments."
help="The directory to search for deployments.",
)
parser.add_argument(
"--ignore",
"-i",
nargs="*",
action="extend",
default=["template"],
help="Ignore one or more deployment targets."
help="Ignore one or more deployment targets.",
)
parser.add_argument(
"--only-deploy",
"-o",
nargs="*",
help="Only deploy the specified hubs."
"--only-deploy", "-o", nargs="*", help="Only deploy the specified hubs."
)
args = parser.parse_args()

Expand Down
2 changes: 1 addition & 1 deletion hub/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ apiVersion: v1
appVersion: '1.0'
description: Deployment Chart for JupyterHub
name: hub
version: 20240731-224556.git.8059.hf384f3d1
version: 20240731-224556.git.8607.hf7abb041
2 changes: 1 addition & 1 deletion images/node-placeholder-scaler/scaler/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


if __name__ == "__main__":
main()
main()
6 changes: 5 additions & 1 deletion images/node-placeholder-scaler/scaler/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

log = logging.getLogger(__name__)


def _event_repr(event):
"""
Simple repr of a calenar event
Expand All @@ -24,6 +25,7 @@ def _event_repr(event):
else:
return f"{event.summary} {event.start.strftime('%Y-%m-%d %H:%M %Z')} to {event.end.strftime('%Y-%m-%d %H:%M %Z')}"


def _get_cal_tz(calendar):
"""
Get the calendar timezone.
Expand All @@ -38,6 +40,7 @@ def _get_cal_tz(calendar):
else:
return zoneinfo.ZoneInfo("UTC")


def get_calendar(url: str):
"""
Get a calendar from local file or URL.
Expand All @@ -62,6 +65,7 @@ def get_calendar(url: str):
else:
logging.error(f"Unable to get calendar from resource: {url}")


def get_events(calendar, time=None):
"""
Get events from a calendar. If no time is passed, assume now.
Expand All @@ -80,6 +84,6 @@ def get_events(calendar, time=None):
# https://stackoverflow.com/questions/753052/strip-html-from-strings-in-python
events = [x for x in events_iter]
for ev in events:
ev.description = re.sub('<[^<]+?>', '', ev.description)
ev.description = re.sub("<[^<]+?>", "", ev.description)

return events
5 changes: 4 additions & 1 deletion images/node-placeholder-scaler/scaler/scaler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .calendar import get_calendar, get_events, _event_repr

from ruamel.yaml import YAML

yaml = YAML(typ="safe")


Expand Down Expand Up @@ -54,7 +55,9 @@ def get_replica_counts(events):
try:
pools_replica_config = yaml.load(ev.description)
except Exception as e:
logging.error(f"Caught unhandled exception parsing event description:\n{e}")
logging.error(
f"Caught unhandled exception parsing event description:\n{e}"
)
logging.error(f"Error in parsing description of {_event_repr(ev)}")
logging.error(f"{ev.description=}")
pass
Expand Down
2 changes: 1 addition & 1 deletion node-placeholder/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 20240731-224556.git.8187.he9c6025c
version: 20240731-224556.git.8610.hedc17750

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
Expand Down
2 changes: 1 addition & 1 deletion node-placeholder/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ image:
repository: us-central1-docker.pkg.dev/ucb-datahub-2018/core/node-placeholder-scaler
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "20240731-224556.git.8187.he9c6025c"
tag: "20240731-224556.git.8610.hedc17750"

imagePullSecrets: []
nameOverride: ""
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ chardet
niquests==3.7.2
urllib3<2.0.0
yamllint==1.35.1
pre-commit==4.0.1
86 changes: 50 additions & 36 deletions scripts/delete-unused-users.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Core functionality from @minrk:
https://discourse.jupyter.org/t/is-there-a-way-to-bulk-delete-old-users/20866/3
"""

import argparse
from datetime import timedelta, datetime
import json
Expand All @@ -36,8 +37,8 @@ def parse_timedelta(args):
https://docs.python.org/3/library/datetime.html#datetime.timedelta
"""
result = {}
for arg in args.split(','):
key, value = arg.split('=')
for arg in args.split(","):
key, value = arg.split("=")
try:
value = int(value)
except ValueError:
Expand All @@ -48,6 +49,7 @@ def parse_timedelta(args):
result[key] = value
return timedelta(**result)


def retrieve_users(hub_url, headers, inactive_since):
"""Returns generator of user models that should be deleted"""
url = hub_url.rstrip("/") + "/hub/api/users"
Expand All @@ -72,36 +74,44 @@ def retrieve_users(hub_url, headers, inactive_since):
"limit": next_page["limit"],
}


def should_delete(user, inactive_since):
"""
Returns a boolean if user is to be deleted. The critera are:
- was the user active in the past inactive_since period?
- is there a current user server running?
"""
last_activity_str = user.get('last_activity', False)
last_activity_str = user.get("last_activity", False)
if last_activity_str:
try:
last_activity = parse(user['last_activity'])
last_activity = parse(user["last_activity"])
except:
logger.error(f"Unexpected value for user['last_activity']: {user['last_activity']}")
logger.error(
f"Unexpected value for user['last_activity']: {user['last_activity']}"
)
raise
if isinstance(last_activity, datetime):
was_active_recently = datetime.now().astimezone() - last_activity < inactive_since
was_active_recently = (
datetime.now().astimezone() - last_activity < inactive_since
)
else:
logger.error(f"For user {user['name']}, expected datetime.datetime class for last_activity but got {type(last_activity)} instead.")
logger.error(
f"For user {user['name']}, expected datetime.datetime class for last_activity but got {type(last_activity)} instead."
)
raise

logger.debug(f"User: {user['name']}")
logger.debug(f"Last login: {last_activity}")
logger.debug(f"Recent activity: {was_active_recently}")
logger.debug(f"Running server: {user['server']}")
if was_active_recently or user['server'] is not None:
if was_active_recently or user["server"] is not None:
logger.info(f"Not flagging {user['name']} for deletion.")
return False
else:
logger.info(f"Flagging {user['name']} for deletion.")
return True


def delete_user(hub_url, headers, name):
"""Delete a given user by name via JupyterHub API"""
r = niquests.delete(
Expand All @@ -110,6 +120,7 @@ def delete_user(hub_url, headers, name):
)
r.raise_for_status()


def delete_users_from_hub(hub_url, token, inactive_since, dry_run=False):
"""Delete users from a provided hub url"""
headers = {
Expand All @@ -124,7 +135,7 @@ def delete_users_from_hub(hub_url, token, inactive_since, dry_run=False):
count = 1
for user in users:
if not dry_run:
delete_user(hub_url, headers, user['name'])
delete_user(hub_url, headers, user["name"])
logger.info(f"{count}: deleting {user['name']}")
else:
logger.info(f"Skipped {user['name']} due to dry run.")
Expand All @@ -134,19 +145,26 @@ def delete_users_from_hub(hub_url, token, inactive_since, dry_run=False):
if not dry_run:
print(f"Deleted {count} total users from the ORM for hub: {hub_url}")
else:
print(f"Dry run: Did not delete {count} total users from the ORM for hub: {hub_url}")
print(
f"Dry run: Did not delete {count} total users from the ORM for hub: {hub_url}"
)


def main(args):
"""
Get users from a hub, check to see if they should be deleted from the ORM
and if so, delete them!
"""
if args.credentials and args.hub_url:
logger.error("Please use only one of --hub_url or --credentials options when executing the script.")
logger.error(
"Please use only one of --hub_url or --credentials options when executing the script."
)
raise

if args.hub_url:
logger.info(f"Checking for and deleting ORM users on a single hub: {args.hub_url}")
logger.info(
f"Checking for and deleting ORM users on a single hub: {args.hub_url}"
)
token = os.environ["JUPYTERHUB_API_TOKEN"]

if not token:
Expand All @@ -169,48 +187,44 @@ def main(args):
print()

else:
logger.error("You must specify a single hub with the --hub_url argument, or a json file containing hubs and api keys with the --credentials argument.")
logger.error(
"You must specify a single hub with the --hub_url argument, or a json file containing hubs and api keys with the --credentials argument."
)
raise


if __name__ == "__main__":
argparser = argparse.ArgumentParser()
argparser.add_argument(
'-c',
'--credentials',
dest='credentials',
help='Path to a json file containing hub url and api keys. Format is: {"hub1_url": "hub1_key", "hub2_url":, "hub2_key"}'
"-c",
"--credentials",
dest="credentials",
help='Path to a json file containing hub url and api keys. Format is: {"hub1_url": "hub1_key", "hub2_url":, "hub2_key"}',
)
argparser.add_argument(
'-H',
'--hub_url',
help='Fully qualified URL to the JupyterHub. You must also set the JUPYTERHUB_API_TOKEN environment variable with the API key.'
"-H",
"--hub_url",
help="Fully qualified URL to the JupyterHub. You must also set the JUPYTERHUB_API_TOKEN environment variable with the API key.",
)
argparser.add_argument(
'--dry_run',
action='store_true',
help='Dry run without deleting users.'
"--dry_run", action="store_true", help="Dry run without deleting users."
)
argparser.add_argument(
'--inactive_since',
default='hours=24',
"--inactive_since",
default="hours=24",
type=parse_timedelta,
help='Period of inactivity after which users are considered for deletion (literal string constructor values for timedelta objects).'
help="Period of inactivity after which users are considered for deletion (literal string constructor values for timedelta objects).",
# https://docs.python.org/3/library/datetime.html#timedelta-objects
)
argparser.add_argument(
'-v',
'--verbose',
dest='verbose',
action='store_true',
help='Set info log level.'
"-v",
"--verbose",
dest="verbose",
action="store_true",
help="Set info log level.",
)
argparser.add_argument(
'-d',
'--debug',
dest='debug',
action='store_true',
help='Set debug log level.'
"-d", "--debug", dest="debug", action="store_true", help="Set debug log level."
)
args = argparser.parse_args()

Expand Down
Loading
Loading