From ede882d384ae0959eb8a9484b7d491baa628a1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20=C5=A0imko?= Date: Mon, 6 May 2024 14:24:27 +0200 Subject: [PATCH 01/15] fix(reana-admin): respect service domain when cleaning sessions (#687) Removes hard-coded infrastructure component host name domain "svc.local.cluster" during interactive session clean-up procedures, since this assumption is not really necessary and would not work under some deployment scenarios, such as on SSL-RIVER. Closes reanahub/reana-commons#457 --- reana_server/reana_admin/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reana_server/reana_admin/cli.py b/reana_server/reana_admin/cli.py index 2cbe0fea..b76036ac 100644 --- a/reana_server/reana_admin/cli.py +++ b/reana_server/reana_admin/cli.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of REANA. -# Copyright (C) 2020, 2021, 2022 CERN. +# Copyright (C) 2020, 2021, 2022, 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. @@ -1077,7 +1077,7 @@ def interactive_session_cleanup( try: session_status = requests.get( - f"http://reana-run-session-{workflow_id}.{REANA_RUNTIME_KUBERNETES_NAMESPACE}.svc.cluster.local:8081/{workflow_id}/api/status", + f"http://reana-run-session-{workflow_id}.{REANA_RUNTIME_KUBERNETES_NAMESPACE}:8081/{workflow_id}/api/status", headers={"Authorization": f"token {token}"}, ).json() except Exception as e: From d2d3673dac8917d746ddafd84bb3660e7f83c9b6 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Tue, 25 Jun 2024 17:02:19 +0200 Subject: [PATCH 02/15] fix(start): validate endpoint parameters (#689) Closes reanahub/reana-client#718 --- docs/openapi.json | 4 +++ reana_server/rest/workflows.py | 48 +++++++++++++++++++++++----------- tests/test_views.py | 2 +- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 9cf454c8..f009978f 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3198,15 +3198,19 @@ "schema": { "properties": { "input_parameters": { + "description": "Optional. Additional input parameters that override the ones from the workflow specification.", "type": "object" }, "operational_options": { + "description": "Optional. Additional operational options for workflow execution.", "type": "object" }, "reana_specification": { + "description": "Optional. Replace the original workflow specification with the given one. Only considered when restarting a workflow.", "type": "object" }, "restart": { + "description": "Optional. If true, restart the given workflow.", "type": "boolean" } }, diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index de6f7861..06b68daa 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -1132,8 +1132,16 @@ def get_workflow_status(workflow_id_or_name, user): # noqa @blueprint.route("/workflows//start", methods=["POST"]) @signin_required() +@use_kwargs( + { + "operational_options": fields.Dict(location="json"), + "input_parameters": fields.Dict(location="json"), + "restart": fields.Boolean(location="json"), + "reana_specification": fields.Raw(location="json"), + } +) @check_quota -def start_workflow(workflow_id_or_name, user): # noqa +def start_workflow(workflow_id_or_name, user, **parameters): # noqa r"""Start workflow. --- post: @@ -1166,12 +1174,20 @@ def start_workflow(workflow_id_or_name, user): # noqa type: object properties: operational_options: - type: object - reana_specification: + description: Optional. Additional operational options for workflow execution. type: object input_parameters: + description: >- + Optional. Additional input parameters that override the ones from + the workflow specification. + type: object + reana_specification: + description: >- + Optional. Replace the original workflow specification with the given one. + Only considered when restarting a workflow. type: object restart: + description: Optional. If true, restart the given workflow. type: boolean responses: 200: @@ -1285,17 +1301,23 @@ def start_workflow(workflow_id_or_name, user): # noqa "message": "Status resume is not supported yet." } """ + + operational_options = parameters.get("operational_options", {}) + input_parameters = parameters.get("input_parameters", {}) + restart = parameters.get("restart", False) + reana_specification = parameters.get("reana_specification") + try: if not workflow_id_or_name: raise ValueError("workflow_id_or_name is not supplied") - parameters = request.json if request.is_json else {} + workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, str(user.id_)) - operational_options = parameters.get("operational_options", {}) operational_options = validate_operational_options( workflow.type_, operational_options ) + restart_type = None - if "restart" in parameters: + if restart: if workflow.status not in [RunStatus.finished, RunStatus.failed]: raise ValueError("Only finished or failed workflows can be restarted.") if workflow.workspace_has_pending_retention_rules(): @@ -1303,14 +1325,9 @@ def start_workflow(workflow_id_or_name, user): # noqa "The workflow cannot be restarted because some retention rules are " "currently being applied to the workspace. Please retry later." ) - restart_type = ( - parameters.get("reana_specification", {}) - .get("workflow", {}) - .get("type", None) - ) - workflow = clone_workflow( - workflow, parameters.get("reana_specification", None), restart_type - ) + if reana_specification: + restart_type = reana_specification.get("workflow", {}).get("type", None) + workflow = clone_workflow(workflow, reana_specification, restart_type) elif workflow.status != RunStatus.created: raise ValueError( "Workflow {} is already {} and cannot be started " @@ -1319,11 +1336,12 @@ def start_workflow(workflow_id_or_name, user): # noqa if "yadage" in (workflow.type_, restart_type): _load_and_save_yadage_spec(workflow, operational_options) - input_parameters = parameters.get("input_parameters", {}) validate_workflow( workflow.reana_specification, input_parameters=input_parameters ) + # when starting the workflow, the scheduler will call RWC's `set_workflow_status` + # with the given `parameters` publish_workflow_submission(workflow, user.id_, parameters) response = { "message": "Workflow submitted.", diff --git a/tests/test_views.py b/tests/test_views.py index 5db68a3f..f940350d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -206,7 +206,7 @@ def test_restart_workflow_validates_specification( workflow_specification["workflow"]["type"] = "unknown" body = { "reana_specification": workflow_specification, - "restart": "can be anything here doesnt matter", + "restart": True, } res = client.post( url_for("workflows.start_workflow", workflow_id_or_name="test"), From 46633d6bcc151c73880f9ecbd2c02d2246492794 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Wed, 26 Jun 2024 12:09:45 +0200 Subject: [PATCH 03/15] fix(get_workflow_specification): avoid returning null parameters (#689) Closes reanahub/reana-client#718 --- reana_server/rest/workflows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index 06b68daa..144b3061 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -765,7 +765,8 @@ def get_workflow_specification(workflow_id_or_name, user): # noqa jsonify( { "specification": workflow.reana_specification, - "parameters": workflow.input_parameters, + # `input_parameters` can be null, if so return an empty dict + "parameters": workflow.input_parameters or {}, } ), 200, From 69f45fc3aae9bc625ed733de9af13eb7c0111048 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Tue, 2 Jul 2024 12:15:11 +0200 Subject: [PATCH 04/15] ci(commitlint): improve checking of merge commits (#689) --- .github/workflows/ci.yml | 2 +- run-tests.sh | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a32817f2..6dd286bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - name: Check commit message compliance of the pull request if: github.event_name == 'pull_request' run: | - ./run-tests.sh --check-commitlint ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.number }} + ./run-tests.sh --check-commitlint ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.number }} lint-shellcheck: runs-on: ubuntu-20.04 diff --git a/run-tests.sh b/run-tests.sh index 3396e016..ba2cc000 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -56,15 +56,25 @@ check_commitlint () { npx commitlint --from="$from" --to="$to" found=0 while IFS= read -r line; do - if echo "$line" | grep -qP "\(\#$pr\)$"; then + commit_hash=$(echo "$line" | cut -d ' ' -f 1) + commit_title=$(echo "$line" | cut -d ' ' -f 2-) + commit_number_of_parents=$(git rev-list --parents "$commit_hash" -n1 | awk '{print NF-1}') + if [ "$commit_number_of_parents" -gt 1 ]; then + if echo "$commit_title" | grep -qP "^chore\(.*\): merge "; then + break + else + echo "✖ Merge commits are not allowed in feature branches: $commit_title" + found=1 + fi + elif echo "$commit_title" | grep -qP "^chore\(.*\): release"; then true - elif echo "$line" | grep -qP "^chore\(.*\): release"; then + elif echo "$commit_title" | grep -qP "\(\#$pr\)$"; then true else - echo "✖ Headline does not end by '(#$pr)' PR number: $line" + echo "✖ Headline does not end by '(#$pr)' PR number: $commit_title" found=1 fi - done < <(git log "$from..$to" --format="%s") + done < <(git log "$from..$to" --format="%H %s") if [ $found -gt 0 ]; then exit 1 fi From 6e35bd776e17c1bc04145c68c1f5ea3ce5143b7e Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Mon, 1 Jul 2024 16:16:56 +0200 Subject: [PATCH 05/15] fix(set_workflow_status): publish workflows to submission queue (#691) When starting a new workflow, publish the workflow to the submission queue instead of executing the workflow immediately by calling `set_workflow_status` in workflow-controller. Closes #690 --- docs/openapi.json | 24 +++- reana_server/rest/workflows.py | 194 +++++++++++++++++++++------------ tests/test_views.py | 4 +- 3 files changed, 147 insertions(+), 75 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index f009978f..327c70a8 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3599,6 +3599,11 @@ }, { "description": "Required. New workflow status.", + "enum": [ + "start", + "stop", + "deleted" + ], "in": "query", "name": "status", "required": true, @@ -3612,19 +3617,30 @@ "type": "string" }, { - "description": "Optional. Additional input parameters and operational options.", + "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_server/rest/workflows.py b/reana_server/rest/workflows.py index 144b3061..e92ebf35 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -1131,6 +1131,75 @@ def get_workflow_status(workflow_id_or_name, user): # noqa return jsonify({"message": str(e)}), 500 +def _start_workflow(workflow_id_or_name, user, **parameters): + """Start given workflow by publishing it to the submission queue. + + This function is used by both the `set_workflow_status` and `start_workflow`. + """ + operational_options = parameters.get("operational_options", {}) + input_parameters = parameters.get("input_parameters", {}) + restart = parameters.get("restart", False) + reana_specification = parameters.get("reana_specification") + + try: + if not workflow_id_or_name: + raise ValueError("workflow_id_or_name is not supplied") + + workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, str(user.id_)) + operational_options = validate_operational_options( + workflow.type_, operational_options + ) + + restart_type = None + if restart: + if workflow.status not in [RunStatus.finished, RunStatus.failed]: + raise ValueError("Only finished or failed workflows can be restarted.") + if workflow.workspace_has_pending_retention_rules(): + raise ValueError( + "The workflow cannot be restarted because some retention rules are " + "currently being applied to the workspace. Please retry later." + ) + if reana_specification: + restart_type = reana_specification.get("workflow", {}).get("type", None) + workflow = clone_workflow(workflow, reana_specification, restart_type) + elif workflow.status != RunStatus.created: + raise ValueError( + "Workflow {} is already {} and cannot be started " + "again.".format(workflow.get_full_workflow_name(), workflow.status.name) + ) + if "yadage" in (workflow.type_, restart_type): + _load_and_save_yadage_spec(workflow, operational_options) + + validate_workflow( + workflow.reana_specification, input_parameters=input_parameters + ) + + # when starting the workflow, the scheduler will call RWC's `set_workflow_status` + # with the given `parameters` + publish_workflow_submission(workflow, user.id_, parameters) + response = { + "message": "Workflow submitted.", + "workflow_id": workflow.id_, + "workflow_name": workflow.name, + "status": RunStatus.queued.name, + "run_number": workflow.run_number, + "user": str(user.id_), + } + return response, 200 + except HTTPError as e: + logging.error(traceback.format_exc()) + return e.response.json(), e.response.status_code + except (REANAValidationError, ValidationError) as e: + logging.error(traceback.format_exc()) + return {"message": str(e)}, 400 + except ValueError as e: + logging.error(traceback.format_exc()) + return {"message": str(e)}, 403 + except Exception as e: + logging.error(traceback.format_exc()) + return {"message": str(e)}, 500 + + @blueprint.route("/workflows//start", methods=["POST"]) @signin_required() @use_kwargs( @@ -1302,74 +1371,25 @@ def start_workflow(workflow_id_or_name, user, **parameters): # noqa "message": "Status resume is not supported yet." } """ - - operational_options = parameters.get("operational_options", {}) - input_parameters = parameters.get("input_parameters", {}) - restart = parameters.get("restart", False) - reana_specification = parameters.get("reana_specification") - - try: - if not workflow_id_or_name: - raise ValueError("workflow_id_or_name is not supplied") - - workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, str(user.id_)) - operational_options = validate_operational_options( - workflow.type_, operational_options - ) - - restart_type = None - if restart: - if workflow.status not in [RunStatus.finished, RunStatus.failed]: - raise ValueError("Only finished or failed workflows can be restarted.") - if workflow.workspace_has_pending_retention_rules(): - raise ValueError( - "The workflow cannot be restarted because some retention rules are " - "currently being applied to the workspace. Please retry later." - ) - if reana_specification: - restart_type = reana_specification.get("workflow", {}).get("type", None) - workflow = clone_workflow(workflow, reana_specification, restart_type) - elif workflow.status != RunStatus.created: - raise ValueError( - "Workflow {} is already {} and cannot be started " - "again.".format(workflow.get_full_workflow_name(), workflow.status.name) - ) - if "yadage" in (workflow.type_, restart_type): - _load_and_save_yadage_spec(workflow, operational_options) - - validate_workflow( - workflow.reana_specification, input_parameters=input_parameters - ) - - # when starting the workflow, the scheduler will call RWC's `set_workflow_status` - # with the given `parameters` - publish_workflow_submission(workflow, user.id_, parameters) - response = { - "message": "Workflow submitted.", - "workflow_id": workflow.id_, - "workflow_name": workflow.name, - "status": RunStatus.queued.name, - "run_number": workflow.run_number, - "user": str(user.id_), - } - return jsonify(response), 200 - except HTTPError as e: - logging.error(traceback.format_exc()) - return jsonify(e.response.json()), e.response.status_code - except (REANAValidationError, ValidationError) as e: - logging.error(traceback.format_exc()) - return jsonify({"message": str(e)}), 400 - except ValueError as e: - logging.error(traceback.format_exc()) - return jsonify({"message": str(e)}), 403 - except Exception as e: - logging.error(traceback.format_exc()) - return jsonify({"message": str(e)}), 500 + response, status_code = _start_workflow(workflow_id_or_name, user, **parameters) + return jsonify(response), status_code @blueprint.route("/workflows//status", methods=["PUT"]) @signin_required() -def set_workflow_status(workflow_id_or_name, user): # noqa +@use_kwargs( + { + "status": fields.Str(required=True, location="query"), + # parameters for "start" + "input_parameters": fields.Dict(location="json"), + "operational_options": fields.Dict(location="json"), + "restart": fields.Boolean(location="json"), + # parameters for "deleted" + "all_runs": fields.Boolean(location="json"), + "workspace": fields.Boolean(location="json"), + } +) +def set_workflow_status(workflow_id_or_name, user, status, **parameters): # noqa r"""Set workflow status. --- put: @@ -1393,6 +1413,10 @@ def set_workflow_status(workflow_id_or_name, user): # noqa description: Required. New workflow status. required: true type: string + enum: + - start + - stop + - deleted - name: access_token in: query description: The API access_token of workflow owner. @@ -1401,18 +1425,37 @@ def set_workflow_status(workflow_id_or_name, user): # noqa - name: parameters in: body description: >- - Optional. Additional input parameters and operational options. + 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: description: >- @@ -1528,7 +1571,20 @@ def set_workflow_status(workflow_id_or_name, user): # noqa try: if not workflow_id_or_name: raise ValueError("workflow_id_or_name is not supplied") - status = request.args.get("status") + + if status == "start": + # We can't call directly RWC when starting a workflow, as otherwise + # the workflow would skip the queue. Instead, we do what the + # `start_workflow` endpoint does. + response, status_code = _start_workflow( + workflow_id_or_name, user, **parameters + ) + if "run_number" in response: + # run_number is returned by `start_workflow`, + # but not by `set_status_workflow` + del response["run_number"] + return jsonify(response), status_code + parameters = request.json if request.is_json else None response, http_response = current_rwc_api_client.api.set_workflow_status( user=str(user.id_), diff --git a/tests/test_views.py b/tests/test_views.py index f940350d..282ff0d3 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -319,7 +319,7 @@ def test_get_workflow_status(app, default_user, _get_user_mock): def test_set_workflow_status(app, default_user, _get_user_mock): - """Test get_workflow_logs view.""" + """Test set_workflow_status view.""" with app.test_client() as client: with patch( "reana_server.rest.workflows.current_rwc_api_client", @@ -341,7 +341,7 @@ def test_set_workflow_status(app, default_user, _get_user_mock): headers={"Content-Type": "application/json"}, query_string={"access_token": default_user.access_token}, ) - assert res.status_code == 500 + assert res.status_code == 422 res = client.put( url_for("workflows.set_workflow_status", workflow_id_or_name="1"), From 7356b2801840c6a78a346d7a29cb774cff645e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tibor=20=C5=A0imko?= Date: Mon, 2 Sep 2024 17:12:09 +0200 Subject: [PATCH 06/15] chore(docker): pin setuptools 70 (#700) 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. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2ed6c9d6..f235804a 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. @@ -33,7 +33,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 6ee6422d87d38339b359ad7a306575b97f210440 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 16 May 2024 10:09:21 +0200 Subject: [PATCH 07/15] fix(config): read secret key from env (#713) Make sure the secret key is propagated to the Flask app, instead of incorrectly using the default one. --- reana_server/config.py | 4 +++- reana_server/factory.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/reana_server/config.py b/reana_server/config.py index 472361f8..1c9bb41f 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -186,7 +186,9 @@ def _(x): #: Secret key - each installation (dev, production, ...) needs a separate key. #: It should be changed before deploying. -SECRET_KEY = "CHANGE_ME" +SECRET_KEY = os.getenv("REANA_SECRET_KEY", "CHANGE_ME") +"""Secret key used for the application user sessions.""" + #: Sets cookie with the secure flag by default SESSION_COOKIE_SECURE = True #: Sets session to be samesite to avoid CSRF attacks diff --git a/reana_server/factory.py b/reana_server/factory.py index 98381072..f0a05582 100644 --- a/reana_server/factory.py +++ b/reana_server/factory.py @@ -31,7 +31,6 @@ def create_app(config_mapping=None): app.config.from_object("reana_server.config") if config_mapping: app.config.from_mapping(config_mapping) - app.secret_key = "hyper secret key" app.session = Session From a2aad8ac506b98e5c29d357cec65172b6437cc8f Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 23 May 2024 12:29:28 +0200 Subject: [PATCH 08/15] feat(config): support password-protected redis (#713) REANA can now connect to password-protected Redis instances. --- reana_server/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reana_server/config.py b/reana_server/config.py index 1c9bb41f..c719a5f3 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -136,8 +136,10 @@ def _(x): # Accounts # ======== #: Redis URL -ACCOUNTS_SESSION_REDIS_URL = "redis://{host}:6379/1".format( - host=REANA_INFRASTRUCTURE_COMPONENTS_HOSTNAMES["cache"] +REANA_CACHE_PASSWORD = os.getenv("REANA_CACHE_PASSWORD", "") +ACCOUNTS_SESSION_REDIS_URL = "redis://:{password}@{host}:6379/1".format( + password=REANA_CACHE_PASSWORD, + host=REANA_INFRASTRUCTURE_COMPONENTS_HOSTNAMES["cache"], ) #: Email address used as sender of account registration emails. SECURITY_EMAIL_SENDER = SUPPORT_EMAIL From c98cbc1d15afca9309e4839db543ac19cd2036ce Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Mon, 18 Nov 2024 17:01:49 +0100 Subject: [PATCH 09/15] fix(config): do not set DEBUG programmatically (#713) The `DEBUG` configuration value is not set programmatically anymore, as Flask documentation warns that it might behave unexpectedly if set in code. `DEBUG` can still be customised via the `FLASK_DEBUG` environment variable. --- reana_server/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reana_server/config.py b/reana_server/config.py index c719a5f3..8bc93ef5 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -320,8 +320,6 @@ def _get_rate_limit(env_variable: str, default: str) -> str: OAUTHCLIENT_REMOTE_APPS["cern_openid"] = OAUTH_REMOTE_REST_APP OAUTHCLIENT_REST_REMOTE_APPS["cern_openid"] = OAUTH_REMOTE_REST_APP -DEBUG = True - SECURITY_PASSWORD_SALT = "security-password-salt" SECURITY_SEND_REGISTER_EMAIL = False From 5b6c276f57f642cc0965f096fa59875b9599df08 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Mon, 18 Nov 2024 17:02:27 +0100 Subject: [PATCH 10/15] feat(config): make PROXYFIX_CONFIG customisable (#713) Allow customisation of the `PROXYFIX_CONFIG` configuration value, to support deployments where REANA is served behind multiple proxies. --- reana_server/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reana_server/config.py b/reana_server/config.py index 8bc93ef5..7af41247 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -207,7 +207,8 @@ def _(x): # Security configuration # ====================== -PROXYFIX_CONFIG = {"x_proto": 1} +PROXYFIX_CONFIG = json.loads(os.getenv("PROXYFIX_CONFIG", '{"x_proto": 1}')) + APP_DEFAULT_SECURE_HEADERS["content_security_policy"] = {} APP_HEALTH_BLUEPRINT_ENABLED = False From 1919358cb3b05f09bceff9a904e9607760bc3fb1 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Mon, 18 Nov 2024 17:02:52 +0100 Subject: [PATCH 11/15] feat(config): make APP_DEFAULT_SECURE_HEADERS customisable (#713) Allow customisation of the `APP_DEFAULT_SECURE_HEADERS_ configuration value, to be able to configure Flask-Talisman's security mechanisms. --- reana_server/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/reana_server/config.py b/reana_server/config.py index 7af41247..1c6841af 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -210,6 +210,14 @@ def _(x): PROXYFIX_CONFIG = json.loads(os.getenv("PROXYFIX_CONFIG", '{"x_proto": 1}')) APP_DEFAULT_SECURE_HEADERS["content_security_policy"] = {} +APP_DEFAULT_SECURE_HEADERS.update( + json.loads(os.getenv("APP_DEFAULT_SECURE_HEADERS", "{}")) +) +if "REANA_FORCE_HTTPS" in os.environ: + APP_DEFAULT_SECURE_HEADERS["force_https"] = bool( + strtobool(os.getenv("REANA_FORCE_HTTPS")) + ) + APP_HEALTH_BLUEPRINT_ENABLED = False From 8c01d513c2365f337c26a2211c2ddb82df4186d4 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Mon, 18 Nov 2024 17:43:38 +0100 Subject: [PATCH 12/15] feat(config): make ACCOUNTS_USERINFO_HEADERS customisable (#713) Allow enabling and disabling the `X-User-ID` and `X-User-Session` headers that were always set on HTTP responses. These are useful only for debug purposes, and they are not needed otherwise. --- reana_server/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reana_server/config.py b/reana_server/config.py index 1c6841af..f1bebc0d 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -150,7 +150,9 @@ def _(x): #: and X-User-ID headers to HTTP response. You MUST ensure that NGINX (or other #: proxies) removes these headers again before sending the response to the #: client. Set to False, in case of doubt. -ACCOUNTS_USERINFO_HEADERS = True +ACCOUNTS_USERINFO_HEADERS = bool( + strtobool(os.getenv("ACCOUNTS_USERINFO_HEADERS", "False")) +) #: Disable password recovery by users. SECURITY_RECOVERABLE = False REANA_USER_EMAIL_CONFIRMATION = strtobool( From bbab1bf7338e9790e2195a02e320df16db1826f6 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Tue, 19 Nov 2024 17:22:37 +0100 Subject: [PATCH 13/15] feat(ext): improve error message for db decryption error (#713) --- reana_server/ext.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/reana_server/ext.py b/reana_server/ext.py index d1ed5967..378a0a31 100644 --- a/reana_server/ext.py +++ b/reana_server/ext.py @@ -14,8 +14,10 @@ from flask_limiter.errors import RateLimitExceeded from marshmallow.exceptions import ValidationError from reana_commons.config import REANA_LOG_FORMAT, REANA_LOG_LEVEL +from sqlalchemy_utils.types.encrypted.padding import InvalidPaddingError from werkzeug.exceptions import UnprocessableEntity + from reana_server import config @@ -52,6 +54,17 @@ def handle_args_validation_error(error: UnprocessableEntity): return jsonify({"message": error_message}), 400 +def handle_invalid_padding_error(error: InvalidPaddingError): + """Error handler for sqlalchemy_utils exception ``InvalidPaddingError``. + + This error handler raises an exception with a more understandable message. + """ + raise InvalidPaddingError( + "Error decrypting the database. Did you set the correct secret key? " + "If you changed the secret key, did you run the migration command?" + ) from error + + class REANA(object): """REANA Invenio app.""" @@ -103,3 +116,4 @@ def init_error_handlers(self, app): """Initialize custom error handlers.""" app.register_error_handler(RateLimitExceeded, handle_rate_limit_error) app.register_error_handler(UnprocessableEntity, handle_args_validation_error) + app.register_error_handler(InvalidPaddingError, handle_invalid_padding_error) From 94fbf7766218f4ffaf3f23be64ec6d46be1acb00 Mon Sep 17 00:00:00 2001 From: Marco Donadoni Date: Thu, 28 Nov 2024 11:27:53 +0100 Subject: [PATCH 14/15] build(python): bump shared REANA packages as of 2024-11-28 (#714) --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index d8a6ea9a..fca72f6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -159,8 +159,8 @@ pytz==2024.1 # via babel, bravado-core, celery pywebpack==1.2.0 # via flask-webpackext pyyaml==6.0.1 # via bravado, bravado-core, kubernetes, packtivity, reana-commons, snakemake, swagger-spec-validator, yadage, yadage-schemas, yte rdflib==5.0.0 # via cwltool, prov, schema-salad -reana-commons[cwl,kubernetes,snakemake,yadage]==0.9.8 # via reana-db, reana-server (setup.py) -reana-db==0.9.4 # via reana-server (setup.py) +reana-commons[cwl,kubernetes,snakemake,yadage]==0.9.9 # via reana-db, reana-server (setup.py) +reana-db==0.9.5 # via reana-server (setup.py) redis==5.0.2 # via invenio-accounts, invenio-celery requests[security]==2.25.0 # via bravado, bravado-core, cachecontrol, cwltool, kubernetes, packtivity, reana-server (setup.py), requests-oauthlib, schema-salad, snakemake, yadage, yadage-schemas requests-oauthlib==1.1.0 # via flask-oauthlib, invenio-oauth2server, invenio-oauthclient, kubernetes diff --git a/setup.py b/setup.py index b8e8abcb..82ed779d 100644 --- a/setup.py +++ b/setup.py @@ -60,8 +60,8 @@ "flask-celeryext<0.5.0", "gitpython>=3.1", "marshmallow>2.13.0,<=2.20.1", - "reana-commons[kubernetes,yadage,snakemake,cwl]>=0.9.8,<0.10.0", - "reana-db>=0.9.4,<0.10.0", + "reana-commons[kubernetes,yadage,snakemake,cwl]>=0.9.9,<0.10.0", + "reana-db>=0.9.5,<0.10.0", "requests==2.25.0", "tablib>=0.12.1", "uWSGI>=2.0.17", From 29f9c7b6942814d18a0074ce85dbf5586819ff89 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:33 +0000 Subject: [PATCH 15/15] chore(maint-0.9): release 0.9.4 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ Dockerfile | 4 ++-- docs/openapi.json | 2 +- reana_server/version.py | 2 +- 5 files changed, 36 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 08563c3d..fb743d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## [0.9.4](https://github.com/reanahub/reana-server/compare/0.9.3...0.9.4) (2024-11-29) + + +### Build + +* **python:** bump shared REANA packages as of 2024-11-28 ([#714](https://github.com/reanahub/reana-server/issues/714)) ([94fbf77](https://github.com/reanahub/reana-server/commit/94fbf7766218f4ffaf3f23be64ec6d46be1acb00)) + + +### Features + +* **config:** make ACCOUNTS_USERINFO_HEADERS customisable ([#713](https://github.com/reanahub/reana-server/issues/713)) ([8c01d51](https://github.com/reanahub/reana-server/commit/8c01d513c2365f337c26a2211c2ddb82df4186d4)) +* **config:** make APP_DEFAULT_SECURE_HEADERS customisable ([#713](https://github.com/reanahub/reana-server/issues/713)) ([1919358](https://github.com/reanahub/reana-server/commit/1919358cb3b05f09bceff9a904e9607760bc3fb1)) +* **config:** make PROXYFIX_CONFIG customisable ([#713](https://github.com/reanahub/reana-server/issues/713)) ([5b6c276](https://github.com/reanahub/reana-server/commit/5b6c276f57f642cc0965f096fa59875b9599df08)) +* **config:** support password-protected redis ([#713](https://github.com/reanahub/reana-server/issues/713)) ([a2aad8a](https://github.com/reanahub/reana-server/commit/a2aad8ac506b98e5c29d357cec65172b6437cc8f)) +* **ext:** improve error message for db decryption error ([#713](https://github.com/reanahub/reana-server/issues/713)) ([bbab1bf](https://github.com/reanahub/reana-server/commit/bbab1bf7338e9790e2195a02e320df16db1826f6)) + + +### Bug fixes + +* **config:** do not set DEBUG programmatically ([#713](https://github.com/reanahub/reana-server/issues/713)) ([c98cbc1](https://github.com/reanahub/reana-server/commit/c98cbc1d15afca9309e4839db543ac19cd2036ce)) +* **config:** read secret key from env ([#713](https://github.com/reanahub/reana-server/issues/713)) ([6ee6422](https://github.com/reanahub/reana-server/commit/6ee6422d87d38339b359ad7a306575b97f210440)) +* **get_workflow_specification:** avoid returning null parameters ([#689](https://github.com/reanahub/reana-server/issues/689)) ([46633d6](https://github.com/reanahub/reana-server/commit/46633d6bcc151c73880f9ecbd2c02d2246492794)) +* **reana-admin:** respect service domain when cleaning sessions ([#687](https://github.com/reanahub/reana-server/issues/687)) ([ede882d](https://github.com/reanahub/reana-server/commit/ede882d384ae0959eb8a9484b7d491baa628a1ee)) +* **set_workflow_status:** publish workflows to submission queue ([#691](https://github.com/reanahub/reana-server/issues/691)) ([6e35bd7](https://github.com/reanahub/reana-server/commit/6e35bd776e17c1bc04145c68c1f5ea3ce5143b7e)), closes [#690](https://github.com/reanahub/reana-server/issues/690) +* **start:** validate endpoint parameters ([#689](https://github.com/reanahub/reana-server/issues/689)) ([d2d3673](https://github.com/reanahub/reana-server/commit/d2d3673dac8917d746ddafd84bb3660e7f83c9b6)) + + +### Continuous integration + +* **commitlint:** improve checking of merge commits ([#689](https://github.com/reanahub/reana-server/issues/689)) ([69f45fc](https://github.com/reanahub/reana-server/commit/69f45fc3aae9bc625ed733de9af13eb7c0111048)) + ## [0.9.3](https://github.com/reanahub/reana-server/compare/0.9.2...0.9.3) (2024-03-04) diff --git a/Dockerfile b/Dockerfile index f235804a..fb85b641 100644 --- a/Dockerfile +++ b/Dockerfile @@ -86,7 +86,7 @@ CMD ["uwsgi --ini uwsgi.ini"] # 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 - server component" LABEL org.opencontainers.image.documentation="https://reana-server.readthedocs.io/" LABEL org.opencontainers.image.licenses="MIT" @@ -95,5 +95,5 @@ LABEL org.opencontainers.image.title="reana-server" LABEL org.opencontainers.image.url="https://github.com/reanahub/reana-server" 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 327c70a8..0beadfa7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "info": { "description": "Submit workflows to be run on REANA Cloud", "title": "REANA Server", - "version": "0.9.3" + "version": "0.9.4" }, "paths": { "/account/settings/linkedaccounts/": {}, diff --git a/reana_server/version.py b/reana_server/version.py index c6573789..38527e36 100644 --- a/reana_server/version.py +++ b/reana_server/version.py @@ -14,4 +14,4 @@ from __future__ import absolute_import, print_function -__version__ = "0.9.3" +__version__ = "0.9.4"