Git Product home page Git Product logo

gdev's Introduction

gdev

What is this for?

Gdev is a helper script for all your container related development needs. It's for locally developing containerized applications.

Quick installation

Install gdev dependencies and start the development environment by running:

$ curl -fsSL https://raw.githubusercontent.com/devgeniem/gdev/master/bin/bootstrap | bash

If you're installing on a Ubuntu machine, run:

$ curl -fsSL https://raw.githubusercontent.com/devgeniem/gdev/master/bin/ubuntu | bash

When using linux and MacOS computers on a shared project and you have different docker-compose.yml files for each, linux users need to add an environment variable for gdev to be able to use the correct compose configuration. Add this to your chosen shell configuration (for example .bashrc or .zshrc).

$ # Export compose file for ubuntu
$ export COMPOSE_FILE="docker-compose-ubuntu.yml"

Start project containers

Start a new shell and cd to a project that uses docker-compose.yml

$ gdev up

What this tool includes

gdev installer installs docker for mac plus few settings for better development environment. It setups 4 service containers into your docker.

Service Containers

Nginx proxy

This project uses excellent nginx proxy container from jwilder/nginx-proxy. This proxy reserver ports http/https ports from your localhost and proxies the requests to your project containers. Just provide VIRTUAL_HOST=your-address.test env in your projects docker-compose.yml and nginx proxy will take care of the rest.

Custom DNS server

We want to use real addresses for all containers. Some applications have strange behaviour if they are just used from localhost:8080. We use andyshinn/dnsmasq for local dnsmasq which always responds 127.0.0.1 to any request. Installation script adds custom resolver file for your machine:

$ cat /etc/resolver/test
domain test
nameserver 127.0.0.1
search_order 1

This means that all *.test addresses are now pointing into your local machine so you don't have to edit /etc/hosts file ever again. We used .test tld because it is reserved by IETF and will never be sold to google like what happened to it's popular cousin .dev.

Custom https certificate generator

It's a really good practise to use https in production but only a few people use it in development. This makes it harder for people to notice mixed content error messages in development.

While using gdev you won't see any of these:

non-trusted https

and instead more of these:

self trusted https

gdev includes custom certificate generator onnimonni/signaler. gdev installer creates a self-signed unique ca certificate during installation and saves it in your system keychain. If you provide HTTPS_HOST=your-address.test env in your docker-compose.yml you will automatically have self-signed and trusted certificate for your development environment.

Custom SMTP server for debugging email

We included mailhog/mailhog docker container for easier debugging of emails. Just use 172.17.0.1:25 as smtp server in your application and you are good to go.

Or if your legacy application has hard coded email server you can use this trick in your docker-compose.yml:

extra_hosts:
    - "my-mail-server.com:172.17.0.1"

and docker will add it into the /etc/hosts file inside the container automatically during startup.

Short list of usual commands

# This is similiar to vagrant up
# It reads docker-compose.yml from current directory and starts up containers
$ gdev up

# Quickly pause the project and free resources
$ gdev pause

# Wake the project up from pause as quickly as it was paused
$ gdev unpause

# Open shell into web container
$ gdev shell

# Restart all project containers
$ gdev reload

# List all containers from project
$ gdev ps

# List all containers from docker
$ docker ps -a

# Open bash into any container
$ docker exec -it $CONTAINER_ID bash

# Create new project (interactive wizard for setting up project)
$ gdev create

Creating new project

Before creating a new project you should setup a GIT repository for your new project.

It's also advisable to create a config file to your home directory with some default values. File should be named ~/.gdev/gdevconf.yml

Example gdevconf.yml:

create:
  defaults:
    wordpress:
      # Flynn stage cloud address
      stage: stage.yourdomain.com
      # Flynn production cloud address
      production: production.yourdomain.com
      smtp_host: "172.17.0.1"
      components: "dustpress"
      theme: "[email protected]:devgeniem/wp-starter-dustpress-theme.git"
    nodejs: TODO
    silverbullet: TODO

Workflow

  • The source code running inside a project container is loaded from the directory on your hard drive. You can use text editors and Git clients on the host machines, and shouldn't need to work in the guest machine or the container.
  • You should not need to run any application code directly from your host machine. Try to force yourself to find a containerized way of accomplishing things.
  • Run gdev without any arguments for lots of help

Troubleshooting

No space left on device

Docker for Mac has only limited amount of disk space and this means that older images or stopped containers are taking all of the 60gb/120gb share.

