Compare commits

11 Commits
1.0.0 ... 1.0.2

Author SHA1 Message Date
allegroai
09ab2af34c Version bump 2021-05-27 17:13:19 +03:00
allegroai
8bb26a6b0b Fix fileserver depends on deprecated flask._compat.fspath and safe_join 2021-05-27 17:13:02 +03:00
allegroai
3f2304549d Move new migrations to 1_0_2 2021-05-27 16:56:47 +03:00
allegroai
ad72a435f1 Clean Task runtime on reset 2021-05-27 16:56:03 +03:00
allegroai
f34332344e Fix Task container raises validation error on null values 2021-05-27 16:55:32 +03:00
allegroai
d324b57dd7 Fix bad error message format 2021-05-27 16:55:00 +03:00
allegroai
2216bfe875 Version bump 2021-05-11 16:12:48 +03:00
allegroai
9beefa7473 Add missing login.logout endpoint 2021-05-11 16:12:27 +03:00
allegroai
8ebc334889 Fix broken config dir backwards compatibility (/opt/trains/config should still be supported) 2021-05-11 16:12:13 +03:00
allegroai
e662c850af Update config file in docs 2021-05-04 11:07:38 +03:00
allegroai
1e5163e530 Upgrade jinja2 version due to CVE-2020-28493 2021-05-03 23:23:06 +03:00
14 changed files with 70 additions and 33 deletions

View File

@@ -238,6 +238,7 @@ def reset_task(
set__last_metrics={},
set__metric_stats={},
set__models__output=[],
set__runtime={},
unset__output__result=1,
unset__output__error=1,
unset__last_worker=1,

View File

@@ -19,7 +19,7 @@ from pyparsing import (
from apiserver.utilities import json
EXTRA_CONFIG_PATHS = ("/opt/clearml/config",)
EXTRA_CONFIG_PATHS = ("/opt/trains/config", "/opt/clearml/config")
DEFAULT_PREFIXES = ("clearml", "trains")
EXTRA_CONFIG_PATH_SEP = ":" if platform.system() != "Windows" else ";"

View File

@@ -176,6 +176,13 @@ class SafeMapField(MapField, DictValidationMixin):
self.error("Empty keys are not allowed in a MapField")
class NullableStringField(StringField):
def validate(self, value):
if value is None:
return
super(NullableStringField, self).validate(value)
class SafeDictField(DictField, DictValidationMixin):
def validate(self, value):
self._safe_validate(value)

View File

@@ -18,6 +18,7 @@ from apiserver.database.fields import (
UnionField,
SafeSortedListField,
EmbeddedDocumentListField,
NullableStringField,
)
from apiserver.database.model import AttributedDocument
from apiserver.database.model.base import ProperDictMixin, GetMixin
@@ -260,7 +261,7 @@ class Task(AttributedDocument):
configuration = SafeMapField(field=EmbeddedDocumentField(ConfigurationItem))
runtime = SafeDictField(default=dict)
models: Models = EmbeddedDocumentField(Models, default=Models)
container = SafeMapField(field=StringField(default=""))
container = SafeMapField(field=NullableStringField())
enqueue_status = StringField(
choices=get_options(TaskStatus), exclude_by_default=True
)

View File

@@ -97,22 +97,6 @@ def _migrate_model_labels(db: Database):
tasks.update_one({"_id": doc["_id"]}, {"$set": set_commands})
def _migrate_project_description(db: Database):
projects: Collection = db["project"]
filter = {
"$or": [
{
"$expr": {"$lt": [{"$strLenCP": "$description"}, 100]},
"description": {"$regex": "^Auto-generated at ", "$options": "i"},
},
{"description": {"$regex": "^Auto-generated during move$", "$options": "i"}},
{"description": {"$regex": "^Auto-generated while cloning$", "$options": "i"}},
]
}
for doc in projects.find(filter=filter):
projects.update_one({"_id": doc["_id"]}, {"$unset": {"description": 1}})
def _migrate_project_names(db: Database):
projects: Collection = db["project"]
@@ -141,5 +125,4 @@ def migrate_backend(db: Database):
_migrate_docker_cmd(db)
_migrate_model_labels(db)
_migrate_project_names(db)
_migrate_project_description(db)
_drop_all_indices_from_collections(db, ["task*"])

View File

@@ -0,0 +1,22 @@
from pymongo.collection import Collection
from pymongo.database import Database
def _migrate_project_description(db: Database):
projects: Collection = db["project"]
filter = {
"$or": [
{
"$expr": {"$lt": [{"$strLenCP": "$description"}, 100]},
"description": {"$regex": "^Auto-generated at ", "$options": "i"},
},
{"description": {"$regex": "^Auto-generated during move$", "$options": "i"}},
{"description": {"$regex": "^Auto-generated while cloning$", "$options": "i"}},
]
}
for doc in projects.find(filter=filter):
projects.update_one({"_id": doc["_id"]}, {"$unset": {"description": 1}})
def migrate_backend(db: Database):
_migrate_project_description(db)

View File

@@ -12,7 +12,7 @@ funcsigs==1.0.2
furl>=2.0.0
gunicorn>=19.7.1
humanfriendly==4.18
jinja2==2.10.1
jinja2==2.11.3
jsonmodels>=2.3
jsonschema>=2.6.0
luqum>=0.10.0

View File

@@ -93,3 +93,19 @@ supported_modes {
}
}
}
logout {
authorize: false
allow_roles = [ "*" ]
"2.13" {
description: """ Logout (including SSO, if used)) """
request {
type: object
additionalProperties: false
}
response {
type: object
additionalProperties: false
}
}
}

