Git Product home page Git Product logo

aptly_cli's Introduction

AptlyCli

Build Status Gem Version Coverage Status

A command line interface to execute Aptly commands againts remote Aptly API servers. Aptly-cli will allow you to interact with the file, repo, snapshot, publish, packages, graph and version API endpoints of your Aptly server.

Installation

Install Gem:

$ gem install aptly_cli

or...

Install and run aptly-cli from Docker:

# Optional: If you don't pull explicitly, `docker run` will do it for you
$ docker pull sepulworld/aptly-cli

$ alias aptly-cli='\
  docker run \
    -v /etc/aptly-cli.conf:/etc/aptly-cli.conf \
    -it --rm --name=aptly-cli \
    sepulworld/aptly-cli'

(Depending on how your system is set up, you might have to add sudo in front of the above docker commands or add your user to the docker group).

If you don't do the docker pull, the first time you run aptly-cli, the docker run command will automatically pull the sepulworld/aptly-cli image on the Docker Hub. Subsequent runs will use a locally cached copy of the image and will not have to download anything.

Create a configuration file with aptly server and port, /etc/aptly-cli.conf (YAML syntax):

---
:proto: http
:server: 127.0.0.1
:port: 8082
:debug: false

If you use Basic Authentication to protect your API, add username and password:

:username: api-user
:password: api-password

The username and password can also be configured for prompt entry using the following in aptly-cli.conf:

:username: ${PROMPT}
:password: ${PROMPT_PASSWORD}

The tool will prompt for the specified values, where ${PROMPT} results in a regular prompt and ${PROMPT_PASSWORD} results in a password prompt where the input is replaced by asterisks, e.g.:

$ aptly-cli version
  Enter a value for username:
  zane
  Enter a value for password:
  ********

Another possibility for storing passwords is ${KEYRING}. To use this feature, you must have the keyring gem installed and also have a system that is set up to use one of the backends that the keyring gem supports, such as Mac OS X Keychain or GNOME 2 Keyring (Note: Only Mac OS X Keychain has been tested thus far):

$ gem install keyring

Then you can put something like this in aptly-cli.conf:

:username: zane
:password: ${KEYRING}

The first time you run an aptly-cli command, you will be prompted to enter a password.

$ aptly-cli version
Enter a value for password:
***************

The entered password will be stored in your keyring so that future uses of aptly-cli can get the password from your keyring:

$ aptly-cli version
{"Version"=>"0.9.7"}

Also make sure that your config file isn't world readable (chmod o-rw /etc/aptly-cli.conf)

If a configuration file is not found, the defaults in the example configuration file above will be used.

Usage - available aptly-cli commands

The --config (-c) option allows specifying an alternative config file, e.g.:

$ aptly-cli -c ~/.config/aptly-cli/aptly-cli.conf repo_list

The --server, --port, --username, and --password options allow specifying those things on the command-line and not even requiring a config file.

$ aptly-cli --server 10.3.0.46 --port 9000 --username marca --password '${PROMPT_PASSWORD}' repo_list

Note that you can use ${PROMPT}, ${PROMPT_PASSWORD}, and ${KEYRING} in the values of these options, just as you can in a config file. Note that you might have to quote them to prevent the shell from trying to expand them.

