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

feat: automatically remove user registry secrets #435

Merged
merged 49 commits into from
Oct 26, 2020
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
073036e
add cronjob that periodically deletes users image pull secrets
olevski Oct 16, 2020
ea13d9c
test log warnings
olevski Oct 16, 2020
d1848d1
set logging level python
olevski Oct 16, 2020
8982dd8
edit to find pod name by servername using annotations
olevski Oct 16, 2020
91343b2
add serveiceaccountname to cronjob
olevski Oct 16, 2020
e85ac5e
fix cronjob spec formatting
olevski Oct 16, 2020
dfd906a
fix up cronjob code
olevski Oct 16, 2020
eb617b5
cleanup cronjob python code
olevski Oct 16, 2020
3cb9b94
add message that cronjob started looking for secrets
olevski Oct 16, 2020
e72c068
add tests, edit python code
olevski Oct 17, 2020
f32ea82
edit cronjob
olevski Oct 17, 2020
ce98a13
escape underscores in server name
olevski Oct 18, 2020
7a84dba
edit code to work when escpaing servername that is None
olevski Oct 18, 2020
2ddc3fa
Merge branch 'master' into automatically-remove-user-registry-secrets
olevski Oct 18, 2020
2ebddaf
adjust tests
olevski Oct 20, 2020
7626eb1
edit chartpress for image for cleaning registry secrets
olevski Oct 20, 2020
58e7d98
edit values chart
olevski Oct 20, 2020
29fb72f
edit cronjob spec
olevski Oct 20, 2020
f01562a
move code to separate folder
olevski Oct 20, 2020
5b2527a
remove sha from version that was committed in by accident
olevski Oct 20, 2020
eed1f33
add secret and pod labels
olevski Oct 20, 2020
bc084bf
find user pod by labels
olevski Oct 20, 2020
4c1e3d6
edits
olevski Oct 20, 2020
622ca8f
remove try/except for k8s api
olevski Oct 20, 2020
e0c1969
remove unnecessary warning
olevski Oct 20, 2020
a8f46d1
edit secret name
olevski Oct 22, 2020
5f104a8
put back label annotations
olevski Oct 22, 2020
f4952a7
fix tests
olevski Oct 22, 2020
b950a15
keep black format check happy
olevski Oct 22, 2020
14e3431
fix create registry secret
olevski Oct 23, 2020
fb8c100
update imports of function that changed name
olevski Oct 23, 2020
a30bae7
fix black formatting, raise exception on multiple pods that match a s…
olevski Oct 23, 2020
08f6c80
remove unneeded command from dockerfile
olevski Oct 23, 2020
572b728
pin kubernetes and escapism version in pipfile for cleaning secrets
olevski Oct 23, 2020
03a2f3a
upadate pipfile lock after updating pipfile
olevski Oct 23, 2020
cd175f2
update folder and image name
olevski Oct 23, 2020
3185a3f
fix black formatting
olevski Oct 23, 2020
ea577de
address comments
olevski Oct 23, 2020
8d88fb3
black formatting
olevski Oct 23, 2020
10af254
rename secret age threshold
olevski Oct 23, 2020
d16ef43
set max secret age to 5 mins
olevski Oct 23, 2020
4c61a23
rolled back explicit pinning in pipfile
olevski Oct 23, 2020
4ca5e3c
cleanup tests
olevski Oct 23, 2020
e0681d5
fix bug with label selector in secrets
olevski Oct 23, 2020
84e750f
clean up tests
olevski Oct 23, 2020
af82128
fix year in copyright message
olevski Oct 23, 2020
5c8acd3
edit helm chart to include schedule and max secret age
olevski Oct 23, 2020
2b334b7
clarify helm values file
olevski Oct 23, 2020
9400c21
remove dashes
olevski Oct 23, 2020
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
7 changes: 7 additions & 0 deletions chartpress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ charts:
- .
- jupyterhub
- singleuser
- cull_secrets
images:
renku-notebooks:
contextPath: .
Expand All @@ -26,3 +27,9 @@ charts:
valuesPath: git_clone.image
paths:
- git-clone
cull_secrets:
contextPath: cull_secrets
dockerfilePath: cull_secrets/Dockerfile
valuesPath: cull_secrets.image
paths:
- cull_secrets
16 changes: 16 additions & 0 deletions cull_secrets/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.7-alpine

LABEL maintainer="[email protected]"

RUN apk add --no-cache curl build-base libffi-dev openssl-dev && \
pip install --no-cache-dir --disable-pip-version-check -U pip && \
pip install --no-cache-dir --disable-pip-version-check pipenv

# Install all packages
COPY Pipfile Pipfile.lock /cull_secrets/
WORKDIR /cull_secrets
RUN pipenv install --system --deploy

COPY clean_user_registry_secrets.py /cull_secrets/

CMD ["python", "clean_user_registry_secrets.py"]
15 changes: 15 additions & 0 deletions cull_secrets/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
escapism = "*"
kubernetes = "*"