View File

@@ -41,14 +41,12 @@ def login(call: APICall, *_, **__):
)
# Add authorization cookie
call.result.cookies[
config.get("apiserver.auth.session_auth_cookie_name")
] = call.result.data_model.token
call.result.set_auth_cookie(call.result.data_model.token)
@endpoint("auth.logout", min_version="2.2")
def logout(call: APICall, *_, **__):
call.result.cookies[config.get("apiserver.auth.session_auth_cookie_name")] = None
call.result.set_auth_cookie(None)
@endpoint(

View File

@@ -1,5 +1,3 @@
from jsonmodels.fields import BoolField
from apiserver.apimodels.login import (
GetSupportedModesRequest,
GetSupportedModesResponse,
@@ -35,3 +33,8 @@ def supported_modes(call: APICall, _, __: GetSupportedModesRequest):
),
authenticated=call.auth is not None,
)
@endpoint("login.logout", min_version="2.13")
def logout(call: APICall, _, __):
call.result.set_auth_cookie(None)

View File

@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.0.2"

View File

@@ -1,8 +1,10 @@
auth {
# Fixed users login credentials
# No other user will be able to login
# Note: password may be bcrypt-hashed (generate using `python3 -c 'import bcrypt,base64; print(base64.b64encode(bcrypt.hashpw("password".encode(), bcrypt.gensalt())))'`)
fixed_users {
enabled: true
pass_hashed: false
users: [
{
username: "jane"

View File

@@ -84,7 +84,7 @@ class BasicConfig:
if not path.is_dir() and str(path) != DEFAULT_EXTRA_CONFIG_PATH
]
if invalid:
print(f"WARNING: Invalid paths in {self.extra_config_path_env_key} env var: {' '.join(invalid)}")
print(f"WARNING: Invalid paths in {self.extra_config_path_env_key} env var: {' '.join(map(str,invalid))}")
return [path for path in paths if path.is_dir()]
def _load(self, verbose=True):

View File

@@ -5,10 +5,11 @@ from argparse import ArgumentParser
from pathlib import Path
from boltons.iterutils import first
from flask import Flask, request, send_from_directory, safe_join, abort, Response
from flask._compat import fspath
from flask import Flask, request, send_from_directory, abort, Response
from flask_compress import Compress
from flask_cors import CORS
from werkzeug.exceptions import NotFound
from werkzeug.security import safe_join
from config import config
@@ -34,7 +35,10 @@ def upload():
if not filename:
continue
file_path = filename.lstrip(os.sep)
target = Path(safe_join(app.config["UPLOAD_FOLDER"], file_path))
safe_path = safe_join(app.config["UPLOAD_FOLDER"], file_path)
if safe_path is None:
raise NotFound()
target = Path(safe_path)
target.parent.mkdir(parents=True, exist_ok=True)
file.save(str(target))
results.append(file_path)
@@ -61,8 +65,8 @@ def download(path):
def delete(path):
real_path = Path(
safe_join(
fspath(app.config["UPLOAD_FOLDER"]),
fspath(path)
os.fspath(app.config["UPLOAD_FOLDER"]),
os.fspath(path)
)
)
if not real_path.exists() or not real_path.is_file():