GithubHelp home page GithubHelp logo

fnndsc / chris_store Goto Github PK

View Code? Open in Web Editor NEW
11.0 11.0 20.0 561 KB

Backend for ChRIS plugin apps store

License: MIT License

Shell 7.38% Python 91.83% Dockerfile 0.79%
django django-rest-framework docker openshift kubernetes python3 pipelines swift-storage medical

chris_store's Introduction

ChRIS logo ChRIS_store

Build Status License Last Commit

Backend for the ChRIS store. This is a Django-PostgreSQL project that houses descriptions of ChRIS plugin-apps and workflows for registering to a ChRIS CUBE instance.

ChRIS store development, testing and deployment

Abstract

This page describes how to quickly get the set of services comprising the backend up and running for development and how to run the automated tests. A production deployment of the ChRIS store backend services is also explained.

Preconditions

Install latest Docker and Docker Compose

Currently tested platforms:

On a Linux machine make sure to add your computer user to the docker group

Consult this page: https://docs.docker.com/engine/install/linux-postinstall/

TL;DR

If you read nothing else on this page, and just want to get an instance of the ChRIS store backend services up and running with no mess, no fuss:

git clone https://github.com/FNNDSC/ChRIS_store.git
cd ChRIS_store
./make.sh down ; ./make.sh up

The resulting instance uses the default Django development server and therefore is not suitable for production.

Production deployment on a single-machine Docker Swarm cluster

To get the production system up:

Start a local Docker Swarm cluster if not already started:

docker swarm init --advertise-addr 127.0.0.1

Fetch source code:

git clone https://github.com/FNNDSC/ChRIS_store
cd ChRIS_store

Create secrets directory:

mkdir swarm/prod/secrets

Now copy all the required secret configuration files into the secrets directory, please take a look at this wiki page to learn more about these files

Deploy ChRIS store backend containers:

./deploy.sh up

To tear down:

Remove ChRIS store backend containers:

cd ChRIS_store
./deploy.sh down

Remove the local Docker Swarm cluster if desired:

docker swarm leave --force

Development

Instantiate ChRIS Store dev environment

Start ChRIS Store services by running the make bash script from the repository source directory

git clone https://github.com/FNNDSC/ChRIS_store.git
./make.sh up

All the steps performed by the above script are properly documented in the script itself.

After running this script all the automated tests should have successfully run and a Django development server should be running in interactive mode in this terminal.

Rerun automated tests after modifying source code

Open another terminal and run the Unit and Integration tests within the container running the Django server:

To run only the Unit tests:

cd ChRIS_store
docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test --exclude-tag integration

To run only the Integration tests:

docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test --tag integration

To run all the tests:

docker-compose -f docker-compose_dev.yml exec chris_store_dev python manage.py test 

Check code coverage of the automated tests

Make sure the store_backend/ dir is world writable. Then type:

docker-compose -f docker-compose_dev.yml exec chris_store_dev coverage run --source=plugins,pipelines,users manage.py test
docker-compose -f docker-compose_dev.yml exec chris_store_dev coverage report

Using HTTPie to play with the REST API

A simple GET request:

http http://localhost:8010/api/v1/

A simple POST request to register a new plugin app in the store:

First save the plugin representation json file by running the plugin with the --savejson flag:

docker run --rm -v /tmp/json:/json fnndsc/pl-simplefsapp simplefsapp --savejson /json

Then upload the plugin representation json file to the ChRIS Store as part of the POST request:

http -a cubeadmin:cubeadmin1234 -f POST http://localhost:8010/api/v1/plugins/ dock_image=fnndsc/pl-simplefsapp descriptor_file@/tmp/json/SimpleFSApp.json public_repo=https://github.com/FNNDSC/pl-simplefsapp name=pl-simplefsapp

An unauthenticated POST request to create a new ChRIS store user account:

http POST http://localhost:8010/api/v1/users/ Content-Type:application/vnd.collection+json Accept:application/vnd.collection+json template:='{"data":[{"name":"email","value":"[email protected]"}, {"name":"password","value":"newstoreuser1234"}, {"name":"username","value":"newstoreuser"}]}'

Destroy ChRIS Store dev environment

Stop and remove ChRIS Store services by running the make bash script from the repository source directory

./make.sh down

REST API Documentation

Available here.

Install Sphinx and the http extension (useful to document the REST API)