[dev-packages]

[requires]

[pipenv]
allow_prereleases = true
188 changes: 188 additions & 0 deletions cull_secrets/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

156 changes: 156 additions & 0 deletions cull_secrets/clean_user_registry_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
#
# Copyright 2019 - Swiss Data Science Center (SDSC)
olevski marked this conversation as resolved.
Show resolved Hide resolved
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
# Eidgenössische Technische Hochschule Zürich (ETHZ).
#
# 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.
"""Scripts used to remove user registry secrets in k8s"""

import argparse
from datetime import datetime, timedelta
import logging
from pathlib import Path
import re

from kubernetes import client
from kubernetes.config.incluster_config import (
SERVICE_CERT_FILENAME,
SERVICE_TOKEN_FILENAME,
InClusterConfigLoader,
)


def find_pod_by_secret(secret, k8s_client):
"""Find the user jupyterhub podname based on the registry pull secret."""
label_keys = ["renku.io/commit-sha", "renku.io/projectName", "renku.io/username"]
label_selector = []
for label_key in label_keys:
label_selector.append(f"{label_key}={secret.metadata.labels[label_key]}")
label_selector = ",".join(label_selector)

pod_list = k8s_client.list_namespaced_pod(
secret.metadata.namespace,
label_selector=label_selector,
)
if len(pod_list.items) > 1:
raise Exception(
"There should at most one pod that matches a secret, "
f"found {len(pod_list.items)} that match the secret {secret.metadata.name}"
)
elif len(pod_list.items) == 1:
return pod_list.items[0].metadata.name
return None


def remove_user_registry_secret(namespace, k8s_client, max_secret_age_hrs=0.25):
"""Used in a cronjob to periodically remove old user registry secrets"""
secret_name_regex = ".+-registry-[a-z0-9-]{36}$"
label_keys = ["renku.io/commit-sha", "renku.io/projectName", "renku.io/username"]
logging.info(
f"Checking for user registry secrets whose "
f"names match the regex: {secret_name_regex}"
)
secret_list = k8s_client.list_namespaced_secret(
namespace, label_selector={"component=singleuser-server"}
)
max_secret_age = timedelta(hours=max_secret_age_hrs)
for secret in secret_list.items:
# loop through secrets and find ones that match the predefined regex
secret_name = secret.metadata.name
secret_name_match = re.match(secret_name_regex, secret_name)
# calculate secret age
tz = secret.metadata.creation_timestamp.tzinfo
secret_age = datetime.now(tz=tz) - secret.metadata.creation_timestamp
if (
secret_name_match is not None
and secret.type == "kubernetes.io/dockerconfigjson"
and all(
[ # check that label keys for sha, project and username are present
label_key in secret.metadata.labels.keys()
for label_key in label_keys
]
)
):
podname = find_pod_by_secret(secret, k8s_client)
if podname is None:
# pod does not exist, delete if secret is old enough
if secret_age > max_secret_age:
logging.info(
f"User pod that used secret {secret_name} does not exist, "
f"deleting secret as it is older "
f"than the {max_secret_age_hrs} hours threshold"
)
k8s_client.delete_namespaced_secret(secret_name, namespace)
else:
# check if the pod has the expected annotations and is running or succeeded
# no need to check for secret age because we are certain secret has been used
pod = k8s_client.read_namespaced_pod(podname, namespace)
if (
pod.metadata.labels.get("app") == "jupyterhub"
and pod.metadata.labels.get("component") == "singleuser-server"
and pod.status.phase in ["Running", "Succeeded"]
):
logging.info(
f"Found user pod {podname} that used the secret, "
f"deleting secret {secret_name}."
)
k8s_client.delete_namespaced_secret(secret_name, namespace)


def float_gt_zero(number):
if float(number) <= 0:
raise argparse.ArgumentTypeError(
f"{number} should be a float and greater than zero."
)
else:
return float(number)


def main():
# set logging level
logging.basicConfig(level=logging.INFO)

# check arguments
parser = argparse.ArgumentParser(description="Clean up user registry secrets.")
parser.add_argument(
"-n",
"--namespace",
type=str,
required=True,
help="K8s namespace where the user pods and registry secrets are located.",
)
parser.add_argument(
"-a",
"--age-hours-minimum",
type=float_gt_zero,
default=0.25,
help="The maximum age allowed for a registry secret to have before it is removed"
"if the user Jupyterhub pod cannot be found.",
)
args = parser.parse_args()

# initialize k8s client
token_filename = Path(SERVICE_TOKEN_FILENAME)
cert_filename = Path(SERVICE_CERT_FILENAME)
InClusterConfigLoader(
token_filename=token_filename, cert_filename=cert_filename
).load_and_set()
k8s_client = client.CoreV1Api()

# remove user registry secret
remove_user_registry_secret(args.namespace, k8s_client, args.age_hours_minimum)


if __name__ == "__main__":
main()
Loading