Skip to content

Commit

Permalink
Merge pull request #812 from ministryofjustice/ag--tool-show-version
Browse files Browse the repository at this point in the history
Show deployed tools versions
  • Loading branch information
xoen authored Jun 11, 2020
2 parents 89cbb4c + 3bc69aa commit 37a8428
Show file tree
Hide file tree
Showing 13 changed files with 612 additions and 44 deletions.
18 changes: 18 additions & 0 deletions controlpanel/api/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TOOL_IDLED = 'Idled'
TOOL_NOT_DEPLOYED = 'Not deployed'
TOOL_READY = 'Ready'
TOOL_RESTARTING = 'Restarting'
TOOL_UPGRADED = 'Upgraded'
TOOL_STATUS_UNKNOWN = 'Unknown'

Expand Down Expand Up @@ -345,6 +346,23 @@ def get_deployment(self, id_token):

return deployments[0]


def get_installed_chart_version(self, id_token):
"""
Returns the installed helm chart version of the tool
This is extracted from the `chart` label in the corresponding
`Deployment`.
"""

try:
deployment = self.get_deployment(id_token)
_, chart_version = deployment.metadata.labels["chart"].rsplit("-", 1)
return chart_version
except ObjectDoesNotExist:
return None


def get_status(self, id_token):
try:
deployment = self.get_deployment(id_token)
Expand Down
117 changes: 108 additions & 9 deletions controlpanel/api/helm.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from datetime import datetime
from datetime import datetime, timedelta
import logging
import os
import re
import subprocess

from django.conf import settings
from rest_framework.exceptions import APIException
import yaml


log = logging.getLogger(__name__)
Expand All @@ -19,7 +20,8 @@ class HelmError(APIException):

class Helm(object):

def _execute(self, *args, check=True, **kwargs):
@classmethod
def execute(cls, *args, check=True, **kwargs):
should_wait = False
if 'timeout' in kwargs:
should_wait = True
Expand Down Expand Up @@ -67,25 +69,22 @@ def _execute(self, *args, check=True, **kwargs):

return proc

def update_repositories(self, *args):
self._execute("repo", "update", timeout=None)

def upgrade_release(self, release, chart, *args):
self.update_repositories()
HelmRepository.update()

return self._execute(
return self.__class__.execute(
"upgrade", "--install", "--wait", release, chart, *args,
)

def delete(self, purge=True, *args):
default_args = []
if purge:
default_args.append("--purge")
self._execute("delete", *default_args, *args)
self.__class__.execute("delete", *default_args, *args)

def list_releases(self, *args):
# TODO - use --max and --offset to paginate through releases
proc = self._execute("list", "-q", "--max=1024", *args, timeout=None)
proc = self.__class__.execute("list", "-q", "--max=1024", *args, timeout=None)
return proc.stdout.read().split()


Expand Down Expand Up @@ -148,4 +147,104 @@ def parse_upgrade_output(output):
}


class Chart(object):

def __init__(self, name, description, version, app_version):
self.name = name
self.description = description
self.version = version
self.app_version = app_version


class HelmRepository(object):

CACHE_FOR_MINUTES = 30

HELM_HOME = Helm.execute("home").stdout.read().strip()
REPO_PATH = os.path.join(
HELM_HOME,
"repository",
"cache",
f"{settings.HELM_REPO}-index.yaml",
)

_updated_at = None
_repository = {}

@classmethod
def update(cls, force=True):
if force or cls._outdated():
Helm.execute("repo", "update", timeout=None)
cls._load()
cls._updated_at = datetime.utcnow()

@classmethod
def _load(cls):
# Read and parse helm repository YAML file
try:
with open(cls.REPO_PATH) as f:
cls._repository = yaml.load(f, Loader=yaml.FullLoader)
except Exception as err:
wrapped_err = HelmError(err)
wrapped_err.detail = f"Error while opening/parsing helm repository cache: '{cls.REPO_PATH}'"
raise HelmError(wrapped_err)

