Skip to content

Commit

Permalink
chore(master): merge maint-0.9 (#622)
Browse files Browse the repository at this point in the history
chore(maint-0.9): release 0.9.4 (#556)
build(python): bump shared REANA packages as of 2024-11-28 (#620)
fix(manager): avoid privilege escalation in Kubernetes jobs (#615)
fix(manager): pass RabbitMQ connection details to workflow engine (#615)
fix(config): read secret key from env (#615)
feat(config): upgrade to Jupyter SciPy 7.2.2 notebook (#614)
build(docker): pin setuptools 70 (#601)
fix(set_workflow_status): validate endpoint arguments (#589)

Note: The merge commit removes the changes related to pinning
`setuptools` to version 70, because this was only necessary for the
`maint-0.9` branches, as well as other 0.9.4 release-related changes.
  • Loading branch information
tiborsimko committed Dec 3, 2024
2 parents 7896c61 + 050b4e6 commit 47b799d
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.9.3"
".": "0.9.4"
}
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
19 changes: 15 additions & 4 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1356,19 +1356,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"
}
},
Expand Down
16 changes: 12 additions & 4 deletions reana_workflow_controller/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import json

from distutils.util import strtobool
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__
Expand All @@ -23,6 +27,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."""

Expand Down Expand Up @@ -120,7 +127,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."""

Expand Down Expand Up @@ -217,8 +225,8 @@ def _parse_interactive_sessions_environments(env_var):
"jupyter": {
"recommended": [
{
"name": "Jupyter SciPy Notebook 6.4.5",
"image": "docker.io/jupyter/scipy-notebook:notebook-6.4.5"
"name": "Jupyter SciPy Notebook 7.2.2",
"image": "docker.io/jupyter/scipy-notebook:notebook-7.2.2"
}
],
"allow_custom": true
Expand Down
1 change: 0 additions & 1 deletion reana_workflow_controller/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,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,
Expand Down
4 changes: 3 additions & 1 deletion reana_workflow_controller/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,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):
Expand Down
23 changes: 11 additions & 12 deletions reana_workflow_controller/rest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
Expand Down
80 changes: 53 additions & 27 deletions reana_workflow_controller/rest/workflows_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -331,7 +334,28 @@ def get_workflow_status(workflow_id_or_name): # noqa


@blueprint.route("/workflows/<workflow_id_or_name>/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.
---
Expand Down Expand Up @@ -365,21 +389,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:
Expand Down Expand Up @@ -473,24 +512,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 (
Expand All @@ -506,8 +532,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(
Expand Down
2 changes: 1 addition & 1 deletion reana_workflow_controller/version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# 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.
Expand Down
1 change: 1 addition & 0 deletions reana_workflow_controller/workflow_run_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,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]

Expand Down
45 changes: 45 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,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,
Expand Down

0 comments on commit 47b799d

Please sign in to comment.