pip install Sphinx
pip install sphinxcontrib-httpdomain

Build the html documentation

cd docs/
make html

Learn More

If you are interested in contributing or joining us, Check here.

chris_store's People

Contributors

aaishpra avatar jbernal0019 avatar jennydaman avatar mairin avatar rudolphpienaar avatar sandip117 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

chris_store's Issues

Case-insensitive search support

MySQL and Django string case-sensitivity handling has some weirdness going on. Since upgrade to MySQL 8 in c45e69d, a bug was fixed where now plugin names are case-insensitive but case-insensitive search no longer works. This is a regression, case-insensitive search must be reimplemented.

Until case-insensitive search can be implemented, we should not do the migration on chrisstore.co

Maybe we can evaluate ElasticSearch?

Sorting out Authors vs. Owners

ChRIS Store has authors and owners per plugin...

  • authors are who is listed in the JSON
  • owner is person who uploads it.

We should make the meaning of the distinction more clear.

Icon upload feature: static file server so icons can be uploaded and hosted on the ChRIS store

Current Situation

ICON is part of the JSON description specification of a ChRIS plugin [1,2]. It's kind of awkward to have this defined in the Python source code of a ChRIS plugin.

By convention and per documentation, ICON is supposed to be an HTTPS URL to an image file. However, it is not currently used anywhere: All of our plugins (to my knowledge) define ICON as an empty string ("") and neither of our front-ends https://github.com/FNNDSC/ChRIS_ui/ nor https://github.com/FNNDSC/ChRIS_store_ui/ use the icon data.

[1] https://github.com/FNNDSC/cookiecutter-chrisapp/blob/ad4fb567640ae312baad69d9727e5a4ada40970b/%7B%7Bcookiecutter.app_repo_name%7D%7D/%7B%7Bcookiecutter.app_name%7D%7D/%7B%7Bcookiecutter.app_name%7D%7D.py#L92
[2] https://github.com/FNNDSC/chrisapp/blob/1863f2dc6d0b74cf789590e84370890d26e24c7d/chrisapp/base.py#L425

Proposal

ICON should no longer be something defined by the source code of a plugin. Instead, the ChRIS_store backend should provide the functionality to upload and serve icons. For example, one might upload a file like this:

https -a chris:chris1234 -f POST https://chrisstore.co/api/v1/plugins/6/icon/ icon@/home/jenni/my_icon.svg

The ChRIS_store backend will store the uploaded file in OpenStack Swift Object Storage.

The uploaded file will be accessible from:

GET https://chrisstore.co/api/v1/plugins/6/icon/

NOTE: the server should specify MIME type correctly.

An alternative implementation would be for the endpoint /api/v1/plugins/6/icon/ to return something like

{
	"collection": {
		"version": "1.0",
		"href": "https://chrisstore.co/api/v1/plugins/6/icon/",
		"items": [
			{
				"data": [],
				"href": "https://chrisstore.co/api/v1/plugins/6/icon/",
				"links": [
					{
						"rel": "file_resource",
						"value": "https://chrisstore.co/api/v1/uploadedfiles/1/my_icon.svg"
					}
				]
			}
		],
		"links": [],
		"template": {
			"data": [
				{
					"name": "icon",
					"value": ""
				}
			]
		},
		"total": 1
	}
}

The data returned from https://chrisstore.co/api/v1/plugins/6/ should fill in the correct value for icon (a URL to the static file, hosted on the backend).

The front-end(s) should make use of these icons.

Tasks

  • allow the icon field to be optional when uploading a plugin
  • upload icon file functionality
  • serving icon file functionality

Add "Categories" object and allow all categories to be fetched

The Category of a plugin is currently only a string and anything can be put in its place.
This is a feature request to add a Categories object which can have certain predefined values and the ability to create more, such that a plugin can be assigned that category.

Also the API should be able to serve a list of categories when it needs to.

Modernize Dockerfile

There are several code smells in our Dockerfile, for instance, the creation and switching to a new local user. We should try to revamp the Dockerfile, especially with the intention of targeting deployment on OpenShift. For instance, assume filesystem is not writable unless it's in /tmp or a volume.

Pagination in plugin upload script

https://github.com/FNNDSC/ChRIS_store/tree/01817695223a9d83a0e650f2ffcac3555526cd9f/utils/scripts

In the folder above are scripts used to fetch plugins from the Github API and then upload them in bulk to a ChRIS_store instance as a specific user โ€” these two fields are arguments to the scripts.

