Skip to content

Commit

Permalink
Merge pull request #822 from ministryofjustice/ag--show-future-tool-v…
Browse files Browse the repository at this point in the history
…ersion

Show tool version on tool Deploy/Upgrade
  • Loading branch information
xoen authored Jun 29, 2020
2 parents cef30cb + 0228d73 commit 54d6732
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 197 deletions.
81 changes: 36 additions & 45 deletions controlpanel/api/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
log = logging.getLogger(__name__)


TOOL_DEPLOYING = 'Deploying'
TOOL_DEPLOY_FAILED = 'Failed'
TOOL_IDLED = 'Idled'
TOOL_NOT_DEPLOYED = 'Not deployed'
TOOL_READY = 'Ready'
TOOL_RESTARTING = 'Restarting'
TOOL_UPGRADED = 'Upgraded'
TOOL_STATUS_UNKNOWN = 'Unknown'
TOOL_DEPLOYING = "Deploying"
TOOL_DEPLOY_FAILED = "Failed"
TOOL_IDLED = "Idled"
TOOL_NOT_DEPLOYED = "Not deployed"
TOOL_READY = "Ready"
TOOL_RESTARTING = "Restarting"
TOOL_UPGRADED = "Upgraded"
TOOL_STATUS_UNKNOWN = "Unknown"


class User:
Expand All @@ -31,21 +31,23 @@ class User:
A user is represented by an IAM role, which is assumed by their tools.
"""

def __init__(self, user):
self.user = user
self.k8s_namespace = f'user-{self.user.slug}'
self.k8s_namespace = f"user-{self.user.slug}"

@property
def iam_role_name(self):
return f'{settings.ENV}_user_{self.user.username.lower()}'
return f"{settings.ENV}_user_{self.user.username.lower()}"

def create(self):
aws.create_user_role(self.user)

helm.upgrade_release(
f"init-user-{self.user.slug}",
f"{settings.HELM_REPO}/init-user",
f"--set=" + (
f"--set="
+ (
f"Env={settings.ENV},"
f"NFSHostname={settings.NFS_HOSTNAME},"
f"OidcDomain={settings.OIDC_DOMAIN},"
Expand Down Expand Up @@ -107,8 +109,7 @@ def url(self):

repo_name = github_repository_name(self.app.repo_url)
ingresses = k8s.ExtensionsV1beta1Api.list_namespaced_ingress(
self.APPS_NS,
label_selector=f"repo={repo_name}",
self.APPS_NS, label_selector=f"repo={repo_name}",
).items

if len(ingresses) != 1:
Expand All @@ -119,6 +120,7 @@ def url(self):

class S3Bucket:
"""Wraps a S3Bucket model to provide convenience methods for AWS"""

def __init__(self, bucket):
self.bucket = bucket

Expand All @@ -141,6 +143,7 @@ class RoleGroup:
This is because IAM doesn't allow adding roles to IAM groups
See https://stackoverflow.com/a/48087433/455642
"""

def __init__(self, iam_managed_policy):
self.policy = iam_managed_policy

Expand All @@ -150,18 +153,16 @@ def arn(self):

@property
def path(self):
return f'/{settings.ENV}/group/'
return f"/{settings.ENV}/group/"

def create(self):
aws.create_group(
self.policy.name,
self.policy.path,
self.policy.name, self.policy.path,
)

def update_members(self):
aws.update_group_members(
self.arn,
{user.iam_role_name for user in self.policy.users.all()},
self.arn, {user.iam_role_name for user in self.policy.users.all()},
)

def delete(self):
Expand Down Expand Up @@ -194,7 +195,7 @@ def get_repositories(user):
org = github.get_organization(name)
repos.extend(org.get_repos())
except GithubException as err:
log.warning(f'Failed getting {name} Github org repos for {user}: {err}')
log.warning(f"Failed getting {name} Github org repos for {user}: {err}")
raise err
return repos

Expand All @@ -204,22 +205,21 @@ def get_repository(user, repo_name):
try:
return github.get_repo(repo_name)
except GithubException.UnknownObjectException:
log.warning(f'Failed getting {repo_name} Github repo for {user}: {err}')
log.warning(f"Failed getting {repo_name} Github repo for {user}: {err}")
return None


class ToolDeploymentError(Exception):
pass


class ToolDeployment():

class ToolDeployment:
def __init__(self, user, tool):
self.user = user
self.tool = tool

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

@property
def chart_name(self):
Expand Down Expand Up @@ -252,7 +252,6 @@ def _delete_legacy_release(self):
if old_release_name in helm.list_releases(old_release_name):
helm.delete(True, old_release_name)


