Skip to content

Commit

Permalink
validate: check image existence and uid/gid
Browse files Browse the repository at this point in the history
- Checks if the environment image exists on dockerhub
- Checks if the UID/GID on the environment image match the ones in REANA config.

closes reanahub#457
closes reanahub#382
  • Loading branch information
mvidalgarcia committed Feb 25, 2021
1 parent 59dd3d7 commit 6a602c2
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 4 deletions.
4 changes: 4 additions & 0 deletions reana_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@

ENVIRONMENT_IMAGE_SUSPECTED_TAGS_VALIDATOR = ["latest", "master", ""]
"""Warns user if above environment image tags are used."""


DOCKER_REGISTRY_INDEX_URL = "https://index.docker.io/v1/repositories/{image}/tags/{tag}"
"""Docker Hub registry index URL."""
153 changes: 149 additions & 4 deletions reana_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# under the terms of the MIT License; see LICENSE file for more details.
"""REANA client utils."""
import base64
import datetime
import json
import logging
import os
Expand All @@ -16,16 +17,22 @@
from uuid import UUID

import click
import requests
import yadageschemas
import yaml
from jsonschema import ValidationError, validate
from reana_client.config import ENVIRONMENT_IMAGE_SUSPECTED_TAGS_VALIDATOR
from reana_commons.config import WORKFLOW_RUNTIME_USER_GID, WORKFLOW_RUNTIME_USER_UID
from reana_commons.errors import REANAValidationError
from reana_commons.operational_options import validate_operational_options
from reana_commons.serial import serial_load
from reana_commons.utils import get_workflow_status_change_verb

from reana_client.config import reana_yaml_schema_file_path, reana_yaml_valid_file_names
from reana_client.config import (
reana_yaml_schema_file_path,
reana_yaml_valid_file_names,
DOCKER_REGISTRY_INDEX_URL,
ENVIRONMENT_IMAGE_SUSPECTED_TAGS_VALIDATOR,
)


def workflow_uuid_or_name(ctx, param, value):
Expand Down Expand Up @@ -207,12 +214,16 @@ def _validate_serial_workflow_environment(workflow_steps):
:raises Warning: Warns user if the workflow environment is invalid in serial workflow steps.
"""
for step in workflow_steps:
_validate_image_tag(step["environment"])
image = step["environment"]
image_name, image_tag = _validate_image_tag(image)
_image_exists(image_name, image_tag)
uid, gids = _get_image_uid_gids(image_name, image_tag)
_validate_uid_gids(uid, gids, kubernetes_uid=step.get("kubernetes_uid"))


def _validate_image_tag(image):
"""Validate if image tag is valid."""

image_name, image_tag = "", ""
has_warnings = False
if ":" in image:
environment = image.split(":", 1)
Expand Down Expand Up @@ -242,13 +253,118 @@ def _validate_image_tag(image):
fg="yellow",
)
has_warnings = True
image_name = image
if not has_warnings:
click.echo(
click.style(
"==> Environment image {} has correct format.".format(image),
fg="green",
)
)
return image_name, image_tag


def _image_exists(image, tag):
"""Verify if image exists."""

docker_registry_url = DOCKER_REGISTRY_INDEX_URL.format(image=image, tag=tag)
# Remove traling slash if no tag was specified
if not tag:
docker_registry_url = docker_registry_url[:-1]
try:
response = requests.get(docker_registry_url)
except requests.exceptions.RequestException as e:
logging.error(traceback.format_exc())
click.secho(
"==> ERROR: Something went wrong when querying {}".format(
docker_registry_url
),
err=True,
fg="red",
)
raise e

if not response.ok:
if response.status_code == 404:
msg = response.text
click.secho(
"==> ERROR: Environment image {}{} does not exist: {}".format(
image, ":{}".format(tag) if tag else "", msg
),
err=True,
fg="red",
)
else:
click.secho(
"==> ERROR: Existence of environment image {}{} could not be verified. Status code: {} {}".format(
image,
":{}".format(tag) if tag else "",
response.status_code,
response.reason,
),
err=True,
fg="red",
)
sys.exit(1)
else:
click.secho(
"==> Environment image {}{} exists.".format(
image, ":{}".format(tag) if tag else ""
),
fg="green",
)


def _get_image_uid_gids(image, tag):
"""Obtain environment image UID and GIDs.
:returns: A tuple with UID and GIDs.
"""
# Check if docker is installed.
run_command("docker version", display=False, return_output=True)
# Run ``id``` command inside the container.
uid_gid_output = run_command(
'docker run -i -t --rm {}{} bash -c "/usr/bin/id -u && /usr/bin/id -G"'.format(
image, ":{}".format(tag) if tag else ""
),
display=False,
return_output=True,
)
ids = uid_gid_output.splitlines()
uid, gids = (
int(ids[0]),
[int(gid) for gid in ids[1].split()],
)
return uid, gids


def _validate_uid_gids(uid, gids, kubernetes_uid=None):
"""Check whether container UID and GIDs are valid."""
if WORKFLOW_RUNTIME_USER_GID not in gids:
click.secho(
"==> ERROR: Environment image GID must be {}. GIDs {} were found.".format(
WORKFLOW_RUNTIME_USER_GID, gids
),
err=True,
fg="red",
)
sys.exit(1)
if kubernetes_uid is not None:
if kubernetes_uid != uid:
click.secho(
"==> WARNING: `kubernetes_uid` set to {}. UID {} was found.".format(
kubernetes_uid, uid
),
fg="yellow",
)
elif uid != WORKFLOW_RUNTIME_USER_UID:
click.secho(
"==> WARNING: Environment image UID is recommended to be {}. UID {} was found.".format(
WORKFLOW_RUNTIME_USER_UID, uid
),
err=True,
fg="yellow",
)


def _validate_reana_yaml(reana_yaml):
Expand Down Expand Up @@ -467,3 +583,32 @@ def get_reana_yaml_file_path():
return path
# If none of the valid paths exists, fall back to reana.yaml.
return "reana.yaml"


def run_command(cmd, display=True, return_output=False):
"""Run given command on shell in the current directory.
Exit in case of troubles.
:param cmd: shell command to run
:param display: should we display command to run?
:param return_output: shall the output of the command be returned?
:type cmd: str
:type display: bool
:type return_output: bool
"""
now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
if display:
click.secho("[{0}] ".format(now), bold=True, nl=False, fg="green")
click.secho("{0}".format(cmd), bold=True)
try:
if return_output:
result = subprocess.check_output(cmd, shell=True)
return result.decode().rstrip("\r\n")
else:
subprocess.check_call(cmd, shell=True)
except subprocess.CalledProcessError as err:
if display:
click.secho("[{0}] ".format(now), bold=True, nl=False, fg="green")
click.secho("{0}".format(err), bold=True, fg="red")
sys.exit(err.returncode)

0 comments on commit 6a602c2

Please sign in to comment.