@classmethod
def get_chart_info(cls, name):
"""
Get information about the given chart
Returns a dictionary with the chart versions as keys and the chart
as value (`Chart` instance)
Returns an empty dictionary when the chart is not in the helm
repository index.
```
rstudio_info = HelmRepository.get_chart_info("rstudio")
# rstudio_info = {
# "2.2.5": <Chart name="rstudio" version="2.2.5" app_version=""RStudio: 1.2.13...">,
# "2.2.4": <Chart ...>,
# }
```
"""

cls.update(force=False)

try:
versions = cls._repository["entries"][name]
except KeyError:
# No such a chart with this name, returning {}
return {}

# Convert to dictionary
chart_info = {}
for version_info in versions:
chart = Chart(
version_info["name"],
version_info["description"],
version_info["version"],
# appVersion is relatively new and some old helm chart don't
# have it
version_info.get("appVersion", None),
)
chart_info[chart.version] = chart
return chart_info

@classmethod
def _outdated(cls):
# helm update never called?
if not cls._updated_at:
return True

# helm update called more than `CACHE_FOR_MINUTES` ago
now = datetime.utcnow()
elapsed = now - cls._updated_at
if elapsed > timedelta(minutes=cls.CACHE_FOR_MINUTES):
return True

# helm update called recently
return False


helm = Helm()
59 changes: 53 additions & 6 deletions controlpanel/api/models/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django_extensions.db.models import TimeStampedModel

from controlpanel.api import cluster
from controlpanel.api.helm import HelmRepository


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -49,14 +50,12 @@ def create(self, *args, **kwargs):
return tool_deployment

def filter(self, **kwargs):
deployed_versions = {}
user = kwargs["user"]
id_token = kwargs["id_token"]
filter = Q(chart_name=None) # Always False
deployments = cluster.ToolDeployment.get_deployments(user, id_token)
for deployment in deployments:
chart_name, version = deployment.metadata.labels["chart"].rsplit("-", 1)
deployed_versions[chart_name] = version
filter = filter | (
Q(chart_name=chart_name)
# & Q(version=version)
Expand All @@ -65,8 +64,7 @@ def filter(self, **kwargs):
tools = Tool.objects.filter(filter)
results = []
for tool in tools:
outdated = tool.version != deployed_versions[tool.chart_name]
tool_deployment = ToolDeployment(tool, user, outdated)
tool_deployment = ToolDeployment(tool, user)
results.append(tool_deployment)
return results

Expand All @@ -82,15 +80,64 @@ class ToolDeployment:

objects = ToolDeploymentManager()

def __init__(self, tool, user, outdated=False):
def __init__(self, tool, user):
self._subprocess = None
self.tool = tool
self.user = user
self.outdated = outdated

def __repr__(self):
return f'<ToolDeployment: {self.tool!r} {self.user!r}>'

def get_installed_app_version(self, id_token):
"""
Returns the version of the deployed tool
NOTE: This is the version coming from the helm
chart `appVersion` field, **not** the version
of the chart released in the user namespace.
e.g. if user has `rstudio-2.2.5` (chart version)
installed in his namespace, this would return
"RStudio: 1.2.1335+conda, R: 3.5.1, Python: 3.7.1, patch: 10"
**not** "2.2.5".
Also bear in mind that Helm added this `appVersion`
field only "recently" so if a user has an old
version of a tool chart installed this would return
`None` as we can't determine the tool version
as this information is simply not available
in the helm repository index.
"""

td = cluster.ToolDeployment(self.user, self.tool)
chart_version = td.get_installed_chart_version(id_token)
if chart_version:
chart_info = HelmRepository.get_chart_info(self.tool.chart_name)

version_info = chart_info.get(chart_version, None)
if version_info:
return version_info.app_version

return None


def outdated(self, id_token):
"""
Returns true if the tool helm chart version is old
NOTE: This is simple/naive at the moment and it returns true if
the installed chart for the tool has a different version
than the one in the corresponding Tool record.
"""

td = cluster.ToolDeployment(self.user, self.tool)
chart_version = td.get_installed_chart_version(id_token)

if chart_version:
return self.tool.version != chart_version

return False

def delete(self, id_token):
"""
Remove the release from the cluster
Expand Down
Loading

0 comments on commit 37a8428

Please sign in to comment.