def _set_values(self, **kwargs):
"""
Return the list of `--set KEY=VALUE` helm upgrade arguments
Expand All @@ -275,8 +274,8 @@ def _set_values(self, **kwargs):
values.update(kwargs)
set_values = []
for key, val in values.items():
escaped_val = val.replace(',', '\,')
set_values.extend(['--set', f'{key}={escaped_val}'])
escaped_val = val.replace(",", "\,")
set_values.extend(["--set", f"{key}={escaped_val}"])

return set_values

Expand All @@ -288,9 +287,11 @@ def install(self, **kwargs):

return helm.upgrade_release(
self.release_name,
f'{settings.HELM_REPO}/{self.chart_name}', # XXX assumes repo name
# f'--version', tool.version,
f'--namespace', self.k8s_namespace,
f"{settings.HELM_REPO}/{self.chart_name}", # XXX assumes repo name
f"--version",
self.tool.version,
f"--namespace",
self.k8s_namespace,
*set_values,
)

Expand All @@ -300,18 +301,13 @@ def install(self, **kwargs):
def uninstall(self, id_token):
deployment = self.get_deployment(id_token)
helm.delete(
deployment.metadata.name,
f"--namespace={self.k8s_namespace}",
deployment.metadata.name, f"--namespace={self.k8s_namespace}",
)

def restart(self, id_token):
k8s = KubernetesClient(id_token=id_token)
return k8s.AppsV1Api.delete_collection_namespaced_replica_set(
self.k8s_namespace,
label_selector=(
f"app={self.chart_name}"
# f'-{tool_deployment.tool.version}'
),
self.k8s_namespace, label_selector=(f"app={self.chart_name}"),
)

@classmethod
Expand Down Expand Up @@ -346,7 +342,6 @@ 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
Expand All @@ -362,7 +357,6 @@ def get_installed_chart_version(self, id_token):
except ObjectDoesNotExist:
return None


def get_status(self, id_token):
try:
deployment = self.get_deployment(id_token)
Expand All @@ -376,8 +370,7 @@ def get_status(self, id_token):
return TOOL_STATUS_UNKNOWN

conditions = {
condition.type: condition
for condition in deployment.status.conditions
condition.type: condition for condition in deployment.status.conditions
}

if "Available" in conditions:
Expand All @@ -386,14 +379,12 @@ def get_status(self, id_token):
return TOOL_IDLED
return TOOL_READY

if 'Progressing' in conditions:
progressing_status = conditions['Progressing'].status
if "Progressing" in conditions:
progressing_status = conditions["Progressing"].status
if progressing_status == "True":
return TOOL_DEPLOYING
elif progressing_status == "False":
return TOOL_DEPLOY_FAILED

log.warning(
f"Unknown status for {self!r}: {deployment.status.conditions}"
)
log.warning(f"Unknown status for {self!r}: {deployment.status.conditions}")
return TOOL_STATUS_UNKNOWN
83 changes: 49 additions & 34 deletions controlpanel/api/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,24 @@ class HelmError(APIException):


class Helm(object):

@classmethod
def execute(cls, *args, check=True, **kwargs):
should_wait = False
if 'timeout' in kwargs:
if "timeout" in kwargs:
should_wait = True
timeout = kwargs.pop('timeout')
timeout = kwargs.pop("timeout")

try:
log.debug(' '.join(['helm', *args]))
log.debug(" ".join(["helm", *args]))
env = os.environ.copy()
# helm checks for existence of DEBUG env var
if 'DEBUG' in env:
del env['DEBUG']
if "DEBUG" in env:
del env["DEBUG"]
proc = subprocess.Popen(
["helm", *args],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
encoding='utf8',
encoding="utf8",
env=env,
**kwargs,
)
Expand Down Expand Up @@ -92,63 +91,61 @@ def parse_upgrade_output(output):
section = None
columns = None
last_deployed = None
namespace = ''
namespace = ""
resource_type = None
resources = {}
notes = []

for line in output.split('\n'):
for line in output.split("\n"):

if line.startswith('LAST DEPLOYED:'):
if line.startswith("LAST DEPLOYED:"):
last_deployed = datetime.strptime(
line.split(':', 1)[1],
' %a %b %d %H:%M:%S %Y',
line.split(":", 1)[1], " %a %b %d %H:%M:%S %Y",
)
continue

if line.startswith('NAMESPACE:'):
namespace = line.split(':', 1)[1].strip()
if line.startswith("NAMESPACE:"):
namespace = line.split(":", 1)[1].strip()
continue

if line.startswith('==> ') and section == 'RESOURCES':
resource_type = line.split(' ', 1)[1].strip()
if line.startswith("==> ") and section == "RESOURCES":
resource_type = line.split(" ", 1)[1].strip()
continue

if line.startswith('RESOURCES:'):
section = 'RESOURCES'
if line.startswith("RESOURCES:"):
section = "RESOURCES"
continue

if line.startswith('NAME') and resource_type:
if line.startswith("NAME") and resource_type:
columns = line.lower()
columns = re.split(r'\s+', columns)
columns = re.split(r"\s+", columns)
continue

if section == 'NOTES':
if section == "NOTES":
notes.append(line)
continue

if line.startswith('NOTES:'):
section = 'NOTES'
if line.startswith("NOTES:"):
section = "NOTES"
continue

if section and line.strip():
row = re.split(r'\s+', line)
row = re.split(r"\s+", line)
row = dict(zip(columns, row))
resources[resource_type] = [
*resources.get(resource_type, []),
*[row],
]

return {
'last_deployed': last_deployed,
'namespace': namespace,
'resources': resources,
'notes': '\n'.join(notes),
"last_deployed": last_deployed,
"namespace": namespace,
"resources": resources,
"notes": "\n".join(notes),
}


class Chart(object):

def __init__(self, name, description, version, app_version):
self.name = name
self.description = description
Expand All @@ -162,10 +159,7 @@ class HelmRepository(object):

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

_updated_at = None
Expand All @@ -186,7 +180,9 @@ def _load(cls):
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}'"
wrapped_err.detail = (
f"Error while opening/parsing helm repository cache: '{cls.REPO_PATH}'"
)
raise HelmError(wrapped_err)

@classmethod
Expand Down Expand Up @@ -231,6 +227,25 @@ def get_chart_info(cls, name):
chart_info[chart.version] = chart
return chart_info

@classmethod
def get_chart_app_version(cls, name, version):
"""
Returns the "appVersion" metadata for the given
chart name/version.
It returns None if the chart or the chart version
are not found or if that version of a chart doesn't
have the "appVersion" field (e.g. the chart
preceed the introduction of this field)
"""

chart_info = cls.get_chart_info(name)
version_info = chart_info.get(version, None)
if version_info:
return version_info.app_version

return None

@classmethod
def _outdated(cls):
# helm update never called?
Expand Down
Loading

0 comments on commit 54d6732

Please sign in to comment.