To resolve this delete stopped containers, dangling images and dangling volumes. This can be done by running cleanup helper:

$ gdev cleanup

If Docker for Mac still has a bug with freeing up disk space, dump databases you need and reset Docker for Mac settings. This will free all the space Docker is hogging. Then you will need to set up your projects again (import databases).

When in doubt, update and restart everything

To update all containers and settings run following global commands:

$ gdev pull
$ gdev update
$ gdev service pull
$ gdev service build
$ gdev service reload

Then restart docker for mac and run these commands:

# Reload project containers
$ cd /to/your/project
$ gdev reload

Contributors

Contributing

  1. Fork it ( https://github.com/devgeniem/gdev/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

gdev is available under the MIT license. See the LICENSE file for more info.

Copyright 2017 Geniem Oy.

gdev's People

Contributors

benjamin-smith avatar botimer avatar drawpause avatar geerlingguy avatar godbone avatar hooace avatar kochjoel avatar manojlds avatar markogeniem avatar nomafin avatar onnimonni avatar silvamerica avatar villepietarinen avatar vmlinz avatar zarubaru avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gdev's Issues

Cleanup taking minutes.

gdev up gave me a following error:
Traceback (most recent call last): File "<string>", line 3, in <module> File "compose/cli/main.py", line 61, in main File "compose/cli/main.py", line 113, in perform_command File "contextlib.py", line 35, in __exit__ File "compose/cli/errors.py", line 56, in handle_connection_errors TypeError: log_timeout_error() takes exactly 1 argument (0 given) docker-compose returned -1

After that error I ran: gdev cleanupand console has shown Removing exited docker containers... for minutes.

No documentation on how to uninstall

I would like to uninstall and completely undo everything the gdev bootstrap script did. I can't find any script or documentation on how to do that. This script made significant system changes so this isn't just a matter or removing some files.

gdev update leads to pip problem

Previously setup was working ok. Then gdev pull -> gdev update

task path: /usr/local/gdev-env/ansible/mac.yml:25 <127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: artturi <127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p " echo $HOME/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341 " && echo ansible-tmp-1481613925.17-266903657084341=" echo $HOME/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341 " ) && sleep 0' <127.0.0.1> PUT /var/folders/hj/jb8kfyrx7553gggkpf3r9yhr0000gp/T/tmpPcjiwl TO /Users/artturi/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341/easy_install <127.0.0.1> EXEC /bin/sh -c 'chmod u+x /Users/artturi/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341/ /Users/artturi/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341/easy_install && sleep 0' <127.0.0.1> EXEC /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=dsoqeiubwzrbeihvcmixrtpttyimaigp] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-dsoqeiubwzrbeihvcmixrtpttyimaigp; LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /Users/artturi/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341/easy_install; rm -rf "/Users/artturi/.ansible/tmp/ansible-tmp-1481613925.17-266903657084341/" > /dev/null 2>&1'"'"' && sleep 0' fatal: [127.0.0.1]: FAILED! => {"failed": true, "msg": "timeout waiting for privilege escalation password prompt:\n\nWARNING: Improper use of the sudo command could lead to data loss\nor the deletion of important system files. Please double-check your\ntyping when using sudo. Type \"man sudo\" for more information.\n\nTo proceed, enter your password, or type Ctrl-C to abort.\n\n[sudo via ansible, key=dsoqeiubwzrbeihvcmixrtpttyimaigp] password: "}

At the beginning there was one prompt for the sudo pw. At this point there was no prompt or anything.

Allow dynamic ports for unison sync

Current setup:
Unison data sync needs 5000 port to be mapped to localhost.

Feature: Developer needs to use multiple projects simultanously
Allowing any dynamic port for unison sync would allow us to use any number of projects simultaneously.

Sync fails if the site name contains dashes in gdev create

I created a new site with the gdev create wizard and set a site name that contained dashes. Sync failed because of replacements made into the template docker configuration files of wp-project. The dashes were removed in them. I put the dashes back in docker-composer.yml and docker-sync.yml, reloaded everything and the sync started to work.

Here is an example of a functioning sync config file:

version: "2"

options:
  verbose: true
syncs:
  my-site-name-with-dashes-sync:
    src: '.'
    host_disk_mount_mode: 'cached'
  my-site-name-with-dashes-uploads-sync:
    src: './.docker/uploads'
    host_disk_mount_mode: 'cached'

Driver overlay2 failed to remove root filesystem

