Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix docker user for development #279

Merged
merged 3 commits into from
Jul 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
# Docker
# Richie's Makefile
#
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ DISCLAIMER /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\
#
# This Makefile is only meant to be used for DEVELOPMENT purpose as we are
# changing the user id that will run in the container.
#
# PLEASE DO NOT USE IT FOR YOUR CI/PRODUCTION/WHATEVER...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it what we want for the CI and production as well (as opposed to running as root)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We surely do not want to (shouldn't?) run with the root user in most cases, you are right.

Remember that only the dev flavor of Richie's image runs with the root user (we use the UID=10000 in "production"), and, we use the entrypoint trick to allow running commands in Richie containers whatever the container user will be. This allows us to map local user ID with container user ID and mount local volumes without falling into file permission issues.

In the CI, we do not want to mount volumes as we want to test our newly built docker image. Hence, if we run as a non-root user, we will fall into file permission issues in the container itself (tests, coverage, etc. will try to create files in /app, a directory that belongs to the root user). One way to solve this would be to change the ownership of the /app directory from root to a lambda user. But I think it's not a good practice: I prefer having a running user that has no write permissions at all in the container.

To make a long story short, I think running tests with the root user in the CI is a lesser evil than allowing the container running user to write in the application's directory.

WDYT?

Copy link
Contributor

@sampaccoud sampaccoud Jul 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that you should run the CI in conditions as close as possible to the production.
If you run as root and your app writes inside the container to places where it won't be allowed to write in production, then your tests could miss something big...
So maybe we should make the same mounts as in production and run as an unpriviledged user to precisely check that our container works in "readonly" mode.

On more way to look at it is that this seemingly small difference is the whole reason why we have to modify images in https://www.github.com/openshift-docker. The original images from which we build in this repo would pass your CI with a root user but would fail to work in production.

This being said, maybe we can decide that what we are testing here is the code and that the images will be tested through infrastructure tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that you should run the CI in conditions as close as possible to the production.

True.

If you run as root and your app writes inside the container to places where it won't be allowed to write in production, then your tests could miss something big...

Also true. Moreover the dev flavor of the docker image that is used for tests has many additional dependencies installed; potentially changing the app behaviour.

So maybe we should make the same mounts as in production and run as an unpriviledged user to precisely check that our container works in "readonly" mode.

Good idea. But, I was talking about mounting the code as volume for tests. This is not what we want.

This being said, maybe we can decide that what we are testing here is the code and that the images will be tested through infrastructure tests.

That would be an interesting approach.

Conclusion: we need to switch to a random user also for tests (read-only mode); I hope we will be able to configure our (test|lint)-suite to fit with this requirement. I'll declare a new issue for this.

#
# /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\ /!\

# -- Docker

# Get the current user ID to use for docker run and docker exec commands
UID = $(shell id -u)
COMPOSE = docker-compose
COMPOSE_RUN = $(COMPOSE) run --rm
COMPOSE_EXEC = $(COMPOSE) exec
COMPOSE_RUN = $(COMPOSE) run --rm --user=$(UID)
COMPOSE_EXEC = $(COMPOSE) exec --user=$(UID)
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app
COMPOSE_EXEC_NODE = $(COMPOSE_EXEC) --user="$(id -u):$(id -g)" node
COMPOSE_EXEC_NODE = $(COMPOSE_EXEC) node
COMPOSE_RUN_APP = $(COMPOSE_RUN) app
COMPOSE_RUN_NODE = $(COMPOSE_RUN) --user="$(id -u):$(id -g)" node
COMPOSE_RUN_NODE = $(COMPOSE_RUN) node
COMPOSE_TEST = $(COMPOSE) -p richie-test -f docker/compose/test/docker-compose.yml --project-directory .
COMPOSE_TEST_RUN = $(COMPOSE_TEST) run --rm
COMPOSE_TEST_RUN = $(COMPOSE_TEST) run --rm --user=$(UID)
COMPOSE_TEST_RUN_APP = $(COMPOSE_TEST_RUN) app

# Node
# -- Node

YARN = $(COMPOSE_RUN_NODE) yarn

# Django
# -- Django

MANAGE = $(COMPOSE_RUN_APP) python sandbox/manage.py

# -- Rules

default: help

bootstrap: ## install development dependencies
Expand Down
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Among the features that `Richie` offers out of the box:
- flexible custom pages for courses, organizations, subjects, teachers (and their inter-relations),
- Extensible with any third-party `DjangoCMS` plugin or any third-party `Django` application.


## Architecture