We use Github Actions to run the script nightly.
https://github.com/FNNDSC/ChRIS_store/actions/workflows/sync.yml

Output example
Run utils/scripts/upload_all_from_github.sh
  utils/scripts/upload_all_from_github.sh
  shell: /usr/bin/bash -e {0}
  env:
    CHRIS_STORE_URL: https://chrisstore.co/api/v1/
    CHRIS_STORE_USER: adminusername:notmyrealpassword1234
fnndsc/pl-mricnn_predict                                     no Dockerhub tags
fnndsc/pl-mricnn                                             no Dockerhub tags
fnndsc/pl-fastsurfer_inference:1.0.15                        https://chrisstore.co/api/v1/plugins/44/
fnndsc/pl-s3retrieve:1.0.0                                   https://chrisstore.co/api/v1/plugins/27/
fnndsc/pl-med2img:1.1.0.1                                    https://chrisstore.co/api/v1/plugins/31/
fnndsc/pl-fshack                                             no Dockerhub tags
fnndsc/pl-dsdircopy:version-1.0.0                            https://chrisstore.co/api/v1/plugins/58/
fnndsc/pl-matrixmultiply                                     no Dockerhub tags
fnndsc/pl-generate_hdf5                                      no Dockerhub tags
fnndsc/pl-heartbeat                                          no Dockerhub tags
fnndsc/pl-mri10yr06mo01da_normal:1.1.4                       https://chrisstore.co/api/v1/plugins/1/
fnndsc/pl-mgz_converter                                      no Dockerhub tags
fnndsc/pl-parallelex                                         no Dockerhub tags
fnndsc/pl-s3push:version-0.1.1                               https://chrisstore.co/api/v1/plugins/59/
fnndsc/pl-fastsurfer                                         no Dockerhub tags
fnndsc/pl-surfaces-fetus:1.0                                 https://chrisstore.co/api/v1/plugins/6/
fnndsc/pl-z2labelmap:version-2.4.3                           https://chrisstore.co/api/v1/plugins/61/
fnndsc/pl-covidnet-generate-dataset                          no Dockerhub tags
fnndsc/pl-dircopy:2.1.0                                      https://chrisstore.co/api/v1/plugins/25/
fnndsc/pl-antsreg:initialplugin                              null
fnndsc/pl-medcon:1.0.4                                       https://chrisstore.co/api/v1/plugins/50/
fnndsc/pl-pfdicom_tagextract                                 no Dockerhub tags
fnndsc/pl-lungct                                             no Dockerhub tags
fnndsc/pl-civet:2.1.1.3                                      https://chrisstore.co/api/v1/plugins/2/
fnndsc/pl-s3path:version-0.1.1                               https://chrisstore.co/api/v1/plugins/60/
fnndsc/pl-label_decoder                                      no Dockerhub tags
fnndsc/pl-pfdicom_tagsub                                     no Dockerhub tags
fnndsc/pl-pfdorun_imgmagick:1.0.0                            https://chrisstore.co/api/v1/plugins/49/
fnndsc/pl-pfdorun:version-2.2.6                              null
fnndsc/pl-reconall:version-0.1.1                             https://chrisstore.co/api/v1/plugins/62/
fnndsc/pl-dtk:version-0.1.1                                  https://chrisstore.co/api/v1/plugins/63/
fnndsc/pl-mgz2imageslices:2.0.10                             https://chrisstore.co/api/v1/plugins/36/
fnndsc/pl-errorgenerator                                     no Dockerhub tags
fnndsc/pl-cni_challenge                                      no Dockerhub tags
fnndsc/pl-subj2label                                         no Dockerhub tags
fnndsc/pl-simplefsapp:version-1.0.0                          null
fnndsc/pl-matrix-multiplication                              no Dockerhub tags
fnndsc/pl-pfdorun_mriconvert:1.0.0                           https://chrisstore.co/api/v1/plugins/51/
fnndsc/pl-neuproseg                                          no Dockerhub tags
fnndsc/pl-dice_coeff                                         no Dockerhub tags
fnndsc/pl-brainmri_preprocessed                              no Dockerhub tags
fnndsc/pl-mri_unet                                           no Dockerhub tags
fnndsc/pl-covidnet-train                                     no Dockerhub tags
fnndsc/pl-pfdo_mgz2img:1.0.2                                 https://chrisstore.co/api/v1/plugins/40/
fnndsc/pl-tensorflowapp-sample                               no Dockerhub tags
fnndsc/pl-freesurfer_pp:version-2.0.8                        https://chrisstore.co/api/v1/plugins/64/
fnndsc/pl-imageconvert                                       no Dockerhub tags
fnndsc/pl-simpledsapp:2.0.2                                  https://chrisstore.co/api/v1/plugins/69/