ERROR: for db  Driver overlay2 failed to remove root filesystem 15874c3878feb44d0c70cc8792aa2f07c0d4131e319e782e3d79ce288c60a120: remove /var/lib/docker/overlay2/d4b505c0bf5dd42bb2e46343d4f9c00262992d8c64526f1eba318723d3a11fe8/merged: device or resource busy

ERROR: for redis  Driver overlay2 failed to remove root filesystem da4dde124c7efc16868e3ecc5d3f1146e60dd28db48a0111087a423d7e5b4a1f: remove /var/lib/docker/overlay2/39f8aa911fed29b21889c1a41db57fd217569eaadc9b176e3622bf82c0695e11/merged: device or resource busy

ERROR: for elasticsearch  Driver overlay2 failed to remove root filesystem 95ea30159e2d335ed46b533823faa4776349df5590b87d6b0d5f1589f47f7b1a: remove /var/lib/docker/overlay2/8c5dce65663d22dc29f74957a476b58c1ea764f6953510a59e9f1d17273acafb/merged: device or resource busy

ERROR: for mongo  Driver overlay2 failed to remove root filesystem dd5038d814110a7b8ab5d12161398e07805ae8614711546134f94530e0d7397b: remove /var/lib/docker/overlay2/c8ab526713a316036d40820a5d3a4fea345216f75ffc915ad3718d981c3c3ada/merged: device or resource busy

Run webpack from outside the frontend container

Find a way to run webpack watch from outside the frontend container.
Something like:

gdev run frontend "cd {dir_where_the_webpack_config_exists}; ./node_modules/webpack/bin/webpack.js --watch;"

gdev restart stops bg-sync

bg-sync stops working after you run gdev restart. It just gets stuck on "Looking for changes"..

gdev stop -> gdev up fixes it.

Add gdev sync command

Current setup:
Running gdev up starts data container and automatically syncs initial state.
Then it starts other containers which can mount files from data container.
Then it starts unison file watcher to sync files as they change to and from data container.

Preferred way:
Stop automatic sync watcher in the end of gdev up and provide another command which can be used as file sync watcher for project.

gdev up should tell the user to run gdev sync if onnimonni/unison container is available in docker-compose.yml.

It should check if docker-compose is running and fail with sane error message if it isn't.

gdev create fails if theme arg empty or defined theme has missing directories

When running the gdev create wizard and not defining a theme repo gdev create will fail.
Example of gdev create output when living the theme repo empty:

------------------------= Confirm input =------------------------
sitename: dp2017Demo
type: wordpress
components: dustpress
theme:
npm_install: false
run_seed: false
stage:
production:
git:
-----= Enter Y to create project, something else to cancel =-----

Y

Creating project dp2017Demo...

...

Cloning theme from ...
fatal: repository 'dp2017Demo/web/app/themes/dp2017Demo' does not exist

Removing .git directory from theme...

...