`Richie` is a **container-native application** but can also be installed
Expand All @@ -47,7 +46,6 @@ We provide alternative `Docker Compose` configurations for test, CI and producti
application can also be run with your favorite orchestrator. At "France Université Numérique", we
deploy our applications on `OpenShift` using [`Arnold`](https://github.com/openfun/arnold).


## Getting started

First, make sure you have a recent version of Docker and
Expand Down Expand Up @@ -91,7 +89,6 @@ Note that if you don't create the demo site and start from a blank CMS, you will
requesting you to create some required root pages. So it is easier as a first approach to test the
CMS with the demo site.


## Contributing

This project is intended to be community-driven, so please, do not hesitate to get in touch if you
Expand All @@ -100,7 +97,6 @@ have any question related to our implementation or design decisions.
We try to raise our code quality standards and expect contributors to follow the recommandations
from our [handbook](https://openfun.gitbooks.io/handbook/content).


### Checking your code

We use strict flake8, pylint, isort and black linters to check the validity of our backend code:
Expand All @@ -111,7 +107,6 @@ We use strict tslint and prettier to check the validity of our frontend code:

$ make lint-front


### Running tests

On the backend, we use pytest to run our test suite:
Expand All @@ -122,7 +117,6 @@ On the frontend, we use karma to run our test suite:

$ make test-front


### Running migrations

The first time you start the project with `make bootstrap`, the `db` container automatically
Expand All @@ -131,23 +125,32 @@ creates a fresh database named `richie` and performs database migrations. Each t

$ make migrate


### Handling new dependencies

Each time you add new front-end or back-end dependencies, you will need to rebuild the
application. We recommend to use:

$ make bootstrap


### To go further

To see all available commands, run:

$ make

See our [tips and tricks](./docs/docker_development.md) for development with Docker
We also provide shortcuts for docker-compose commands as sugar scripts in the
`bin/` directory:

```
bin
├── exec
├── pylint
├── pytest
└── run
```

More details and tips & tricks can be found in our [development with Docker
documentation](./docs/docker_development.md)

## License

Expand Down
84 changes: 84 additions & 0 deletions bin/_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash

set -eo pipefail

REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
UNSET_USER=0
COMPOSE_FILE="$REPO_DIR/docker-compose.yml"
COMPOSE_PROJECT="richie"

# _set_user: set (or unset) default user id used to run docker commands
#
# usage: _set_user
#
# You can override default user ID (the current host user ID), by defining the
# USER_ID environment variable.
#
# To avoid running docker commands with a custom user, please set the
# $UNSET_USER environment variable to 1.
function _set_user() {

if [ $UNSET_USER -eq 1 ]; then
USER_ID=""
return
fi

# USER_ID = USER_ID or `id -u` if USER_ID is not set
USER_ID=${USER_ID:-$(id -u)}

echo "🙋(user) ID: ${USER_ID}"
}

# docker_compose: wrap docker-compose command
#
# usage: docker_compose [options] [ARGS...]
#
# options: docker-compose command options
# ARGS : docker-compose command arguments
function _docker_compose() {

echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
docker-compose \
-p "${COMPOSE_PROJECT}" \
-f "${COMPOSE_FILE}" \
--project-directory "${REPO_DIR}" \
"$@"
}

# _dc_run: wrap docker-compose run command
#
# usage: _dc_run [options] [ARGS...]
#
# options: docker-compose run command options
# ARGS : docker-compose run command arguments
function _dc_run() {
_set_user

echo "🐳(compose) run command: '$@'"

user_args="--user=$USER_ID"
if [ -z $USER_ID ]; then
user_args=""
fi

_docker_compose run --rm $user_args "$@"
}

# _dc_exec: wrap docker-compose exec command
#
# usage: _dc_exec [options] [ARGS...]
#
# options: docker-compose exec command options
# ARGS : docker-compose exec command arguments
function _dc_exec() {
_set_user

echo "🐳(compose) exec command: '$@'"

user_args="--user=$USER_ID"
if [ -z $USER_ID ]; then
user_args=""
fi

_docker_compose exec $user_args "$@"
}
3 changes: 3 additions & 0 deletions bin/entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
# docker run --rm --env-file env.d/production richie:latest python sandbox/manage.py migrate
#

echo "🐳(entrypoint) creating user running in the container..."
if ! whoami &> /dev/null; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd
fi
fi

echo "🐳(entrypoint) running your command: $@"
exec "$@"
5 changes: 5 additions & 0 deletions bin/exec
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"

_dc_exec "$@"
11 changes: 6 additions & 5 deletions bin/pylint
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash

docker-compose \
-p richie-test \
-f docker/compose/test/docker-compose.yml \
--project-directory . \
run --rm --no-deps app pylint "$@"
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"

COMPOSE_FILE="$REPO_DIR/docker/compose/test/docker-compose.yml"
COMPOSE_PROJECT="richie-test"

_dc_run --no-deps app pylint "$@"
11 changes: 6 additions & 5 deletions bin/pytest
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash

docker-compose \
-p richie-test \
-f docker/compose/test/docker-compose.yml \
--project-directory . \
run --rm app pytest "$@"
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"

COMPOSE_FILE="$REPO_DIR/docker/compose/test/docker-compose.yml"
COMPOSE_PROJECT="richie-test"

_dc_run app pytest "$@"
5 changes: 5 additions & 0 deletions bin/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"

_dc_run "$@"
65 changes: 42 additions & 23 deletions docs/docker_development.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

### Settings

Settings are defined using [Django Configuration]
(https://django-configurations.readthedocs.io/en/stable/) for different environments:

- Development: settings for development on developpers' local environment,
- Test: settings used to run our test suite,
- ContinousIntegration: settings used on the continuous integration platform,
- Feature: settings for deployment of each developers' feature branches,
- Staging: settings for deployment to the staging environment,
- PreProduction: settings for deployment to the pre-production environment,
- Production: settings for deployment to the production environment.
Settings are defined using [Django
Configurations](https://django-configurations.readthedocs.io/en/stable/) for
different environments:

- `Development`: settings for development on developers' local environment,
- `Test`: settings used to run our test suite,
- `ContinousIntegration`: settings used on the continuous integration platform,
- `Feature`: settings for deployment of each developers' feature branches,
- `Staging`: settings for deployment to the staging environment,
- `PreProduction`: settings for deployment to the pre-production environment,
- `Production`: settings for deployment to the production environment.

The `Development` environment is defined as the default environment.


### Front-end tools

If you intend to work on the front-end development of the CMS, we also have sweet candies for you! 🤓
If you intend to work on the front-end development of the CMS, we also have
sweet candies for you! 🤓

```bash
# Start the Sass watcher
Expand All @@ -28,7 +29,6 @@ $ make watch-css
$ make watch-ts
```


### Container control

You can stop/start/restart a container:
Expand All @@ -39,7 +39,6 @@ or stop/start/restart all containers in one command:

$ docker-compose [stop|start|restart]


### Debugging

You can easily see the latest logs for a container:
Expand All @@ -48,27 +47,47 @@ You can easily see the latest logs for a container:

Or follow the stream of logs:

$ docker logs --follow --until=2s
$ docker-compose logs --follow [app|db|elasticsearch]

If you need to debug a running container, you can open a Linux shell with the `exec` command:
If you need to debug a running container, you can open a Linux shell with the
`docker-compose exec` command (we use a sugar script here, see next section):

$ docker-compose exec [app|db|nginx] bash
$ bin/exec [app|db|nginx] bash

While developing on `Richie`, you will also need to run a `Django shell` and it has to be done in
the `app` container:
While developing on `Richie`, you will also need to run a `Django shell` and it
has to be done in the `app` container (we use a sugar script here, see next
section):

$ docker-compose run --rm app python sandbox/manage.py shell
$ bin/run app python sandbox/manage.py shell

### Using sugar scripts

While developing using Docker, you will fall into permission issues if you mount
the code directory as a volume in the container. Indeed, the Docker engine will,
by default, run the containers using the `root` user. Any file created or
updated by the app container on your host, as a result of the volume mounts,
will be owned by the local root user. One way to solve this is to use the
`--user="$(id -u)"` flag when calling the `docker-compose run` or
`docker-compose exec` commands. By using the user flag trick, the running
container user ID will match your local user ID. But, as it's repetitive and
error-prone, we provide shortcuts that we call our "sugar scripts":

- `bin/run`: is a shortcut for `docker-compose run --rm --user="$(id -u)"`
- `bin/exec`: is a shortcut for `docker-compose exec --user="$(id -u)"`
- `bin/pylint`: runs `pylint` in the `app` service using the test docker-compose
file
- `bin/pytest`: runs `pytest` in the `app` service using the test docker-compose
file

### Cleanup

If you work on the Docker configuration and make repeated modifications, remember to periodically
clean the unused docker images and containers by running:
If you work on the Docker configuration and make repeated modifications,
remember to periodically clean the unused docker images and containers by
running:

$ docker image prune
$ docker container prune


### Troubleshooting

#### ElasticSearch service is always down
Expand Down