Skip to content

Commit

Permalink
Adding ability to modify and validate specification
Browse files Browse the repository at this point in the history
Closes #117
  • Loading branch information
costrouc committed Mar 8, 2022
1 parent 4a2d01a commit 03b50e3
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 31 deletions.
16 changes: 14 additions & 2 deletions conda-store-server/conda_store_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
import datetime

from celery import Celery, group
from traitlets import Type, Unicode, Integer, List, default
from traitlets import Type, Unicode, Integer, List, default, Callable
from traitlets.config import LoggingConfigurable
from sqlalchemy.pool import NullPool

from conda_store_server import orm, utils, storage, schema, api, conda


def conda_store_validate_specification(conda_store: 'CondaStore', specification: schema.CondaSpecification) -> schema.CondaSpecification:
return specification


class CondaStore(LoggingConfigurable):
storage_class = Type(
default_value=storage.S3Storage,
Expand Down Expand Up @@ -126,6 +130,12 @@ def _default_celery_results_backend(self):
config=True,
)

validate_specification = Callable(
conda_store_validate_specification,
help="callable function taking conda_store and specification as input arguments to apply for validating and modifying a given environment. If there are validation issues with the environment ValueError should be raised",
config=True
)

@property
def session_factory(self):
if hasattr(self, "_session_factory"):
Expand Down Expand Up @@ -244,7 +254,9 @@ def register_environment(
else:
namespace = namespace_model

specification_model = schema.CondaSpecification.parse_obj(specification)
specification_model = self.validate_specification(
self,
schema.CondaSpecification.parse_obj(specification))
specification_sha256 = utils.datastructure_hash(specification_model.dict())

specification = api.get_specification(self.db, sha256=specification_sha256)
Expand Down
28 changes: 26 additions & 2 deletions conda-store-server/conda_store_server/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import enum
from typing import List, Optional, Union, Dict
import functools
from pkg_resources import Requirement

from pydantic import BaseModel, Field, constr
from conda.models.match_spec import MatchSpec
from pydantic import BaseModel, Field, constr, validator


def _datetime_factory(offset: datetime.timedelta):
Expand Down Expand Up @@ -112,13 +114,35 @@ class Config:
class CondaSpecificationPip(BaseModel):
pip: List[str]

@validator('pip', each_item=True)
def check_pip(cls, v):
try:
Requirement.parse(v)
except Exception:
raise ValueError(f'Invalid pypi package dependency {v}')

return v


class CondaSpecification(BaseModel):
name: constr(regex=f"^[{ALLOWED_CHARACTERS}]+$") # noqa: F722
channels: Optional[List[str]]
channels: Optional[List[str]] = []
dependencies: List[Union[str, CondaSpecificationPip]]
prefix: Optional[str]

@validator('dependencies', each_item=True)
def check_dependencies(cls, v):
if not isinstance(v, str):
return v # ignore pip field

try:
MatchSpec(v)
except Exception as e:
print(e)
raise ValueError(f'Invalid conda package dependency specification {v}')

return v


###############################
# Docker Registry Schema
Expand Down
86 changes: 59 additions & 27 deletions conda-store-server/conda_store_server/server/views/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
app_ui = Blueprint("ui", __name__, template_folder="templates")


@app_ui.route("/create/", methods=["GET", "POST"])
@app_ui.route("/create/", methods=["GET"])
def ui_create_get_environment():
conda_store = get_conda_store()
auth = get_auth()
Expand All @@ -31,32 +31,64 @@ def ui_create_get_environment():
"entity": auth.authenticate_request(),
}

if request.method == "GET":
return render_template("create.html", **context)
elif request.method == "POST":
try:
namespace_id = int(request.form.get("namespace"))
specification_text = request.form.get("specification")
specification = schema.CondaSpecification.parse_obj(
yaml.safe_load(specification_text)
)
namespace = api.get_namespace(conda_store.db, id=namespace_id)
api.post_specification(conda_store, specification.dict(), namespace.name)
return redirect(url_for("ui.ui_list_environments"))
except yaml.YAMLError:
return render_template(
"create.html",
specification=specification_text,
message="Unable to parse. Invalid YAML",
**context,
)
except pydantic.ValidationError as e:
return render_template(
"create.html",
specification=specification_text,
message=str(e),
**context,
)
return render_template("create.html", **context)


@app_ui.route("/create/", methods=["POST"])
def ui_create_post_environment():
conda_store = get_conda_store()
auth = get_auth()

orm_namespaces = auth.filter_namespaces(
api.list_namespaces(conda_store.db, show_soft_deleted=False)
)

context = {
"namespaces": orm_namespaces.all(),
"entity": auth.authenticate_request(),
}

permissions = {Permissions.ENVIRONMENT_CREATE}
namespace_id = int(request.form.get("namespace", conda_store.default_namespace))
namespace = api.get_namespace(conda_store.db, id=namespace_id)
if namespace is None:
permissions.add(Permissions.NAMESPACE_CREATE)

try:
specification_text = request.form.get("specification")
specification = yaml.safe_load(specification_text)
specification = schema.CondaSpecification.parse_obj(specification)
except yaml.error.YAMLError as e:
return render_template(
"create.html",
specification=specification_text,
message="Unable to parse. Invalid YAML",
**context,
)
except pydantic.ValidationError as e:
return render_template(
"create.html",
specification=specification_text,
message=str(e),
**context,
)

auth.authorize_request(
f"{namespace.name}/{specification.name}",
permissions,
require=True,
)

try:
api.post_specification(conda_store, specification.dict(), namespace.name)
return redirect(url_for("ui.ui_list_environments"))
except ValueError as e:
return render_template(
"create.html",
specification=specification_text,
message=str(e),
**context,
)


@app_ui.route("/", methods=["GET"])
Expand Down
10 changes: 10 additions & 0 deletions tests/assets/conda_store_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
c.CondaStore.default_gid = 100
c.CondaStore.default_permissions = "775"


def validate_specification(conda_store, specification):
has_ipykernel = any(('ipykernel' in _) for _ in specification.dependencies)
if not has_ipykernel:
specification.dependencies.append('ipykernel')
return specification

c.CondaStore.validate_specification = validate_specification


c.S3Storage.internal_endpoint = "minio:9000"
c.S3Storage.external_endpoint = "localhost:9000"
c.S3Storage.access_key = "admin"
Expand Down

0 comments on commit 03b50e3

Please sign in to comment.