Replacing 'THEMENAME' strings from all project files with 'dp2017Demo'...
/usr/local/bin/gdev:559:in `read': No such file or directory - dp2017Demo/web/app/themes/dp2017Demo/lib/extras.php (Errno::ENOENT)
	from /usr/local/bin/gdev:559:in `block in create_app'
	from /usr/local/bin/gdev:558:in `each'
	from /usr/local/bin/gdev:558:in `create_app'
	from /usr/local/bin/gdev:794:in `create_wizard'
	from /usr/local/bin/gdev:491:in `create'
	from /usr/local/bin/gdev:54:in `initialize'
	from /usr/local/bin/gdev:867:in `new'
	from /usr/local/bin/gdev:867:in `<main>'

gdev create will also fail with the above output if a theme repo has been defined but the theme does not include some of the following files:

  • /lib/extras.php
  • /lib/images.php
  • /lib/setup.php
  • /package.json
  • /style.css"

gdev needs php >=7.0 when creating project

Error message:

Your requirements could not be resolved to an installable set of packages.
  Problem 1
    - This package requires php >=7.0 but your PHP version (5.6.32) does not satisfy that requirement.

multiple server names causes an error

root@dca1faa656fc:/app# cat /etc/nginx/conf.d/default.conf

HTTP 1.1 support

proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
upstream mail.test {
# gdev_mail_1
server 172.17.0.5:8025;
}
server {
server_name mail.test;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
location / {
proxy_pass http://mail.test;
}
}
upstream site.test *.site.test {
# site_web_1
server 172.17.0.9:8080;
}

Start: site.test *.site.test

server {
server_name site.test *.site.test;
listen 80 ;
access_log /var/log/nginx/access.log vhost;
return 301 https://$host$request_uri;
}
server {
server_name site.test *.site.test;
listen 443 ssl http2 ;
access_log /var/log/nginx/access.log vhost;
include ssl-settings.conf;
ssl_certificate /etc/nginx/certs/site.test.crt;
ssl_certificate_key /etc/nginx/certs/site.test.key;
location / {
proxy_pass http://site.test *.site.test;
}
}
root@dca1faa656fc:/app# nginx -t
2016/12/30 14:56:04 [emerg] 44#44: invalid number of arguments in "upstream" directive in /etc/nginx/conf.d/default.conf:22
nginx: [emerg] invalid number of arguments in "upstream" directive in /etc/nginx/conf.d/default.conf:22
nginx: configuration file /etc/nginx/nginx.conf test failed
root@dca1faa656fc:/app# exit
exit

Running the install script returns an error

fatal: [127.0.0.1]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_args": {"backup": false, "block": "", "content": null, "create": false, "delimiter": null, "dest": "/etc/exports", "directory_mode": null, "follow": false, "force": null, "group": null, "insertafter": null, "insertbefore": null, "marker": ": dlite", "mode": null, "owner": null, "regexp": null, "remote_src": null, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "absent", "validate": null}, "module_name": "blockinfile"}, "msg": "Destination /etc/exports does not exist !", "rc": 257}

If gdev is reloaded gdev sync will go to inconsistent state

Steps to reproduce:

$ gdev up
$ gdev reload
$ gdev sync

Then I can see that the sync fails:

$ gdev sync
INFO: Syncing files to data container with unison, using port 32779...
Contacting server...
Connected [//data-container//var/www/project -> //dev-machine//Users/henrikuittinen/Sources/asunnot.keva.fi]
Looking for changes
Fatal error: Warning: inconsistent state.
The archive file is missing on some hosts.
For safety, the remaining copies should be deleted.
  Archive ar33a3dac36ed37c0d952a172a45ded906 on host dev-machine should be DELETED
  Archive ar87132acb83c8baf3468e5ac97f28636b on host data-container is MISSING
Please delete archive files as appropriate and try again
or invoke Unison with -ignorearchives flag.
INFO: Sync is ready!

I think this can be fixed by adding -ignorearchives to the gdev sync command.

I think it happens because the data container is reloaded and it triggers data container hostname to change as well and because of that unison thinks it is inconsistent even though everthing is okay.

gdev update fails on gdev-env directory permissions

Updating gdev fails on gdev-env permissions check with the following error:

{"failed": true, "msg": "timeout waiting for privilege escalation password prompt:\n\nWe trust you have received the usual lecture from the local System\nAdministrator. It usually boils down to these three things:\n\n    #1) Respect the privacy of others.\n    #2) Think before you type.\n    #3) With great power comes great responsibility.\n\n[sudo via ansible, key=zikfihyxghhcpipjhoffvakhdltgavqx] password: "}

"gdev update" command fails when installing pip

fatal: [127.0.0.1]: FAILED! => {"failed": true, "msg": "timeout waiting for privilege escalation password prompt:\n\nWARNING: Improper use of the sudo command could lead to data loss\nor the deletion of important system files. Please double-check your\ntyping when using sudo. Type "man sudo" for more information.\n\nTo proceed, enter your password, or type Ctrl-C to abort.\n\n[sudo via ansible, key=xxx] password: "}

End_of_file exception raised

Contacting server... Connected [//5335fb0e62f6//var/www/project -> //dev-machine//Users/user/Work/Pipeline/projecta] Looking for changes Fatal error: Server: End_of_file exception raised in loading archive (this indicates a bug!) INFO: Sync is ready!

I.e. the sync ain't working.

gdev up fails: ERROR: for dnsmasq Cannot start service dnsmasq:

Full error:
ERROR: for dnsmasq Cannot start service dnsmasq: driver failed programming external connectivity on endpoint gdev_dnsmasq_1 (ab77b8d1a050270403dca430a587017b9ddcd145ecf203e2aa9eb6ce9870b6fe): Error starting userland proxy: Bind for 10.254.254.254:53: unexpected error (Failure EADDRNOTAVAIL)
ERROR: Encountered errors while bringing up the project

Path error on a working path

Using this:

https://github.com/devgeniem/wp-project/blob/master/docker-compose.yml

Getting this:

web_1            | [services.d] started php-fpm
web_1            | [16-Nov-2016 11:59:20] ERROR: [pool www] the chdir path '/var/www/project/web' does not exist or is not a directory
web_1            | [16-Nov-2016 11:59:20] ERROR: failed to post process the configuration
web_1            | [16-Nov-2016 11:59:20] ERROR: FPM initialization failed

Inside the container there is this path:

/var/www/project/web

Npm install sync problems

After installing node modules within the front container and running webpack, the sync fails due to permission issues.

Docker version 1.12.1, build 6f9534c

Looking for changes
  Waiting for changes from server
Reconciling changes
chgd dir ====> deleted    web/app/plugins/dustpress-events/node_modules/.staging
Propagating updates
UNISON 2.48.4 started propagating changes at 09:41:34.28 on 20 Sep 2016
[BGN] Copying web/app/plugins/dustpress-events/node_modules/.staging from /Users/villes/Projects/turkuamk to //48ef53ef0177//var/www/project
Failed: Error in creating directory:
Permission denied [mkdir(/var/www/project/web/app/plugins/dustpress-events/node_modules/.unison..staging.8b3b5c40481f22f94eb530f1f649c57e.unison.tmp)]
100%  00:00 ETAFailed [web/app/plugins/dustpress-events/node_modules/.staging]: Error in creating directory:
Permission denied [mkdir(/var/www/project/web/app/plugins/dustpress-events/node_modules/.unison..staging.8b3b5c40481f22f94eb530f1f649c57e.unison.tmp)]
UNISON 2.48.4 finished propagating changes at 09:41:34.28 on 20 Sep 2016
Saving synchronizer state
Synchronization incomplete at 09:41:34  (0 items transferred, 0 skipped, 1 failed)
  failed: web/app/plugins/dustpress-events/node_modules/.staging```

