From 5945d7fca095531b3601e551c527457f9413643c Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Tue, 25 Jun 2024 18:19:31 +0200 Subject: [PATCH 1/8] fix(set_workflow_status): validate endpoint arguments (#589) Closes reanahub/reana-client#718 --- docs/openapi.json | 19 ++++- reana_workflow_controller/rest/utils.py | 23 +++--- .../rest/workflows_status.py | 80 ++++++++++++------- tests/test_views.py | 49 +++++++++++- 4 files changed, 126 insertions(+), 45 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 506ab87b..e082d75d 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1116,19 +1116,30 @@ "type": "string" }, { - "description": "Optional. Additional input parameters and operational options for workflow execution. Possible parameters are `CACHE=on/off`, passed to disable caching of results in serial workflows, `all_runs=True/False` deletes all runs of a given workflow if status is set to deleted and `workspace=True/False` which deletes the workspace of a workflow.", + "description": "Optional. Additional parameters to customise the workflow status change.", "in": "body", "name": "parameters", "required": false, "schema": { "properties": { - "CACHE": { - "type": "string" - }, "all_runs": { + "description": "Optional. If true, delete all runs of the workflow. Only allowed when status is `deleted`.", + "type": "boolean" + }, + "input_parameters": { + "description": "Optional. Additional input parameters that override the ones from the workflow specification. Only allowed when status is `start`.", + "type": "object" + }, + "operational_options": { + "description": "Optional. Additional operational options for workflow execution. Only allowed when status is `start`.", + "type": "object" + }, + "restart": { + "description": "Optional. If true, the workflow is a restart of an earlier workflow execution. Only allowed when status is `start`.", "type": "boolean" }, "workspace": { + "description": "Optional, but must be set to true if provided. If true, delete also the workspace of the workflow. Only allowed when status is `deleted`.", "type": "boolean" } }, diff --git a/reana_workflow_controller/rest/utils.py b/reana_workflow_controller/rest/utils.py index 235e514b..ee67c167 100644 --- a/reana_workflow_controller/rest/utils.py +++ b/reana_workflow_controller/rest/utils.py @@ -79,9 +79,8 @@ def start_workflow(workflow, parameters): def _start_workflow_db(workflow, parameters): workflow.status = RunStatus.pending - if parameters: - workflow.input_parameters = parameters.get("input_parameters") - workflow.operational_options = parameters.get("operational_options") + workflow.input_parameters = parameters.get("input_parameters", {}) + workflow.operational_options = parameters.get("operational_options", {}) current_db_sessions.add(workflow) current_db_sessions.commit() @@ -95,15 +94,15 @@ def _start_workflow_db(workflow, parameters): verb=get_workflow_status_change_verb(workflow.status.name), status=str(workflow.status.name), ) - if "restart" in parameters.keys(): - if parameters["restart"]: - if workflow.status not in [ - RunStatus.failed, - RunStatus.finished, - RunStatus.queued, - RunStatus.pending, - ]: - raise REANAWorkflowControllerError(failure_message) + + if parameters.get("restart"): + if workflow.status not in [ + RunStatus.failed, + RunStatus.finished, + RunStatus.queued, + RunStatus.pending, + ]: + raise REANAWorkflowControllerError(failure_message) elif workflow.status not in [RunStatus.created, RunStatus.queued]: if workflow.status == RunStatus.deleted: raise REANAWorkflowStatusError(failure_message) diff --git a/reana_workflow_controller/rest/workflows_status.py b/reana_workflow_controller/rest/workflows_status.py index 72c0b3b1..0d3c2548 100644 --- a/reana_workflow_controller/rest/workflows_status.py +++ b/reana_workflow_controller/rest/workflows_status.py @@ -11,6 +11,9 @@ import json from flask import Blueprint, jsonify, request +from webargs import fields +from webargs.flaskparser import use_kwargs + from reana_commons.config import WORKFLOW_TIME_FORMAT from reana_commons.errors import REANASecretDoesNotExist @@ -314,7 +317,28 @@ def get_workflow_status(workflow_id_or_name): # noqa @blueprint.route("/workflows//status", methods=["PUT"]) -def set_workflow_status(workflow_id_or_name): # noqa +@use_kwargs( + { + # parameters for "start" + "input_parameters": fields.Dict(), + "operational_options": fields.Dict(), + "restart": fields.Boolean(), + # parameters for "deleted" + "all_runs": fields.Boolean(), + "workspace": fields.Boolean(), + }, + location="json", +) +@use_kwargs( + { + "user": fields.Str(required=True), + "status": fields.Str(required=True), + }, + location="query", +) +def set_workflow_status( + workflow_id_or_name: str, user: str, status: str, **parameters: dict +): # noqa r"""Set workflow status. --- @@ -348,21 +372,36 @@ def set_workflow_status(workflow_id_or_name): # noqa - name: parameters in: body description: >- - Optional. Additional input parameters and operational options for - workflow execution. Possible parameters are `CACHE=on/off`, passed - to disable caching of results in serial workflows, - `all_runs=True/False` deletes all runs of a given workflow - if status is set to deleted and `workspace=True/False` which deletes - the workspace of a workflow. + Optional. Additional parameters to customise the workflow status change. required: false schema: type: object properties: - CACHE: - type: string + operational_options: + description: >- + Optional. Additional operational options for workflow execution. + Only allowed when status is `start`. + type: object + input_parameters: + description: >- + Optional. Additional input parameters that override the ones + from the workflow specification. Only allowed when status is `start`. + type: object + restart: + description: >- + Optional. If true, the workflow is a restart of an earlier workflow execution. + Only allowed when status is `start`. + type: boolean all_runs: + description: >- + Optional. If true, delete all runs of the workflow. + Only allowed when status is `deleted`. type: boolean workspace: + description: >- + Optional, but must be set to true if provided. + If true, delete also the workspace of the workflow. + Only allowed when status is `deleted`. type: boolean responses: 200: @@ -456,24 +495,11 @@ def set_workflow_status(workflow_id_or_name): # noqa """ try: - user_uuid = request.args["user"] - workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid) - status = request.args.get("status") + workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user) if not (status in STATUSES): - return ( - jsonify( - { - "message": "Status {0} is not one of: {1}".format( - status, ", ".join(STATUSES) - ) - } - ), - 400, - ) + error_msg = f"Status {status} is not one of: {', '.join(STATUSES)}" + return jsonify({"message": error_msg}), 400 - parameters = {} - if request.is_json: - parameters = request.json if status == START: start_workflow(workflow, parameters) return ( @@ -489,8 +515,8 @@ def set_workflow_status(workflow_id_or_name): # noqa 200, ) elif status == DELETED: - all_runs = True if request.json.get("all_runs") else False - workspace = True if request.json.get("workspace", True) else False + all_runs = parameters.get("all_runs", False) + workspace = parameters.get("workspace", True) if not workspace: return ( jsonify( diff --git a/tests/test_views.py b/tests/test_views.py index 97d80e3d..4ee902ce 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -923,9 +923,9 @@ def test_set_workflow_status_unknown_workflow( url_for( "statuses.set_workflow_status", workflow_id_or_name=random_workflow_uuid ), - query_string={"user": default_user.id_}, + query_string={"user": default_user.id_, "status": payload}, content_type="application/json", - data=json.dumps(payload), + data=json.dumps({}), ) assert res.status_code == 404 @@ -1151,6 +1151,51 @@ def test_start_input_parameters( assert workflow.input_parameters == parameters["input_parameters"] +def test_start_no_input_parameters( + app, + session, + default_user, + user_secrets, + corev1_api_client_with_user_secrets, + sample_serial_workflow_in_db, +): + """Test start workflow with inupt parameters.""" + workflow = sample_serial_workflow_in_db + workflow_uuid = str(sample_serial_workflow_in_db.id_) + + with app.test_client() as client: + # create workflow + workflow.status = RunStatus.created + session.add(workflow) + session.commit() + + payload = START + parameters = {"operational_options": {}} + with mock.patch( + "reana_workflow_controller.workflow_run_manager." + "current_k8s_batchv1_api_client" + ): + # provide user secret store + with mock.patch( + "reana_commons.k8s.secrets.current_k8s_corev1_api_client", + corev1_api_client_with_user_secrets(user_secrets), + ): + # set workflow status to START and pass parameters + res = client.put( + url_for( + "statuses.set_workflow_status", + workflow_id_or_name=workflow_uuid, + ), + query_string={"user": default_user.id_, "status": "start"}, + content_type="application/json", + data=json.dumps(parameters), + ) + json_response = json.loads(res.data.decode()) + assert json_response["status"] == status_dict[payload].name + workflow = Workflow.query.filter(Workflow.id_ == workflow_uuid).first() + assert workflow.input_parameters == dict() + + def test_start_workflow_db_failure( app, session, From be6a3885f4f2e84ca77c7e09a89e5f2f06185452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20=C5=A0imko?= Date: Mon, 2 Sep 2024 17:20:52 +0200 Subject: [PATCH 2/8] build(docker): pin setuptools 70 (#601) Pin `setuptools` to the maximum version of 70 to allow working on Ubuntu 20.04 LTS based environments. (New versions of `setuptools` are not compatible.) Note that this fix is necessary only for the `maint-0.9` branches and the REANA 0.9 release series. In `master` we have switched to Ubuntu 24.04 LTS based environments and Python 3.12 and no pinning is necessary there. --- .github/workflows/ci.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cea15a21..f477480c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,7 +133,7 @@ jobs: - name: Install Python dependencies run: | - pip install --upgrade pip setuptools py + pip install --upgrade pip 'setuptools<71' py pip install -e .[all] - name: Run Sphinx documentation with doctests @@ -152,7 +152,7 @@ jobs: - name: Install Python dependencies run: | - pip install --upgrade pip setuptools py + pip install --upgrade pip 'setuptools<71' py pip install twine wheel pip install -e .[all] diff --git a/Dockerfile b/Dockerfile index 7f7a08a8..f1db54e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # This file is part of REANA. -# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023 CERN. +# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 CERN. # # REANA is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -31,7 +31,7 @@ RUN apt-get update -y && \ python3.8 \ python3.8-dev \ vim-tiny && \ - pip install --no-cache-dir --upgrade pip setuptools && \ + pip install --no-cache-dir --upgrade pip 'setuptools<71' && \ pip install --no-cache-dir -r /code/requirements.txt && \ apt-get remove -y \ gcc \ From 72f0c4c69759c8abf1d67c735232e5b6c033d504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20=C5=A0imko?= Date: Fri, 22 Nov 2024 11:37:20 +0100 Subject: [PATCH 3/8] feat(config): upgrade to Jupyter SciPy 7.2.2 notebook (#614) --- reana_workflow_controller/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reana_workflow_controller/config.py b/reana_workflow_controller/config.py index afc83d20..060ec3a2 100644 --- a/reana_workflow_controller/config.py +++ b/reana_workflow_controller/config.py @@ -155,7 +155,7 @@ def _env_vars_dict_to_k8s_list(env_vars): """Common to all workflow engines environment variables for debug mode.""" JUPYTER_INTERACTIVE_SESSION_DEFAULT_IMAGE = ( - "docker.io/jupyter/scipy-notebook:notebook-6.4.5" + "quay.io/jupyter/scipy-notebook:notebook-7.2.2" ) """Default image for Jupyter based interactive session deployments.""" From 7df1279f45e0981a06c3af705873c4d1d797404d Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 16 May 2024 10:11:17 +0200 Subject: [PATCH 4/8] fix(config): read secret key from env (#615) Make sure the secret key is propagated to the Flask app, instead of incorrectly using the default one. --- reana_workflow_controller/config.py | 3 +++ reana_workflow_controller/factory.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reana_workflow_controller/config.py b/reana_workflow_controller/config.py index 060ec3a2..d4be5a64 100644 --- a/reana_workflow_controller/config.py +++ b/reana_workflow_controller/config.py @@ -22,6 +22,9 @@ def _env_vars_dict_to_k8s_list(env_vars): return [{"name": name, "value": str(value)} for name, value in env_vars.items()] +SECRET_KEY = os.getenv("REANA_SECRET_KEY", "CHANGE_ME") +"""Secret key used for the application user sessions.""" + SQLALCHEMY_TRACK_MODIFICATIONS = False """Track modifications flag.""" diff --git a/reana_workflow_controller/factory.py b/reana_workflow_controller/factory.py index d728651b..e37d0313 100644 --- a/reana_workflow_controller/factory.py +++ b/reana_workflow_controller/factory.py @@ -50,7 +50,6 @@ def create_app(config_mapping=None): if config_mapping: app.config.from_mapping(config_mapping) - app.secret_key = "super secret key" # Register API routes from reana_workflow_controller.rest import ( workflows_session, From cf4ee734788da33f15a80e1fc1f0b3233ea5a007 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 23 May 2024 15:16:18 +0200 Subject: [PATCH 5/8] fix(manager): pass RabbitMQ connection details to workflow engine (#615) Make sure that workflow engines are able to connect to RabbitMQ to be able to publish workflow status update messages. --- reana_workflow_controller/config.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reana_workflow_controller/config.py b/reana_workflow_controller/config.py index d4be5a64..b47fad45 100644 --- a/reana_workflow_controller/config.py +++ b/reana_workflow_controller/config.py @@ -11,7 +11,11 @@ import os import json -from reana_commons.config import REANA_COMPONENT_PREFIX, SHARED_VOLUME_PATH +from reana_commons.config import ( + MQ_CONNECTION_STRING, + REANA_COMPONENT_PREFIX, + SHARED_VOLUME_PATH, +) from reana_db.models import JobStatus, RunStatus from reana_workflow_controller.version import __version__ @@ -122,7 +126,8 @@ def _env_vars_dict_to_k8s_list(env_vars): """ WORKFLOW_ENGINE_COMMON_ENV_VARS = [ - {"name": "SHARED_VOLUME_PATH", "value": SHARED_VOLUME_PATH} + {"name": "SHARED_VOLUME_PATH", "value": SHARED_VOLUME_PATH}, + {"name": "RABBIT_MQ", "value": MQ_CONNECTION_STRING}, ] """Common to all workflow engines environment variables.""" From 24563e568044e29d4399f78d8c081d144f116761 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 14 Aug 2024 14:07:46 +0200 Subject: [PATCH 6/8] fix(manager): avoid privilege escalation in Kubernetes jobs (#615) Configure the security context of workflow orchestrator jobs to disallow privilege escalation. --- reana_workflow_controller/k8s.py | 4 +++- reana_workflow_controller/workflow_run_manager.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/reana_workflow_controller/k8s.py b/reana_workflow_controller/k8s.py index 8d343003..31c3cf46 100644 --- a/reana_workflow_controller/k8s.py +++ b/reana_workflow_controller/k8s.py @@ -221,7 +221,9 @@ def add_environment_variable(self, name, value): def add_run_with_root_permissions(self): """Run interactive session with root.""" - security_context = client.V1SecurityContext(run_as_user=0) + security_context = client.V1SecurityContext( + run_as_user=0, allow_privilege_escalation=False + ) self._session_container.security_context = security_context def add_user_secrets(self): diff --git a/reana_workflow_controller/workflow_run_manager.py b/reana_workflow_controller/workflow_run_manager.py index 3eee0b93..df4ca6d4 100644 --- a/reana_workflow_controller/workflow_run_manager.py +++ b/reana_workflow_controller/workflow_run_manager.py @@ -557,6 +557,7 @@ def _create_job_spec( workflow_engine_container.security_context = client.V1SecurityContext( run_as_group=WORKFLOW_RUNTIME_USER_GID, run_as_user=WORKFLOW_RUNTIME_USER_UID, + allow_privilege_escalation=False, ) workflow_engine_container.volume_mounts = [workspace_mount] From 179fa89ccc4a5e77fca9efa403f4ad2003b40db3 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 28 Nov 2024 11:30:28 +0100 Subject: [PATCH 7/8] build(python): bump shared REANA packages as of 2024-11-28 (#620) --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 06dc27ed..7d30b18a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,8 +52,8 @@ pyrsistent==0.20.0 # via jsonschema python-dateutil==2.9.0 # via bravado, bravado-core, kubernetes pytz==2024.1 # via bravado-core pyyaml==6.0.1 # via bravado, bravado-core, kubernetes, reana-commons, swagger-spec-validator -reana-commons[kubernetes]==0.9.8 # via reana-db, reana-workflow-controller (setup.py) -reana-db==0.9.4 # via reana-workflow-controller (setup.py) +reana-commons[kubernetes]==0.9.9 # via reana-db, reana-workflow-controller (setup.py) +reana-db==0.9.5 # via reana-workflow-controller (setup.py) requests==2.25.0 # via bravado, bravado-core, kubernetes, reana-workflow-controller (setup.py), requests-oauthlib requests-oauthlib==1.3.1 # via kubernetes rfc3987==1.3.8 # via jsonschema diff --git a/setup.py b/setup.py index 253273d6..2cb170dc 100644 --- a/setup.py +++ b/setup.py @@ -56,8 +56,8 @@ "jsonpickle>=0.9.6", "marshmallow>2.13.0,<=2.20.1", "packaging>=18.0", - "reana-commons[kubernetes]>=0.9.8,<0.10.0", - "reana-db>=0.9.4,<0.10.0", + "reana-commons[kubernetes]>=0.9.9,<0.10.0", + "reana-db>=0.9.5,<0.10.0", "requests==2.25.0", "sqlalchemy-utils>=0.31.0", "uwsgi-tools>=1.1.1", From 050b4e6faadf135e79575fb76629a1e2c0cd6e6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:32:45 +0000 Subject: [PATCH 8/8] chore(maint-0.9): release 0.9.4 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 23 +++++++++++++++++++++++ Dockerfile | 4 ++-- docs/openapi.json | 2 +- reana_workflow_controller/version.py | 2 +- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ee2bfff4..1b1f6a80 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.3" + ".": "0.9.4" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe0babc..55278100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [0.9.4](https://github.com/reanahub/reana-workflow-controller/compare/0.9.3...0.9.4) (2024-11-29) + + +### Build + +* **docker:** pin setuptools 70 ([#601](https://github.com/reanahub/reana-workflow-controller/issues/601)) ([be6a388](https://github.com/reanahub/reana-workflow-controller/commit/be6a3885f4f2e84ca77c7e09a89e5f2f06185452)) +* **python:** bump shared REANA packages as of 2024-11-28 ([#620](https://github.com/reanahub/reana-workflow-controller/issues/620)) ([179fa89](https://github.com/reanahub/reana-workflow-controller/commit/179fa89ccc4a5e77fca9efa403f4ad2003b40db3)) + + +### Features + +* **config:** upgrade to Jupyter SciPy 7.2.2 notebook ([#614](https://github.com/reanahub/reana-workflow-controller/issues/614)) ([72f0c4c](https://github.com/reanahub/reana-workflow-controller/commit/72f0c4c69759c8abf1d67c735232e5b6c033d504)) +* **helm:** allow cluster administrator to configure ingress host ([#588](https://github.com/reanahub/reana-workflow-controller/issues/588)) ([a7c9c85](https://github.com/reanahub/reana-workflow-controller/commit/a7c9c851277f3ca191c073fdc6c6d5d4149a95e8)) +* **sessions:** expose user secrets in interactive sessions ([#591](https://github.com/reanahub/reana-workflow-controller/issues/591)) ([784efee](https://github.com/reanahub/reana-workflow-controller/commit/784efee4be8b4a9785d03d3d05b00f3da2b455c2)) + + +### Bug fixes + +* **config:** read secret key from env ([#615](https://github.com/reanahub/reana-workflow-controller/issues/615)) ([7df1279](https://github.com/reanahub/reana-workflow-controller/commit/7df1279f45e0981a06c3af705873c4d1d797404d)) +* **manager:** avoid privilege escalation in Kubernetes jobs ([#615](https://github.com/reanahub/reana-workflow-controller/issues/615)) ([24563e5](https://github.com/reanahub/reana-workflow-controller/commit/24563e568044e29d4399f78d8c081d144f116761)) +* **manager:** pass RabbitMQ connection details to workflow engine ([#615](https://github.com/reanahub/reana-workflow-controller/issues/615)) ([cf4ee73](https://github.com/reanahub/reana-workflow-controller/commit/cf4ee734788da33f15a80e1fc1f0b3233ea5a007)) +* **set_workflow_status:** validate endpoint arguments ([#589](https://github.com/reanahub/reana-workflow-controller/issues/589)) ([5945d7f](https://github.com/reanahub/reana-workflow-controller/commit/5945d7fca095531b3601e551c527457f9413643c)) + ## [0.9.3](https://github.com/reanahub/reana-workflow-controller/compare/0.9.2...0.9.3) (2024-03-04) diff --git a/Dockerfile b/Dockerfile index f1db54e0..d944b1ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -108,7 +108,7 @@ CMD exec uwsgi \ # Set image labels LABEL org.opencontainers.image.authors="team@reanahub.io" -LABEL org.opencontainers.image.created="2024-03-04" +LABEL org.opencontainers.image.created="2024-11-29" LABEL org.opencontainers.image.description="REANA reproducible analysis platform - workflow controller component" LABEL org.opencontainers.image.documentation="https://reana-workflow-controller.readthedocs.io/" LABEL org.opencontainers.image.licenses="MIT" @@ -117,5 +117,5 @@ LABEL org.opencontainers.image.title="reana-workflow-controller" LABEL org.opencontainers.image.url="https://github.com/reanahub/reana-workflow-controller" LABEL org.opencontainers.image.vendor="reanahub" # x-release-please-start-version -LABEL org.opencontainers.image.version="0.9.3" +LABEL org.opencontainers.image.version="0.9.4" # x-release-please-end diff --git a/docs/openapi.json b/docs/openapi.json index e082d75d..26602286 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "info": { "description": "Submit and manage workflows", "title": "REANA Workflow Controller", - "version": "0.9.3" + "version": "0.9.4" }, "paths": { "/api/workflows": { diff --git a/reana_workflow_controller/version.py b/reana_workflow_controller/version.py index 1fda2361..75825193 100644 --- a/reana_workflow_controller/version.py +++ b/reana_workflow_controller/version.py @@ -14,4 +14,4 @@ from __future__ import absolute_import, print_function -__version__ = "0.9.3" +__version__ = "0.9.4"