From 15518f0c598c3f23151381f43e10fe0e1159e465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 20 Aug 2019 18:41:52 +0200 Subject: [PATCH] :sunrise: --- .gitignore | 7 +++ .gitlab-ci.yml | 40 ++++++++++++++++ MANIFEST.in | 2 + README.rst | 37 +++++++++++++++ setup.py | 46 +++++++++++++++++++ tutornotes/__about__.py | 1 + tutornotes/__init__.py | 0 tutornotes/patches/common-env-features | 1 + tutornotes/patches/https-create | 1 + tutornotes/patches/k8s-deployments | 32 +++++++++++++ tutornotes/patches/k8s-ingress-certificates | 18 ++++++++ tutornotes/patches/k8s-ingress-rules | 6 +++ tutornotes/patches/k8s-ingress-tls-hosts | 1 + tutornotes/patches/k8s-services | 12 +++++ .../patches/kustomization-configmapgenerator | 3 ++ tutornotes/patches/lms-env | 2 + .../patches/local-docker-compose-services | 11 +++++ tutornotes/patches/nginx-extra | 37 +++++++++++++++ tutornotes/patches/proxy-apache | 27 +++++++++++ tutornotes/patches/proxy-nginx | 33 +++++++++++++ tutornotes/plugin.py | 41 +++++++++++++++++ .../templates/notes/apps/settings/tutor.py | 33 +++++++++++++ .../templates/notes/build/notes/Dockerfile | 15 ++++++ tutornotes/templates/notes/hooks/lms/init | 18 ++++++++ .../templates/notes/hooks/mysql-client/init | 2 + tutornotes/templates/notes/hooks/notes/init | 1 + 26 files changed, 427 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 setup.py create mode 100644 tutornotes/__about__.py create mode 100644 tutornotes/__init__.py create mode 100644 tutornotes/patches/common-env-features create mode 100644 tutornotes/patches/https-create create mode 100644 tutornotes/patches/k8s-deployments create mode 100644 tutornotes/patches/k8s-ingress-certificates create mode 100644 tutornotes/patches/k8s-ingress-rules create mode 100644 tutornotes/patches/k8s-ingress-tls-hosts create mode 100644 tutornotes/patches/k8s-services create mode 100644 tutornotes/patches/kustomization-configmapgenerator create mode 100644 tutornotes/patches/lms-env create mode 100644 tutornotes/patches/local-docker-compose-services create mode 100644 tutornotes/patches/nginx-extra create mode 100644 tutornotes/patches/proxy-apache create mode 100644 tutornotes/patches/proxy-nginx create mode 100644 tutornotes/plugin.py create mode 100644 tutornotes/templates/notes/apps/settings/tutor.py create mode 100644 tutornotes/templates/notes/build/notes/Dockerfile create mode 100644 tutornotes/templates/notes/hooks/lms/init create mode 100644 tutornotes/templates/notes/hooks/mysql-client/init create mode 100644 tutornotes/templates/notes/hooks/notes/init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6a874f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.*.swp +!.gitignore +TODO +__pycache__ +*.egg-info/ +/build/ +/dist/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c4e919b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +build:image: + script: + - apk add --no-cache docker + - python setup.py install + - tutor plugins enable notes + - tutor config save + - tutor images build notes + only: + refs: + - master + tags: + - private + stage: build + +deploy:image: + script: + - apk add --no-cache docker + - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" + - python setup.py install + - tutor plugins enable notes + - tutor config save + - tutor images push notes + only: + refs: + - master + tags: + - private + stage: deploy + +deploy:pypi: + script: + - pip3 install -U setuptools twine + - python3 setup.py sdist + - twine upload --skip-existing dist/tutor-notes*.tar.gz + only: + refs: + - master + tags: + - private + stage: deploy diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a92f775 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include tutornotes/patches * +recursive-include tutornotes/templates * \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1d371d2 --- /dev/null +++ b/README.rst @@ -0,0 +1,37 @@ +Students notes plugin for `Tutor `_ +=================================================================== + +This is a plugin for `Tutor `_ to easily add the `Open edX note-taking app `_ to an Open edX platform. This app allows students to annotate portions of the courseware (see `the official documentation `_). + +.. image:: https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-ironwood.master/_images/SFD_SN_bodyexample.png + :alt: Notes in action + +Installation +------------ + +The plugin is currently bundled with the `binary releases of Tutor `_. If you have installed Tutor from source, you will have to install this plugin from source, too:: + + pip install tutor-notes + +Then, to enable this plugin, run:: + + tutor plugins enable notes + + +You should beware that the ``notes.`` domain name should exist and point to your server. For instance, if your LMS is hosted at http://myopenedx.com, the notes service should be found at http://notes.myopenedx.com. + +If you would like to host the notes service at a different domain name, you can set the ``NOTES_HOST`` configuration variable (see below). In particular, in development you should set this configuration variable to ``notes.localhost`` in order to be able to access the notes service from the LMS. Otherwise you will get a "Sorry, we could not search the store for annotations" error. + + +Configuration +------------- + +- ``NOTES_MYSQL_PASSWORD`` (default: ``"{{ 8|random_string }}"``) +- ``NOTES_SECRET_KEY`` (default: ``"{{ 24|random_string }}"``) +- ``NOTES_OAUTH2_SECRET`` (default: ``"{{ 24|random_string }}"``) +- ``NOTES_DOCKER_IMAGE`` (default: ``"overhangio/openedx-notes:{{ TUTOR_VERSION }}"``) +- ``NOTES_HOST`` (default: ``"notes.{{ LMS_HOST }}"``) +- ``NOTES_MYSQL_DATABASE`` (default: ``"notes"``) +- ``NOTES_MYSQL_USERNAME`` (default: ``"notes"``) + +These values can be modified with ``tutor config save --set PARAM_NAME=VALUE`` commands. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..673099f --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +import io +import os +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) + +with io.open(os.path.join(here, "README.rst"), "rt", encoding="utf8") as f: + readme = f.read() + +about = {} +with io.open( + os.path.join(here, "tutornotes", "__about__.py"), "rt", encoding="utf-8" +) as f: + exec(f.read(), about) + +setup( + name="tutor-notes", + version=about["__version__"], + url="https://docs.tutor.overhang.io/", + project_urls={ + "Documentation": "https://docs.tutor.overhang.io/", + "Code": "https://github.com/overhangio/tutor/tree/master/plugins/notes", + "Issue tracker": "https://github.com/overhangio/tutor/issues", + "Community": "https://discuss.overhang.io", + }, + license="AGPLv3", + author="Overhang.io", + author_email="contact@overhang.io", + description="A Tutor plugin for student notes", + long_description=readme, + packages=find_packages(exclude=["tests*"]), + include_package_data=True, + python_requires=">=3.5", + install_requires=["tutor-openedx"], + entry_points={"tutor.plugin.v0": ["notes = tutornotes.plugin"]}, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], +) diff --git a/tutornotes/__about__.py b/tutornotes/__about__.py new file mode 100644 index 0000000..d1f2e39 --- /dev/null +++ b/tutornotes/__about__.py @@ -0,0 +1 @@ +__version__ = "0.1.1" \ No newline at end of file diff --git a/tutornotes/__init__.py b/tutornotes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tutornotes/patches/common-env-features b/tutornotes/patches/common-env-features new file mode 100644 index 0000000..789821f --- /dev/null +++ b/tutornotes/patches/common-env-features @@ -0,0 +1 @@ +"ENABLE_EDXNOTES": true \ No newline at end of file diff --git a/tutornotes/patches/https-create b/tutornotes/patches/https-create new file mode 100644 index 0000000..d6d4930 --- /dev/null +++ b/tutornotes/patches/https-create @@ -0,0 +1 @@ +certbot certonly --standalone -n --agree-tos -m admin@{{ LMS_HOST }} -d {{ NOTES_HOST }} \ No newline at end of file diff --git a/tutornotes/patches/k8s-deployments b/tutornotes/patches/k8s-deployments new file mode 100644 index 0000000..6ae25f8 --- /dev/null +++ b/tutornotes/patches/k8s-deployments @@ -0,0 +1,32 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notes + labels: + app.kubernetes.io/name: notes +spec: + selector: + matchLabels: + app.kubernetes.io/name: notes + template: + metadata: + labels: + app.kubernetes.io/name: notes + spec: + containers: + - name: notes + image: {{ DOCKER_REGISTRY }}{{ NOTES_DOCKER_IMAGE }} + ports: + - containerPort: 8000 + env: + - name: DJANGO_SETTINGS_MODULE + value: notesserver.settings.tutor + volumeMounts: + - mountPath: /openedx/edx-notes-api/notesserver/settings/tutor.py + name: settings + subPath: tutor.py + volumes: + - name: settings + configMap: + name: notes-settings diff --git a/tutornotes/patches/k8s-ingress-certificates b/tutornotes/patches/k8s-ingress-certificates new file mode 100644 index 0000000..4279f19 --- /dev/null +++ b/tutornotes/patches/k8s-ingress-certificates @@ -0,0 +1,18 @@ +--- +apiVersion: certmanager.k8s.io/v1alpha1 +kind: Certificate +metadata: + name: {{ NOTES_HOST|replace(".", "-") }} +spec: + secretName: {{ NOTES_HOST }}-tls + issuerRef: + name: letsencrypt + commonName: {{ NOTES_HOST }} + dnsNames: + - {{ NOTES_HOST }} + acme: + config: + - http01: + ingress: web + domains: + - {{ NOTES_HOST }} \ No newline at end of file diff --git a/tutornotes/patches/k8s-ingress-rules b/tutornotes/patches/k8s-ingress-rules new file mode 100644 index 0000000..4d3c6a4 --- /dev/null +++ b/tutornotes/patches/k8s-ingress-rules @@ -0,0 +1,6 @@ +- host: {{ NOTES_HOST }} + http: + paths: + - backend: + serviceName: nginx + servicePort: {% if ACTIVATE_HTTPS %}443{% else %}80{% endif %} \ No newline at end of file diff --git a/tutornotes/patches/k8s-ingress-tls-hosts b/tutornotes/patches/k8s-ingress-tls-hosts new file mode 100644 index 0000000..33100be --- /dev/null +++ b/tutornotes/patches/k8s-ingress-tls-hosts @@ -0,0 +1 @@ +- {{ NOTES_HOST }} \ No newline at end of file diff --git a/tutornotes/patches/k8s-services b/tutornotes/patches/k8s-services new file mode 100644 index 0000000..2f9e6c6 --- /dev/null +++ b/tutornotes/patches/k8s-services @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: notes +spec: + type: NodePort + ports: + - port: 8000 + protocol: TCP + selector: + app.kubernetes.io/name: notes diff --git a/tutornotes/patches/kustomization-configmapgenerator b/tutornotes/patches/kustomization-configmapgenerator new file mode 100644 index 0000000..f8cc39f --- /dev/null +++ b/tutornotes/patches/kustomization-configmapgenerator @@ -0,0 +1,3 @@ +- name: notes-settings + files: + - plugins/notes/apps/settings/tutor.py \ No newline at end of file diff --git a/tutornotes/patches/lms-env b/tutornotes/patches/lms-env new file mode 100644 index 0000000..227ad0a --- /dev/null +++ b/tutornotes/patches/lms-env @@ -0,0 +1,2 @@ +"EDXNOTES_PUBLIC_API": "{{ "https" if ACTIVATE_HTTPS else "http" }}://{{ NOTES_HOST }}/api/v1", +"EDXNOTES_INTERNAL_API": "http://notes:8000/api/v1" \ No newline at end of file diff --git a/tutornotes/patches/local-docker-compose-services b/tutornotes/patches/local-docker-compose-services new file mode 100644 index 0000000..6ed39ae --- /dev/null +++ b/tutornotes/patches/local-docker-compose-services @@ -0,0 +1,11 @@ +############# Notes: backend store for edX Student Notes +notes: + image: {{ DOCKER_REGISTRY }}{{ NOTES_DOCKER_IMAGE }} + environment: + DJANGO_SETTINGS_MODULE: notesserver.settings.tutor + volumes: + - ../plugins/notes/apps/settings/tutor.py:/openedx/edx-notes-api/notesserver/settings/tutor.py + - ../../data/notes:/openedx/data + restart: unless-stopped + {% if ACTIVATE_MYSQL %}depends_on: + - mysql{% endif %} \ No newline at end of file diff --git a/tutornotes/patches/nginx-extra b/tutornotes/patches/nginx-extra new file mode 100644 index 0000000..3499ba5 --- /dev/null +++ b/tutornotes/patches/nginx-extra @@ -0,0 +1,37 @@ +### Student notes service +upstream notes-backend { + server notes:8000 fail_timeout=0; +} + +{% if ACTIVATE_HTTPS %} +server { + server_name {{ NOTES_HOST }}; + listen 80; + return 301 https://$server_name$request_uri; +} +{% endif %} + +server { + {% if ACTIVATE_HTTPS %}listen 443 {{ "" if WEB_PROXY else "ssl" }};{% else %}listen 80;{% endif %} + server_name notes.localhost {{ NOTES_HOST }}; + + {% if ACTIVATE_HTTPS and not WEB_PROXY %} + ssl_certificate /etc/letsencrypt/live/{{ NOTES_HOST }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ NOTES_HOST }}/privkey.pem; + {% endif %} + + # Disables server version feedback on pages and in headers + server_tokens off; + + location / { + {% if not WEB_PROXY %} + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-For $remote_addr; + {% endif %} + proxy_set_header Host $http_host; + proxy_redirect off; + + proxy_pass http://notes-backend; + } +} \ No newline at end of file diff --git a/tutornotes/patches/proxy-apache b/tutornotes/patches/proxy-apache new file mode 100644 index 0000000..083ea89 --- /dev/null +++ b/tutornotes/patches/proxy-apache @@ -0,0 +1,27 @@ +{% if ACTIVATE_HTTPS %} + + ServerName {{ NOTES_HOST }} + Redirect / https://notes.{{ LMS_HOST }}/ + + + + ServerName {{ NOTES_HOST }} + SSLEngine on + SSLCertificateFile /etc/letsencrypt/live/{{ NOTES_HOST }}/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/{{ NOTES_HOST }}/privkey.pem + + ProxyPreserveHost On + ProxyRequests On + ProxyPass / http://localhost:{{ NGINX_HTTP_PORT }}/ + ProxyPassReverse / http://localhost:{{ NGINX_HTTP_PORT }}/ + +{% else %} + + ServerName {{ NOTES_HOST }} + + ProxyPreserveHost On + ProxyRequests On + ProxyPass / http://localhost:{{ NGINX_HTTP_PORT }}/ + ProxyPassReverse / http://localhost:{{ NGINX_HTTP_PORT }}/ + +{% endif %} diff --git a/tutornotes/patches/proxy-nginx b/tutornotes/patches/proxy-nginx new file mode 100644 index 0000000..bd1ed11 --- /dev/null +++ b/tutornotes/patches/proxy-nginx @@ -0,0 +1,33 @@ +server { + listen 80; + server_name {{ NOTES_HOST }}; + + server_tokens off; + location / { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://localhost:{{ NGINX_HTTP_PORT }}; + } +} +{% if ACTIVATE_HTTPS %} +server { + listen 443 ssl; + server_name {{ NOTES_HOST }}; + + ssl_certificate /etc/letsencrypt/live/{{ NOTES_HOST }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ NOTES_HOST }}/privkey.pem; + + server_tokens off; + location / { + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://localhost:{{ NGINX_HTTPS_PORT }}; + } +} +{% endif %} \ No newline at end of file diff --git a/tutornotes/plugin.py b/tutornotes/plugin.py new file mode 100644 index 0000000..18c5576 --- /dev/null +++ b/tutornotes/plugin.py @@ -0,0 +1,41 @@ +from glob import glob +import os + +import pkg_resources + +from .__about__ import __version__ + + +config = { + "add": { + "MYSQL_PASSWORD": "{{ 8|random_string }}", + "SECRET_KEY": "{{ 24|random_string }}", + "OAUTH2_SECRET": "{{ 24|random_string }}", + }, + "defaults": { + "VERSION": __version__, + "DOCKER_IMAGE": "overhangio/openedx-notes:{{ NOTES_VERSION }}", + "HOST": "notes.{{ LMS_HOST }}", + "MYSQL_DATABASE": "notes", + "MYSQL_USERNAME": "notes", + }, +} + +templates = pkg_resources.resource_filename("tutornotes", "templates") +hooks = { + "init": ["mysql-client", "lms", "notes"], + "build-image": {"notes": "{{ NOTES_DOCKER_IMAGE }}"}, + "remote-image": {"notes": "{{ NOTES_DOCKER_IMAGE }}"}, +} + + +def patches(): + all_patches = {} + for path in glob( + os.path.join(pkg_resources.resource_filename("tutornotes", "patches"), "*") + ): + with open(path) as patch_file: + name = os.path.basename(path) + content = patch_file.read() + all_patches[name] = content + return all_patches diff --git a/tutornotes/templates/notes/apps/settings/tutor.py b/tutornotes/templates/notes/apps/settings/tutor.py new file mode 100644 index 0000000..dbb7be6 --- /dev/null +++ b/tutornotes/templates/notes/apps/settings/tutor.py @@ -0,0 +1,33 @@ +from .common import * + +SECRET_KEY = "{{ NOTES_SECRET_KEY }}" +ALLOWED_HOSTS = [ + "localhost", + "notes", + "notes.localhost", + "{{ NOTES_HOST }}", +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "HOST": "{{ MYSQL_HOST }}", + "PORT": {{MYSQL_PORT}}, + "NAME": "{{ NOTES_MYSQL_DATABASE }}", + "USER": "{{ NOTES_MYSQL_USERNAME }}", + "PASSWORD": "{{ NOTES_MYSQL_PASSWORD }}", + } +} + +CLIENT_ID = "notes" +CLIENT_SECRET = "{{ NOTES_OAUTH2_SECRET }}" + +HAYSTACK_CONNECTIONS = { + "default": { + "ENGINE": "notesserver.highlight.ElasticsearchSearchEngine", + "URL": "http://{{ ELASTICSEARCH_HOST }}:{{ ELASTICSEARCH_PORT }}/", + "INDEX_NAME": "notes", + } +} + +LOGGING["handlers"]["local"] = LOGGING["handlers"]["console"].copy() diff --git a/tutornotes/templates/notes/build/notes/Dockerfile b/tutornotes/templates/notes/build/notes/Dockerfile new file mode 100644 index 0000000..534c812 --- /dev/null +++ b/tutornotes/templates/notes/build/notes/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:18.04 +MAINTAINER Overhang.io + +RUN apt update && \ + apt upgrade -y && \ + apt install -y language-pack-en git python-pip libmysqlclient-dev + +RUN mkdir /openedx +RUN git clone https://github.com/edx/edx-notes-api --branch open-release/ironwood.2 --depth 1 /openedx/edx-notes-api +WORKDIR /openedx/edx-notes-api + +RUN pip install -r requirements/base.txt + +EXPOSE 8000 +CMD gunicorn --workers=2 --name notes --bind=0.0.0.0:8000 --max-requests=1000 notesserver.wsgi:application diff --git a/tutornotes/templates/notes/hooks/lms/init b/tutornotes/templates/notes/hooks/lms/init new file mode 100644 index 0000000..90fb261 --- /dev/null +++ b/tutornotes/templates/notes/hooks/lms/init @@ -0,0 +1,18 @@ +export DJANGO_SETTINGS_MODULE=lms.envs.tutor.production + +# Modify users created an incorrect email and that might clash with the newly created users +./manage.py lms shell -c \ + "from django.contrib.auth import get_user_model;\ + get_user_model().objects.filter(username='notes').exclude(email='notes@openedx').update(email='notes@openedx')" + +./manage.py lms manage_user notes notes@openedx --staff --superuser +./manage.py lms create_oauth2_client \ + "http://notes:8000" \ + "http://notes:8000/complete/edx-oidc/" \ + confidential \ + --client_name edx-notes \ + --client_id notes \ + --client_secret {{ NOTES_OAUTH2_SECRET }} \ + --trusted \ + --logout_uri "http://notes:8000/logout/" \ + --username notes \ No newline at end of file diff --git a/tutornotes/templates/notes/hooks/mysql-client/init b/tutornotes/templates/notes/hooks/mysql-client/init new file mode 100644 index 0000000..55fb49e --- /dev/null +++ b/tutornotes/templates/notes/hooks/mysql-client/init @@ -0,0 +1,2 @@ +mysql -u root --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e 'CREATE DATABASE IF NOT EXISTS {{ NOTES_MYSQL_DATABASE }};' +mysql -u root --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e 'GRANT ALL ON {{ NOTES_MYSQL_DATABASE }}.* TO "{{ NOTES_MYSQL_USERNAME }}"@"%" IDENTIFIED BY "{{ NOTES_MYSQL_PASSWORD }}";' \ No newline at end of file diff --git a/tutornotes/templates/notes/hooks/notes/init b/tutornotes/templates/notes/hooks/notes/init new file mode 100644 index 0000000..6647646 --- /dev/null +++ b/tutornotes/templates/notes/hooks/notes/init @@ -0,0 +1 @@ +./manage.py migrate --settings=notesserver.settings.tutor