Error response from daemon: Syntax error - can't find = in "#". Must be of the form: name=value

This is for production docker image

FROM devgeniem/wordpress-server:php7.0

Use port 8080 for flynn/router

Use these uid/gid in production by default and these can be changed when needed

ENV PORT=8080
FLYNN_PROCESS_TYPE='WEB'
WP_UID=10000
WP_GID=10001
BASIC_AUTH_USER=test
BASIC_AUTH_PASSWORD_HASH='{PLAIN}test'

Skip dynamic user creation and

create user with ID WP_UID/WP_GID here for nginx/php-fpm

RUN rm -f /etc/cont-init.d/01-create-web-user
&& rm -f /etc/cont-init.d/01-init-web
&& addgroup web -S -g $WP_GID
&& adduser wordpress -S -G web -u $WP_UID

Install things in certain order to allow better caching

Stuff that changes only rarely should be prioritized first

Install web root files

COPY web/*.php /var/www/project/web/

Install wp core

COPY web/wp /var/www/project/web/wp

Install scripts

COPY scripts /var/www/project/scripts

Install database migration config

COPY phinx.yml /var/www/project/phinx.yml

Install database migrations and seeds

COPY db /var/www/project/db

Install nginx configs

COPY nginx /var/www/project/nginx

Install application config

COPY config /var/www/project/config

Install vendor

COPY vendor /var/www/project/vendor

Install cronjobs

COPY tasks.cron /var/www/project/

Install wp-content

COPY web/app/*.php /var/www/project/web/app/
COPY web/app/languages /var/www/project/web/app/languages
COPY web/app/mu-plugins /var/www/project/web/app/mu-plugins
COPY web/app/plugins /var/www/project/web/app/plugins
COPY web/app/themes /var/www/project/web/app/themes

docker-sync should be installed

We have started to use docker-compose version 3 and declare used volumes as external. This requires docker sync to work. Consider these lines to be added in the install script for macOS.

# Install non-system ruby to run commands non-sudo
brew install ruby
# Install docker-sync to run .docker-sync.yml defined volumes
gem install docker-sync

Sometimes data container startup is slow and gdev fails to do initial sync

This happened me today:

onnimonni@omppukone:~/P/G/s/client> gdev up                                                                 
gdev_nginx_1 is up-to-date
gdev_signaler_1 is up-to-date
gdev_mail_1 is up-to-date
gdev_dnsmasq_1 is up-to-date
Starting wp_project_data_1
INFO: Starting to sync into port: 32768...
Fatal error: Lost connection with the server

But when I run gdev sync afterwise it worked just fine.

Gdev should poll on the container port and wait that unison is ready before starting the initial sync. Or try again after 1s if the initial sync fails.

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.