$ aptly-cli --help
  NAME:

    aptly-cli

  DESCRIPTION:

    Aptly repository API client (https://github.com/sepulworld/aptly_cli)

  COMMANDS:

    file_delete         File delete
    file_list           List all directories
    file_upload         File upload
    graph               Download an svg or png graph of repository layout
    help                Display global or [command] help documentation
    publish_drop        Delete published repository
    publish_list        List published repositories
    publish_repo        Publish local repository or snapshot under specified prefix
    publish_update      Update published repository
    repo_create         Create a new repository, requires --name
    repo_delete         Delete a local repository, requires --name
    repo_edit           Edit a local repository metadata, requires --name
    repo_list           Show list of currently available local repositories
    repo_package_add    Add existing package to local repository
    repo_package_delete Delete package from local repository
    repo_package_query  List all packages or search on repo contents, requires --name
    repo_show           Returns basic information about local repository
    repo_upload         Import packages from files
    snapshot_create     Create snapshot, require --name
    snapshot_delete     Delete snapshot, require --name
    snapshot_diff       Calculate difference between two snapshots
    snapshot_list       Return list of all snapshots created in the system
    snapshot_search     List all packages in snapshot or perform search
    snapshot_show       Get information about snapshot by name
    snapshot_update     Update snapshot’s description or name
    version             Display aptly server version

  GLOBAL OPTIONS:

    -c, --config FILE
        Path to YAML config file

    --no-config
        Don't try to read YAML config file

    -s, --server SERVER
        Host name or IP address of Aptly API server

    -p, --port PORT
        Port of Aptly API server

    --username USERNAME
        User name or '${PROMPT}'

    --password PASSWORD
        Password or '${PROMPT_PASSWORD}' or '${KEYRING}'

    --debug
        Enable debug output

    -h, --help
        Display help documentation

    -v, --version
        Display version information

    -t, --trace
        Display backtrace when an error occurs

To see more options for each command

$ aptly-cli <command> --help

Development

Measuring coverage locally

$ rake docker_pull
$ rake docker_run
$ bundle exec rake test
...
Coverage report generated for Unit Tests to /Users/marca/dev/git-repos/aptly_cli/coverage. 521 / 566 LOC (92.05%) covered.
[Coveralls] Outside the CI environment, not sending data.

$ open coverage/index.html

screen shot 2016-07-25 at 8 38 28 pm

Rubocop syntax and style check

$ bundle exec rake rubocop
Running RuboCop...
Inspecting 24 files
WCCCWC..CCCC.CCC.WCWCCCC
...

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release to create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Ruby Minitest are implemented using a Docker container for functional tests.

Rake tasks available:

rake build                 # Build aptly_cli-<version>.gem into the pkg directory
rake clean                 # Remove any temporary products
rake clobber               # Remove any generated files
rake docker_build          # Docker build image
rake docker_list_aptly     # List Docker Aptly running containers
rake docker_pull           # Pull Docker image to Docker Hub
rake docker_push           # Push Docker image to Docker Hub
rake docker_restart        # Restart Aptly docker container
rake docker_run            # Start Aptly Docker container on port 8082
rake docker_show_logs      # Show running Aptly process Docker stdout logs
rake docker_stop           # Stop running Aptly Docker containers
rake install               # Build and install aptly_cli-<version>.gem into system gems
rake install:local         # Build and install aptly_cli-<version>.gem into system gems without network access
rake rubocop               # Run RuboCop
rake rubocop:auto_correct  # Auto-correct RuboCop offenses
rake test                  # Run tests

Contributing

  1. Fork it ( https://github.com/[my-github-username]/aptly_cli/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

Related

aptly_cli's People

Contributors

edarzins avatar gitter-badger avatar kumy avatar msabramo avatar neomilium avatar sepulworld avatar shyx0rmz 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

aptly_cli's Issues

--debug, --username, --server, --password appears to be broken

Looks like global options in aptly_command.rb are not working presently.

debug_output $stdout L66 in aptly_command.rb doesn't appear to respond and even if you set debug_output $stdout directly in class AptlyCommand it isn't used when running http queries in let's say aptly_repo

$ aptly-cli repo_list --debug --no-config
{"Name"=>"testrepo", "Comment"=>"", "DefaultDistribution"=>"", "DefaultComponent"=>"main"}
{"Name"=>"testrepocreate", "Comment"=>"testing repo creation", "DefaultDistribution"=>"precisecreatetest", "DefaultComponent"=>""}
{"Name"=>"testrepotoshow", "Comment"=>"testing repo show", "DefaultDistribution"=>"preciseshowtest", "DefaultComponent"=>""}
{"Name"=>"testrepo20", "Comment"=>"", "DefaultDistribution"=>"", "DefaultComponent"=>"main"}
{"Name"=>"testrepotoquery", "Comment"=>"testing repo query with name", "DefaultDistribution"=>"precisequerytest", "DefaultComponent"=>""}
{"Name"=>"testrepoedit", "Comment"=>"testing repo edit distro name", "DefaultDistribution"=>"preciseeditdistnew", "DefaultComponent"=>""}

The above command should include HTTP debug output from HTTParty.

failedFiles vs FailedFiles

I'm seeing:

/opt/ruby/2.1.2/lib/ruby/gems/2.1.0/gems/aptly_cli-0.1.6/lib/aptly_repo.rb:123:in 'repo_upload': undefined method 'empty?' for nil:NilClass (NoMethodError)

in aptly_repo.rb; code references json_response["failedFiles"], but I see FailedFiles if I run the API manually.

test/Dockerfile no longer builds

$ docker build -t sepulworld/aptly_api_test ./test/
Sending build context to Docker daemon 75.26 kB
Step 1 : FROM debian:jessie
 ---> 1b088884749b
Step 2 : EXPOSE 8080
 ---> Using cache
 ---> 059055839ed0
Step 3 : RUN echo "deb http://repo.aptly.info/ squeeze main" > /etc/apt/sources.list.d/aptly.list; apt-key adv --keyserver keys.gnupg.net --recv-keys 2A194991; apt-get update; apt-get install aptly curl xz-utils bzip2 gnupg wget graphviz -y --force-yes; wget --quiet http://mirror.as24220.net/pub/ubuntu-archive/pool/main/z/zeitgeist/zeitgeist_0.9.0-1_all.deb -O /tmp/zeitgeist_0.9.0-1_all.deb; wget --quiet http://mirror.as24220.net/pub/ubuntu-archive/pool/main/z/zsh/zsh_5.1.1-1ubuntu1_i386.deb -O /tmp/zsh_5.1.1-1ubuntu1_i386.deb
 ---> Running in 000ec37cb330
Executing: gpg --ignore-time-conflict --no-options --no-default-keyring --homedir /tmp/tmp.OpKMIiSwnv --no-auto-check-trustdb --trust-model always --primary-keyring /etc/apt/trusted.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-jessie-automatic.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-jessie-security-automatic.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-jessie-stable.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-squeeze-automatic.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-squeeze-stable.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-wheezy-automatic.gpg --keyring /etc/apt/trusted.gpg.d/debian-archive-wheezy-stable.gpg --keyserver keys.gnupg.net --recv-keys 2A194991
gpg: requesting key 2A194991 from hkp server keys.gnupg.net
?: keys.gnupg.net: Host not found
gpgkeys: HTTP fetch error 7: couldn't connect: Connection timed out
gpg: no valid OpenPGP data found.
gpg: Total number processed: 0
Err http://httpredir.debian.org jessie InRelease

Err http://repo.aptly.info squeeze InRelease

Err http://httpredir.debian.org jessie-updates InRelease

Err http://security.debian.org jessie/updates InRelease

Err http://httpredir.debian.org jessie Release.gpg
  Could not resolve 'httpredir.debian.org'
Err http://security.debian.org jessie/updates Release.gpg
  Could not resolve 'security.debian.org'
Err http://repo.aptly.info squeeze Release.gpg
  Could not resolve 'repo.aptly.info'
Err http://httpredir.debian.org jessie-updates Release.gpg
  Could not resolve 'httpredir.debian.org'
Reading package lists...
W: Failed to fetch http://httpredir.debian.org/debian/dists/jessie/InRelease

W: Failed to fetch http://httpredir.debian.org/debian/dists/jessie-updates/InRelease

W: Failed to fetch http://security.debian.org/dists/jessie/updates/InRelease

W: Failed to fetch http://repo.aptly.info/dists/squeeze/InRelease


�
W: Failed to fetch http://httpredir.debian.org/debian/dists/jessie/Release.gpg  Could not resolve 'httpredir.debian.org'

W: Failed to fetch http://repo.aptly.info/dists/squeeze/Release.gpg  Could not resolve 'repo.aptly.info'

W: Failed to fetch http://httpredir.debian.org/debian/dists/jessie-updates/Release.gpg  Could not resolve 'httpredir.debian.org'

W: Failed to fetch http://security.debian.org/dists/jessie/updates/Release.gpg  Could not resolve 'security.debian.org'

W: Some index files failed to download. They have been ignored, or old ones used instead.
Reading package lists...
Building dependency tree...
Package bzip2 is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source

Package xz-utils is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source

E: Unable to locate package aptly
E: Unable to locate package curl
E: Package 'xz-utils' has no installation candidate
E: Package 'bzip2' has no installation candidate
E: Unable to locate package wget
E: Unable to locate package graphviz
/bin/sh: 1: wget: not found
/bin/sh: 1: wget: not found
The command '/bin/sh -c echo "deb http://repo.aptly.info/ squeeze main" > /etc/apt/sources.list.d/aptly.list; apt-key adv --keyserver keys.gnupg.net --recv-keys 2A194991; apt-get update; apt-get install aptly curl xz-utils bzip2 gnupg wget graphviz -y --force-yes; wget --quiet http://mirror.as24220.net/pub/ubuntu-archive/pool/main/z/zeitgeist/zeitgeist_0.9.0-1_all.deb -O /tmp/zeitgeist_0.9.0-1_all.deb; wget --quiet http://mirror.as24220.net/pub/ubuntu-archive/pool/main/z/zsh/zsh_5.1.1-1ubuntu1_i386.deb -O /tmp/zsh_5.1.1-1ubuntu1_i386.deb' returned a non-zero code: 127

Add support for Package Reference API feature

From aptly.info api docs...

CREATE SNAPSHOT FROM PACKAGE REFS
POST /api/snapshots

Create snapshot from list of package references.

This API creates snapshot out of any list of package references. Package references could be obtained from other snapshots, local repos or mirrors.

gpg_batch not returning a Boolean

$ aptly-cli publish_update --forceoverwrite --gpg_batch --gpg_passphrase 'TEST' --distribution test-platform
{"Batch"=>"true", "Passphrase"=>"TEST"}

Batch should not have a string as a value. It needs to be a Boolean

{"error"=>"json: cannot unmarshal string into Go value of type bool", "meta"=>"Operation aborted"}

Incorrect encoding on Ruby 1.9.1

Seeing this on Ruby 1.9.1.

# aptly-cli 
/usr/local/bin/aptly-cli:23:in `load': /var/lib/gems/1.9.1/gems/aptly_cli-0.1.5/bin/aptly-cli:28: invalid multibyte char (US-ASCII) (SyntaxError)
/var/lib/gems/1.9.1/gems/aptly_cli-0.1.5/bin/aptly-cli:28: invalid multibyte char (US-ASCII)
/var/lib/gems/1.9.1/gems/aptly_cli-0.1.5/bin/aptly-cli:28: syntax error, unexpected $end, expecting keyword_end
... would be created if it doesn’t exist.'
...                               ^
    from /usr/local/bin/aptly-cli:23:in `<main>'

Solution is to add the following to the aptly-cli script just below #!/usr/bin/env ruby

#!/usr/bin/env ruby
# encoding: utf-8

Nonintuitive config for /api not at server root

My aptly api is behind a reverse proxy that exposes it at http://example.com:8888/aptly/api. After a lot of fiddling, I was able to get aptly_cli to eventually connect to it. Unfortunately, the behavior is non intuitive because of the way the base uri is constructed:
https://github.com/sepulworld/aptly_cli/blob/master/lib/aptly_command.rb#L55

Instead of having a config file with:
:servername: example.com/aptly
:port: 8888

it was necessary to specify the config as:
:servername: example.com
:port: 8888/aptly

It seems like an extra optional configuration argument that specifies the root prefix (/aptly) or constructing the base url in a different way that recognizes example.com/aptly and inserts the port after .com would be helpful.

aptly-cli snapshot_list --all

Bug with aptly-cli snapshot_list

$ aptly-cli snapshot_list --sort all --trace
/Users/zanew/.rvm/gems/ruby-2.2.1/gems/aptly_cli-0.1.8/lib/aptly_snapshot.rb:25:in `snapshot_list': wrong number of arguments (2 for 0..1) (ArgumentError)
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/aptly_cli-0.1.8/bin/aptly-cli:314:in `block (2 levels) in <top (required)>'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/command.rb:178:in `call'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/command.rb:178:in `call'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/command.rb:153:in `run'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/runner.rb:428:in `run_active_command'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/runner.rb:68:in `run!'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/delegates.rb:15:in `run!'
    from /Users/zanew/.rvm/gems/ruby-2.2.1/gems/commander-4.3.5/lib/commander/import.rb:5:in `block in <top (required)>'

file_upload fails with aptly-cli, but works with curl

I'm having trouble getting file_upload to work, and I can't find any obvious errors anywhere. Suspecting some kind of bug. Uploading a file by accessing the aptly API using curl works. Other aptly-cli commands work, such as file_list, repo_upload and publish_update.

My aptly-cli server is Debian 9.6, Ruby 2.3.3, aptly_cli 0.4.1.
My aptly api server is Debian 9.6, aptly 1.3.0.

File to upload:

# ls -l aptly-test-file.txt 
-rw-r--r-- 1 root root 22 Jan  8 09:33 aptly-test-file.txt

# cat aptly-test-file.txt
1234567890
abcdefghij

Uploading the file using aptly-cli causes the directory to be created on the server, but the file is not uploaded. The response also does not contain the filename as expected:

# aptly-cli file_upload --trace --upload aptly-test-file.txt --directory /testfile
opening connection to my.aptly.server:443...
opened
starting SSL for my.aptly.server:443...
SSL established
<- "POST /api/files/testfile HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nContent-Type: multipart/form-data; boundary=-----------RubyMultipartPost\r\nContent-Length: 447\r\nAuthorization: Basic <redacted>\r\nConnection: close\r\nHost: my.aptly.server\r\n\r\n"
-> "HTTP/1.1 200 OK\r\n"
-> "Server: nginx/1.14.1\r\n"
-> "Date: Tue, 08 Jan 2019 08:42:46 GMT\r\n"
-> "Content-Type: application/json; charset=utf-8\r\n"
-> "Content-Length: 2\r\n"
-> "Connection: close\r\n"
-> "Strict-Transport-Security: max-age=15768000\r\n"
-> "\r\n"
reading 2 bytes...
-> "[]"
read 2 bytes
Conn close

My /etc/aptly-cli.conf looks like this:

# cat /etc/aptly-cli.conf 
---
:proto: https
:server: my.aptly.server
:port: 443
:username: aptlyuser
:password: <redacted>
:debug: true

Using curl to upload the same file works, and shows a significant difference in the reported Content-Length of the upload. I'm not sure exactly how this is calculated:

# curl -v -F [email protected] https://aptlyuser:<redacted>@my.aptly.server/api/files/testfile
*   Trying ip.of.my.aptly.server...
* TCP_NODELAY set
* Connected to my.aptly.server (ip.of.my.aptly.server) port 443 (#0)
[..handshake lines removed..]
> POST /api/files/testfile HTTP/1.1
> Host: my.aptly.server
> Authorization: Basic <redacted>
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 219
> Content-Type: multipart/form-data; boundary=------------------------61a6064b8ba6fb9f
> 
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200 
< server: nginx/1.14.1
< date: Tue, 08 Jan 2019 08:46:08 GMT
< content-type: application/json; charset=utf-8
< content-length: 32
< strict-transport-security: max-age=15768000
<
* Curl_http_done: called premature == 0
* Connection #0 to host my.aptly.server left intact
["testfile/aptly-test-file.txt"]

These requests are, as you can see, all done through Nginx, but I have tried to also talk directly to the aptly API on port 8080, which gives me the exact same results.

For completeness, this is the relevant part in the Nginx config that handles requests to /api/:

location /api/ {
  client_max_body_size 100M;
  auth_basic "Restricted";
  auth_basic_user_file /etc/nginx/.htpasswd.aptly;
  proxy_redirect	off;
  proxy_pass	http://localhost:8080/api/;
  proxy_redirect	http://localhost:8080/api/ /api;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $remote_addr;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header Host $http_host;
  proxy_set_header Origin "";
}

And this is the supervisor config, used to start the aptly api serve:

[program:aptly]
command=/usr/bin/aptly api serve -no-lock
directory=/home/aptly
user=aptly
stdout_logfile=/var/log/supervisor/aptly-stdout.log
stderr_logfile=/var/log/supervisor/aptly-stderr.log

Improve CLI response format

Presently, CLI returns JSON received from Aptly API. It might be good to consider retuning something a bit more user friendly to read on command line.

$ aptly-cli repo_upload --dir test1 --name ssr-precise -trace
{"FailedFiles"=>[], "Report"=>{"Warnings"=>[], "Added"=>["mariadb-common_5.5.32+maria-1~precise_all added"], "Removed"=>[]}}

would look like...

$ aptly-cli repo_upload --dir test1 --name ssr-precise -trace
Added: mariadb-common_5.5.32+maria-1~precise_all

or if it failed to add...

$ aptly-cli repo_upload --dir test1 --name ssr-precise -trace
FailedFiles: mariadb-common_5.5.32+maria-1~precise_all

Basically,
puts ["FailedFiles"] if not empty.
puts ["Report"]["Warnings"] if not empty
puts ["Report"]["Added"] if not empty
puts ["Report"]["Removed"] if not empty

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.