How It Works

  1. use Github's API to find repositories by the FNNDSC organization with the topic chris-app.
  2. for each repository, use DockerHub's API to find docker tags by the same name.
  • tags may not be stale
  • tag does not end with :latest
  • most recently pushed tag is selected (not necessarily the highest version, which is a logical error for when an old version is the most recently pushed tag)
  1. upload the plugin to the ChRIS_store
  2. pull the docker image
  3. use docker inspect to find out its main script
  4. generate a representation of the plugin using script --json
  5. multi-part post request to https://chrisstore.co/api/v1/plugins/

Note: upload_plugins.sh is unrelated.

Pagination

  • pagination of Github API

However, pagination is not supported so there is a hard limit to 100 results. We're soonโ„ข approaching that number! We need to use a for-loop to paginate over the query results from Github's API. (Possibly too over DockerHub's API to be technically correct, though I think not doing so has no practical implications).

Flexible Usage

  • The features can be extended by customizing that query on Github's API
  • Smarter use of DockerHub's API, possibly in conjunction with querying Github's API for tags or releases: use an inner loop to process all non-stale non-:latest docker tags

Implementation

  • currently, bash, curl, and jq are used.
    • these are efficient choices since they are preinstalled in Github Actions virtual environment
  • the above features may warrant a rewrite in python using requests
    • If not using bash, please write a small Dockerfile
  • Primarily, the script is run in CI (continuous integration). Nonetheless, fancy and colorful TTY output is a valuable experience! The script should support well-formatted non-tty and tty output.
    • Recently I've been using progress and it works nicely.

Implementation Objectives

  • aesthetic tty output
  • legible non-tty output
  • portable (using bash or has Dockerfile)

Plugin JSON linting

Error messages need improvement, e.g. invalid plugin representation

{
  "type": "ds",
  "parameters": [],
  "icon": "",
  "authors": "Me, i did this",
  "title": "pl-mgz2labels",
  "category": "",
  "description": "literally nothing",
  "documentation": "http://no",
  "license": "Opensource (MIT)",
  "version": 1.2,
  "selfpath": "/usr/src/lol",
  "selfexec": "hey",
  "execshell": "python3",
  "max_number_of_workers": 1,
  "min_number_of_workers": 1,
  "max_memory_limit": "",
  "min_memory_limit": "",
  "max_cpu_limit": "",
  "min_cpu_limit": "",
  "max_gpu_limit": 0,
  "min_gpu_limit": 0
}

VERSION must be type string

https://github.com/FNNDSC/chrisapp/blob/379c8fbc5b915a8901d8a607f73d1ba0e85225b3/chrisapp/base.py#L371

A user might erroneously define a version as float, e.g. 1.2

Upload gives

http -a chris:chris1234 -f POST :8010/api/v1/plugins/ dock_image=fnndsc/pl-simpledsapp [email protected] name=pf-pluginapp public_repo=https://github.com/FNNDSC/pl-pluginapp
HTTP/1.1 500 Internal Server Error
...
{
    "collection": {
        "error": {
            "message": "Internal server error"
        },
        "href": "http://localhost:8010/api/v1/plugins/",
        "version": "1.0"
    }
}

Checking DB connectivity as root might not be correct

python check_db_connection.py -u root -p $MYSQL_ROOT_PASSWORD --host $DATABASE_HOST --max-attempts 30

I'm in an esoteric situation, trying to migrate the backend for https://chrisstore.co

The MySQL database was seemingly successfully migrated using mysqldump and as you can see, the backend is working

https://chrisstore.co/api/v1/plugins/1/

However I noticed that python check_db_connection.py fails --- which might be expected since by default, MySQL does not permit root login from non-localhost. Regardless there is no effect if the command is unsuccessful.

Support OPTIONS request

https HEAD chrisstore.co/api/v1/plugins/search/
HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS
Connection: Keep-Alive
Content-Length: 13765
Content-Type: application/vnd.collection+json
Date: Sun, 11 Apr 2021 12:31:48 GMT
Keep-Alive: timeout=5, max=100
Server: Apache
Vary: Accept,Cookie
X-Frame-Options: SAMEORIGIN

OPTIONS is allowed but does not work.

https OPTIONS chrisstore.co/api/v1/plugins/search/
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Length: 130
Content-Type: application/vnd.collection+json
Date: Sun, 11 Apr 2021 12:31:38 GMT
Server: Apache
Vary: Cookie
X-Frame-Options: SAMEORIGIN

{
    "collection": {
        "error": {
            "message": "Internal server error"
        },
        "href": "https://chrisstore.co/api/v1/plugins/search/",
        "version": "1.0"
    }
}

Advanced search queries

class PluginFilter(FilterSet):
"""
Filter class for the Plugin model.
"""
min_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
lookup_expr='gte')
max_creation_date = django_filters.IsoDateTimeFilter(field_name='creation_date',
lookup_expr='lte')
owner_username = django_filters.CharFilter(
field_name='meta__owner__username', lookup_expr='exact')
name = django_filters.CharFilter(field_name='meta__name', lookup_expr='icontains')
name_exact = django_filters.CharFilter(field_name='meta__name', lookup_expr='exact')
title = django_filters.CharFilter(field_name='meta__title', lookup_expr='icontains')
category = django_filters.CharFilter(field_name='meta__category',
lookup_expr='icontains')
type = django_filters.CharFilter(field_name='meta__type', lookup_expr='exact')
description = django_filters.CharFilter(field_name='description',
lookup_expr='icontains')
name_title_category = django_filters.CharFilter(method='search_name_title_category')
name_latest = django_filters.CharFilter(method='search_latest')
name_exact_latest = django_filters.CharFilter(method='search_latest')
def search_name_title_category(self, queryset, name, value):
"""
Custom method to get a filtered queryset with all plugins for which name or title
or category matches the search value.
"""
# construct the full lookup expression.
lookup = models.Q(meta__name__icontains=value)
lookup = lookup | models.Q(meta__title__icontains=value)
lookup = lookup | models.Q(meta__category__icontains=value)
return queryset.filter(lookup)
def search_latest(self, queryset, name, value):
"""
Custom method to get a filtered queryset with the latest version according to
creation date of all plugins whose name matches the search value.
"""
if name == 'name_exact_latest':
qs = queryset.filter(meta__name=value)
return qs.order_by('-creation_date')[:1]
else:
qs = queryset.filter(meta__name__icontains=value)
qs = qs.order_by('meta', '-creation_date')
result_id_list = []
meta_id = 0
for pl in qs:
pl_meta_id = pl.meta.id
if pl_meta_id != meta_id:
result_id_list.append(pl.id)
meta_id = pl_meta_id
return qs.filter(pk__in=result_id_list)
class Meta:
model = Plugin
fields = ['id', 'name', 'name_latest', 'name_exact', 'name_exact_latest',
'dock_image', 'type', 'category', 'owner_username', 'min_creation_date',
'max_creation_date', 'title', 'version', 'description',
'name_title_category']

Currently, only a few search queries are supported. By the looks of it, these are

fields = ['id', 'name', 'name_latest', 'name_exact', 'name_exact_latest',
'dock_image', 'type', 'category', 'owner_username', 'min_creation_date',
'max_creation_date', 'title', 'version', 'description',
'name_title_category']

These options are sufficient for most use cases. However there are some fields, such as author, which currently are not searchable.

More fields should be searchable, and individually searchable.

It would be a pain to implement the cross-product of fields as specific query-string options e.g. name_title_category. A preferred paradigm would be:

GET /api/v1/plugins/search/?name=fetal&title=fetal&category=fetal

Moreover, it should be possible to define the boolean operations to apply on search terms. Maybe this would look like

GET /api/v1/plugins/search/?q=(title%3D%22fetal%22ORname%3D%22fetal%22)ANDauthor%3D%22fetal%22

An advanced implementation of search queries might use a search engine like ElasticSearch.

Handle plugin creation errors gracefully

Create a conditional statement to handle the errors at the time of creation of plugin gracefully.

  • Current implementation: As of now, the entire response received from the server is begin rendered as it is in the UI.

  • Expected implementation: Display the reason for failing in creating the plugin.
    Eg: A plugin with the given name already exists. or The URL format for the repo is not valid.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.