diff --git a/.dockercfg.template b/.dockercfg.template new file mode 100644 index 0000000..a578df2 --- /dev/null +++ b/.dockercfg.template @@ -0,0 +1 @@ +{"":{"auth":"","email":""}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92f3c73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/* +*.pyc diff --git a/Dockerfile b/Dockerfile index 25a2630..bb55724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,12 @@ -FROM python:2.7 - -MAINTAINER "Justyna Ilczuk" - -ENV LAST_REFRESHED 2015-06-02 - -RUN apt-get update && apt-get install -qqy git libffi-dev libssl-dev - -RUN pip install git+https://github.com/syncano/syncano-python@master - -ENV export SYNCANO_APIROOT='https://api.syncano.io/' - -COPY requirements.txt /tmp/requirements.txt -RUN pip install -r /tmp/requirements.txt - -RUN chmod 1777 /tmp -# create a special user to run code -# user without root privileges greatly improves security -RUN groupadd -r syncano && useradd -r -g syncano syncano -USER syncano - +FROM ubuntu:trusty +MAINTAINER "Syncano DevOps Team" + +RUN apt-get update && apt-get install -y openssh-server +RUN mkdir /var/run/sshd /root/.ssh +COPY image/id_rsa.pub /root/.ssh/authorized_keys +RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.0.3/dumb-init_1.0.3_amd64.deb +RUN dpkg -i dumb-init_*.deb +EXPOSE 22 + +WORKDIR /tmp +CMD "python" diff --git a/README.md b/README.md index 3ed5144..b9b6be7 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,36 @@ # python-codebox -This repository containes a Dockerfile for a Python 2.7 container with built in support for syncano and basic python libraries. +This repository contains Dockerfile for Python 2.7 image with built-in support for Syncano and selected Python libraries. -You can build a container yourself or pull it from docker registry. +You can build the image yourself (Ansible 2.1.0 is required): ``` -$ docker pull syncano/python-codebox -$ docker run -it syncano/python-codebox +$ ./create_python_image.sh ``` -In a container you can use syncano library for python: +or pull it from Docker registry: ``` +$ docker pull quay.io/syncano/python-codebox +``` + +Then you can run Docker container: + + +``` +$ docker run -it quay.io/syncano/python-codebox +Python 2.7.6 (default, Jun 22 2015, 17:58:13) +[GCC 4.8.2] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +In a container you can use Syncano's Python Library: + +``` +>>> import syncano >>> connection = syncano.connect(api_key='my_api_key') >>> connection.Instance.please.list() ``` -By default, syncano library will use staging version of syncano v4. -More info about the library is [here](https://github.com/Syncano/syncano-python/tree/release/4.0). +More info about the library can be found [here](https://github.com/Syncano/syncano-python/). diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..6f7c766 --- /dev/null +++ b/circle.yml @@ -0,0 +1,36 @@ +machine: + services: + - docker + +dependencies: + override: + - sudo pip install ansible==2.1.0 + - chmod 600 image/id_rsa + - ./create_python_image.sh $BUILD_LEVEL + - docker images + - docker history quay.io/syncano/python-codebox + +test: + override: + - docker run -it -v `pwd`/test.py:/tmp/test.py quay.io/syncano/python-codebox python /tmp/test.py + - docker run -it -v `pwd`/test.py:/tmp/test.py quay.io/syncano/python-codebox python27-lib4.2 /tmp/test.py + + - docker run -it -v `pwd`/test.py:/tmp/test.py quay.io/syncano/python-codebox python27-lib5.0 /tmp/test.py + - docker run -it -v `pwd`/test_v50.py:/tmp/test.py quay.io/syncano/python-codebox python /tmp/test.py + + - docker run -it -v `pwd`/test_v42.py:/tmp/test.py quay.io/syncano/python-codebox python27-lib4.2 /tmp/test.py + - docker run -it -v `pwd`/test_v50.py:/tmp/test.py quay.io/syncano/python-codebox python27-lib5.0 /tmp/test.py + + - if docker run -it quay.io/syncano/python-codebox python3 --version | grep -vq "Python 3.4"; then exit 1; fi + + - docker run -it -v `pwd`/test.py:/tmp/test.py quay.io/syncano/python-codebox python3 /tmp/test.py + - docker run -it -v `pwd`/test_v50.py:/tmp/test.py quay.io/syncano/python-codebox python3-lib5.0 /tmp/test.py + +deployment: + production: + branch: + - master + commands: + - curl -X POST $REFRESH_SCRIPT_URL + - sed -e "s||$DOCKER_REGISTRY|g" -e "s||$DOCKER_EMAIL|g" -e "s||$DOCKER_AUTH|g" < .dockercfg.template > ~/.dockercfg + - docker push quay.io/syncano/python-codebox diff --git a/create_python_image.sh b/create_python_image.sh new file mode 100755 index 0000000..e66953b --- /dev/null +++ b/create_python_image.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ ${1:-full} != "--partial" ] +then + docker build -t quay.io/syncano/python-codebox . +fi + +docker run -d -p 2200:22 --env-file image/environment --name ansible quay.io/syncano/python-codebox dumb-init /usr/sbin/sshd -D +ansible-playbook -i "`docker inspect --format '{{ .NetworkSettings.IPAddress }}' ansible`," -u root --private-key image/id_rsa provision-python-codebox.yml +docker commit ansible quay.io/syncano/python-codebox + +docker run -d --env-file image/environment -u syncano --name python quay.io/syncano/python-codebox dumb-init /usr/sbin/sshd -D +docker commit python quay.io/syncano/python-codebox diff --git a/image/environment b/image/environment new file mode 100644 index 0000000..7228805 --- /dev/null +++ b/image/environment @@ -0,0 +1 @@ +SYNCANO_APIROOT=https://api.syncano.io/ diff --git a/image/id_rsa b/image/id_rsa new file mode 100644 index 0000000..1f1b49a --- /dev/null +++ b/image/id_rsa @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/image/id_rsa.pub b/image/id_rsa.pub new file mode 100644 index 0000000..11354bd --- /dev/null +++ b/image/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCp1U/dH3t7HSaJ0+cIWTJr8BZkb600OMeUdqtPARaN6fFh8m54lOlQLEIVWiaFVBXB0dD16B3LLoqWzELt+E8o2QuSS/6sI2TwWfibc2OYi/l4Y39jGmiPGM2K9/ErQ9pMH0WudmYlA1b+8s+6r3aCXY540d/AjHoHj/ILFC03WFN8Dl/oMpaisPzvhnfanAmIkSc3KBBegd9dl2/j6mZs4sApiMozA2b56nL4IPSvCsKGb8pv2FxX2b1nmzSpblThyo8Gq3dBmCErCyPWDSthBzSkcWDnQ29Y5B48ncYLGy1V/Lm4CTschCPXnGdxvrzFaYVpkstg/Et6Dgsx0SxPB6PGmhvWnW+0IYsgtUVOKXFAI7F8kJFdo0M9AzDEdHJNK8nlfom9eW+zttawrclQaebNJriz0H83jj86s2MtbfsBXHuPsGjUZsGio18BhncZi8p8FMhxnl9JTAfkDuYNI8OAE8vJQ4lSDjKZmnIqKVkBIhVOn3B4ZTQEygecgUFqD8l3rpj3yMJaOke8IYpBQP14gVRztNHC2IjxVUPf4WLphz1LyUIHU7kSIsPiiz8OarX4X9iIfNNac8SlXG5nD6/LPNC/OCuh60creRYbCf2IMHYAEqKfUYGOtClZzSwflq7o1fR2kMvVF9pcdGT+b3qSvpktMF4D1nqDCytMgw== syncano diff --git a/provision-python-codebox.yml b/provision-python-codebox.yml new file mode 100644 index 0000000..62fdb24 --- /dev/null +++ b/provision-python-codebox.yml @@ -0,0 +1,5 @@ +--- +- hosts: all + connection: ssh + roles: + - python diff --git a/python/files/external_requirements.txt b/python/files/external_requirements.txt new file mode 100644 index 0000000..8788358 --- /dev/null +++ b/python/files/external_requirements.txt @@ -0,0 +1,2 @@ +-i https://pypi.fury.io/Nx-poGuiakLbsCzaLwNm/aexol +aexol==0.1.5 diff --git a/python/files/pkglist b/python/files/pkglist new file mode 100644 index 0000000..7c1c2ee --- /dev/null +++ b/python/files/pkglist @@ -0,0 +1,15 @@ +git +libffi-dev +libssl-dev +libjpeg-dev +python-dev +python3-dev +python-tk +python3-tk +python-numpy +python3-numpy +python-pip +python3-pip +python-scipy +python3-scipy +wget diff --git a/python/files/requirements.txt b/python/files/requirements.txt new file mode 100644 index 0000000..0770e7f --- /dev/null +++ b/python/files/requirements.txt @@ -0,0 +1,37 @@ +analytics-python==1.0.3 +BeautifulSoup==3.2.1 +boto3==1.2.5 +braintree==3.20.0 +cryptography==2.0.3 +fastly==0.0.2 +future==0.15.2 +GitPython==2.0.5 +google-api-python-client==1.5.2 +googleads==4.3.0 +html2text==2015.2.18 +httplib2==0.9.1 +icalendar==3.8.4 +Jinja2==2.8 +Mako==1.0.4 +mixpanel-query-py==0.1.7 +mixpanel==4.3.0 +names==0.3.0 +oauth2client==1.4.11 +oauthlib==1.0.3 +osa==0.1.6.6 +pandas==0.17.1 +Pillow==2.7.0 +pkgtools==0.7.3 +python-telegram-bot==5.0.0 +pytz==2014.10 +qrcode==5.1 +requests-oauthlib==0.5.0 +requests==2.7.0 +sendgrid==3.2.10 +Sendinblue==2.0.5.1 +six==1.9.0 +syncano==5.4.6 +tweepy==3.4.0 +twilio==4.4.0 +uritemplate==0.6 +wit==4.1.0 \ No newline at end of file diff --git a/python/files/requirements_base.txt b/python/files/requirements_base.txt new file mode 100644 index 0000000..0f3f52d --- /dev/null +++ b/python/files/requirements_base.txt @@ -0,0 +1 @@ +virtualenv==13.0.3 diff --git a/python/files/requirements_python3.txt b/python/files/requirements_python3.txt new file mode 100644 index 0000000..a55b8a9 --- /dev/null +++ b/python/files/requirements_python3.txt @@ -0,0 +1,33 @@ +analytics-python==1.0.3 +beautifulsoup4==4.4.1 +boto3==1.2.5 +braintree==3.20.0 +cryptography==2.0.3 +fastly==0.0.2 +google-api-python-client==1.5.2 +googleads==3.11.0 +html2text==2015.2.18 +httplib2==0.9.1 +icalendar==3.8.4 +Jinja2==2.8 +mixpanel-query-py==0.1.7 +mixpanel==4.3.0 +names==0.3.0 +oauth2client==1.4.11 +oauthlib==1.0.3 +osa==0.1.6.6 +pandas==0.17.1 +Pillow==2.7.0 +pkgtools==0.7.3 +python-telegram-bot==5.0.0 +pytz==2014.10 +qrcode==5.1 +requests-oauthlib==0.5.0 +requests==2.7.0 +sendgrid==3.2.10 +six==1.9.0 +syncano==5.4.6 +tweepy==3.4.0 +twilio==4.4.0 +uritemplate==0.6 +wit==4.1.0 \ No newline at end of file diff --git a/python/files/requirements_v42.txt b/python/files/requirements_v42.txt new file mode 100644 index 0000000..f04ff33 --- /dev/null +++ b/python/files/requirements_v42.txt @@ -0,0 +1,27 @@ +analytics-python==1.0.3 +BeautifulSoup==3.2.1 +boto3==1.2.5 +braintree==3.20.0 +cryptography==2.0.3 +fastly==0.0.2 +google-api-python-client==1.4.1 +googleads==3.11.0 +html2text==2015.2.18 +httplib2==0.9.1 +icalendar==3.8.4 +names==0.3.0 +oauth2client==1.4.11 +oauthlib==1.0.3 +osa==0.1.6.6 +pandas==0.17.1 +Pillow==2.7.0 +pkgtools==0.7.3 +pytz==2014.10 +qrcode==5.1 +requests-oauthlib==0.5.0 +requests==2.7.0 +six==1.9.0 +syncano==4.2.0 +tweepy==3.4.0 +twilio==4.4.0 +uritemplate==0.6 \ No newline at end of file diff --git a/python/tasks/common.yml b/python/tasks/common.yml new file mode 100644 index 0000000..1f4fcb0 --- /dev/null +++ b/python/tasks/common.yml @@ -0,0 +1,38 @@ +--- +- name: Add syncano group + group: + name: syncano + gid: 1000 + state: present + +- name: Add syncano user + user: + name: syncano + group: syncano + uid: 1000 + shell: /bin/bash + home: /home/syncano + state: present + +- name: Copy apt pkgs + copy: + src: pkglist + dest: /tmp/ + +- name: List apt packages + shell: + cat /tmp/pkglist + register: pkgs + +- name: Install packages + apt: + name: "{{ item }}" + state: present + update_cache: yes + with_items: + "{{ pkgs.stdout_lines }}" + +- name: Allow public usage of /tmp + file: + name: /tmp + mode: "1777" \ No newline at end of file diff --git a/python/tasks/envs.yml b/python/tasks/envs.yml new file mode 100644 index 0000000..e35faad --- /dev/null +++ b/python/tasks/envs.yml @@ -0,0 +1,43 @@ +- name: Install Python packages in envs + pip: + virtualenv: "/home/syncano/{{ item.env }}" + virtualenv_python: "{{ item.python }}" + virtualenv_site_packages: yes + requirements: "/tmp/{{ item.requirements }}" + with_items: + - env: v4.2 + python: python2.7 + requirements: requirements_v42.txt + - env: v4.2 + python: python2.7 + requirements: external_requirements.txt + - env: v5.0 + python: python2.7 + requirements: requirements.txt + - env: v5.0 + python: python2.7 + requirements: external_requirements.txt + - env: p3v5.0 + python: python3 + requirements: requirements_python3.txt + - env: p3v5.0 + python: python3 + requirements: external_requirements.txt + +- name: Set soft links to runtimes + file: + src: "/home/syncano/{{ item.env }}/bin/python" + dest: "/usr/bin/{{ item.python }}" + state: link + force: yes + with_items: + - env: v5.0 + python: python + - env: v5.0 + python: python27-lib5.0 + - env: v4.2 + python: python27-lib4.2 + - env: p3v5.0 + python: python3 + - env: p3v5.0 + python: python3-lib5.0 diff --git a/python/tasks/main.yml b/python/tasks/main.yml new file mode 100644 index 0000000..0a75bf1 --- /dev/null +++ b/python/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- include: common.yml + +- name: Install requirements + pip: + name: " {{ item }}" + extra_args: "--upgrade" + with_items: + - pip + +- name: Copy requirements files + copy: + src: "{{ item }}" + dest: /tmp/ + with_items: + - requirements.txt + - requirements_base.txt + - requirements_v42.txt + - requirements_python3.txt + - external_requirements.txt + +- name: Install requirements + pip: + requirements: /tmp/requirements_base.txt + +- include: envs.yml diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3c78dcb..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Pillow==2.7.0 -requests==2.5.3 -icalendar==3.8.4 -pytz==2014.10 -html2text==2015.2.18 -pkgtools==0.7.3 -names==0.3.0 -httplib2==0.9.1 -twilio==4.4.0 -analytics-python==1.0.3 diff --git a/test.py b/test.py new file mode 100644 index 0000000..a994b27 --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ +if __name__ == '__main__': + import pkgutil + blacklist = ['winreg'] + modules = (name for _, name, is_pkg in pkgutil.iter_modules() + if is_pkg and name not in blacklist) + map(__import__, modules) diff --git a/test_v42.py b/test_v42.py new file mode 100644 index 0000000..64d4aad --- /dev/null +++ b/test_v42.py @@ -0,0 +1,2 @@ +for lib, version in (('numpy', '1.8.2'), ('scipy', '0.13.3'), ('syncano', '4.2.0')): + assert __import__(lib).__version__ == version diff --git a/test_v50.py b/test_v50.py new file mode 100644 index 0000000..e929731 --- /dev/null +++ b/test_v50.py @@ -0,0 +1,2 @@ +for lib, version in (('numpy', '1.8.2'), ('scipy', '0.13.3'), ('syncano', '5.4.6')): + assert __import__(lib).__version__ == version