mirror of
https://github.com/clearlinux/docker.git
synced 2026-08-22 05:56:17 +00:00
Compare commits
31 Commits
docs
..
v0.7.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
| e245abcc4d | |||
| 19c53c3833 | |||
| 51db855bc3 | |||
| 8c1fae65d3 | |||
| 018d3178e3 | |||
| 2bae258bff | |||
| 394d2f4ea7 | |||
| 457a9260bc | |||
| e4fbeddf53 | |||
| a32741b1c2 | |||
| a5dbb9a900 | |||
| 21b9c9c7b4 | |||
| 828ba3494d | |||
| 583340bd56 | |||
| 12d3e3e644 | |||
| e574bbf55e | |||
| 377bfc3ad2 | |||
| c7d854af01 | |||
| a712b7467e | |||
| 248e0c63db | |||
| 20c962696f | |||
| 234587e0ef | |||
| 78adacde93 | |||
| 2e6329a904 | |||
| 3e6d3ad1e4 | |||
| 1b3baab4a8 | |||
| c69549e016 | |||
| 760648e33e | |||
| 36b1be2acf | |||
| e1be306389 | |||
| 2a76af221f |
@@ -1,2 +0,0 @@
|
|||||||
bundles
|
|
||||||
.gopath
|
|
||||||
+13
-31
@@ -1,37 +1,19 @@
|
|||||||
# Docker project generated files to ignore
|
|
||||||
# if you want to ignore files created by your editor/tools,
|
|
||||||
# please consider a global .gitignore https://help.github.com/articles/ignoring-files
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.orig
|
|
||||||
*.rej
|
|
||||||
*.test
|
|
||||||
.*.swp
|
|
||||||
.DS_Store
|
|
||||||
.bashrc
|
|
||||||
.dotcloud
|
|
||||||
.flymake*
|
|
||||||
.git/
|
|
||||||
.gopath/
|
|
||||||
.hg/
|
|
||||||
.vagrant*
|
.vagrant*
|
||||||
Vagrantfile
|
|
||||||
a.out
|
|
||||||
autogen/
|
|
||||||
bin
|
bin
|
||||||
build_src
|
|
||||||
bundles/
|
|
||||||
docker/docker
|
docker/docker
|
||||||
docs/AWS_S3_BUCKET
|
.*.swp
|
||||||
docs/GITCOMMIT
|
a.out
|
||||||
docs/GIT_BRANCH
|
*.orig
|
||||||
docs/VERSION
|
build_src
|
||||||
|
.flymake*
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
docs/_build
|
docs/_build
|
||||||
docs/_static
|
docs/_static
|
||||||
docs/_templates
|
docs/_templates
|
||||||
docs/changed-files
|
.gopath/
|
||||||
# generated by man/man/md2man-all.sh
|
.dotcloud
|
||||||
man/man1
|
*.test
|
||||||
man/man5
|
bundles/
|
||||||
pyenv
|
.hg/
|
||||||
vendor/pkg/
|
.git/
|
||||||
|
|||||||
@@ -1,56 +1,24 @@
|
|||||||
# Generate AUTHORS: hack/generate-authors.sh
|
# Generate AUTHORS: git log --format='%aN <%aE>' | sort -uf | grep -v vagrant-ubuntu-12
|
||||||
|
<charles.hooper@dotcloud.com> <chooper@plumata.com>
|
||||||
# Tip for finding duplicates (besides scanning the output of AUTHORS for name
|
|
||||||
# duplicates that aren't also email duplicates): scan the output of:
|
|
||||||
# git log --format='%aE - %aN' | sort -uf
|
|
||||||
#
|
|
||||||
# For explanation on this file format: man git-shortlog
|
|
||||||
|
|
||||||
Patrick Stapleton <github@gdi2290.com>
|
|
||||||
Shishir Mahajan <shishir.mahajan@redhat.com> <smahajan@redhat.com>
|
|
||||||
Erwin van der Koogh <info@erronis.nl>
|
|
||||||
Ahmed Kamal <email.ahmedkamal@googlemail.com>
|
|
||||||
Tejesh Mehta <tejesh.mehta@gmail.com> <tj@init.me>
|
|
||||||
Cristian Staretu <cristian.staretu@gmail.com>
|
|
||||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
|
|
||||||
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
|
|
||||||
Marcus Linke <marcus.linke@gmx.de>
|
|
||||||
Aleksandrs Fadins <aleks@s-ko.net>
|
|
||||||
Christopher Latham <sudosurootdev@gmail.com>
|
|
||||||
Hu Keping <hukeping@huawei.com>
|
|
||||||
Wayne Chang <wayne@neverfear.org>
|
|
||||||
Chen Chao <cc272309126@gmail.com>
|
|
||||||
Daehyeok Mun <daehyeok@gmail.com>
|
|
||||||
<daehyeok@gmail.com> <daehyeok@daehyeokui-MacBook-Air.local>
|
|
||||||
<jt@yadutaf.fr> <admin@jtlebi.fr>
|
|
||||||
<jeff@docker.com> <jefferya@programmerq.net>
|
|
||||||
<charles.hooper@dotcloud.com> <chooper@plumata.com>
|
|
||||||
<daniel.mizyrycki@dotcloud.com> <daniel@dotcloud.com>
|
<daniel.mizyrycki@dotcloud.com> <daniel@dotcloud.com>
|
||||||
<daniel.mizyrycki@dotcloud.com> <mzdaniel@glidelink.net>
|
<daniel.mizyrycki@dotcloud.com> <mzdaniel@glidelink.net>
|
||||||
Guillaume J. Charmes <guillaume.charmes@docker.com> <charmes.guillaume@gmail.com>
|
Guillaume J. Charmes <guillaume.charmes@dotcloud.com> <charmes.guillaume@gmail.com>
|
||||||
<guillaume.charmes@docker.com> <guillaume@dotcloud.com>
|
<guillaume.charmes@dotcloud.com> <guillaume@dotcloud.com>
|
||||||
<guillaume.charmes@docker.com> <guillaume@docker.com>
|
|
||||||
<guillaume.charmes@docker.com> <guillaume.charmes@dotcloud.com>
|
|
||||||
<guillaume.charmes@docker.com> <guillaume@charmes.net>
|
|
||||||
<kencochrane@gmail.com> <KenCochrane@gmail.com>
|
<kencochrane@gmail.com> <KenCochrane@gmail.com>
|
||||||
Thatcher Peskens <thatcher@docker.com>
|
<sridharr@activestate.com> <github@srid.name>
|
||||||
Thatcher Peskens <thatcher@docker.com> <thatcher@dotcloud.com>
|
Thatcher Peskens <thatcher@dotcloud.com> dhrp <thatcher@dotcloud.com>
|
||||||
Thatcher Peskens <thatcher@docker.com> dhrp <thatcher@gmx.net>
|
Thatcher Peskens <thatcher@dotcloud.com> dhrp <thatcher@gmx.net>
|
||||||
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com> jpetazzo <jerome.petazzoni@dotcloud.com>
|
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com> jpetazzo <jerome.petazzoni@dotcloud.com>
|
||||||
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com> <jp@enix.org>
|
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com> <jp@enix.org>
|
||||||
Joffrey F <joffrey@docker.com>
|
Joffrey F <joffrey@dotcloud.com>
|
||||||
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
|
<joffrey@dotcloud.com> <f.joffrey@gmail.com>
|
||||||
Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
|
|
||||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||||
Andy Smith <github@anarkystic.com>
|
Andy Smith <github@anarkystic.com>
|
||||||
<kalessin@kalessin.fr> <louis@dotcloud.com>
|
<kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||||
<victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
<victor.vieux@dotcloud.com> <victor@dotcloud.com>
|
||||||
<victor.vieux@docker.com> <victor@dotcloud.com>
|
<victor.vieux@dotcloud.com> <dev@vvieux.com>
|
||||||
<victor.vieux@docker.com> <dev@vvieux.com>
|
|
||||||
<victor.vieux@docker.com> <victor@docker.com>
|
|
||||||
<victor.vieux@docker.com> <vieux@docker.com>
|
|
||||||
<victor.vieux@docker.com> <victorvieux@gmail.com>
|
|
||||||
<dominik@honnef.co> <dominikh@fork-bomb.org>
|
<dominik@honnef.co> <dominikh@fork-bomb.org>
|
||||||
|
Thatcher Peskens <thatcher@dotcloud.com>
|
||||||
<ehanchrow@ine.com> <eric.hanchrow@gmail.com>
|
<ehanchrow@ine.com> <eric.hanchrow@gmail.com>
|
||||||
Walter Stanish <walter@pratyeka.org>
|
Walter Stanish <walter@pratyeka.org>
|
||||||
<daniel@gasienica.ch> <dgasienica@zynga.com>
|
<daniel@gasienica.ch> <dgasienica@zynga.com>
|
||||||
@@ -58,114 +26,3 @@ Roberto Hashioka <roberto_hashioka@hotmail.com>
|
|||||||
Konstantin Pelykh <kpelykh@zettaset.com>
|
Konstantin Pelykh <kpelykh@zettaset.com>
|
||||||
David Sissitka <me@dsissitka.com>
|
David Sissitka <me@dsissitka.com>
|
||||||
Nolan Darilek <nolan@thewordnerd.info>
|
Nolan Darilek <nolan@thewordnerd.info>
|
||||||
<mastahyeti@gmail.com> <mastahyeti@users.noreply.github.com>
|
|
||||||
Benoit Chesneau <bchesneau@gmail.com>
|
|
||||||
Jordan Arentsen <blissdev@gmail.com>
|
|
||||||
Daniel Garcia <daniel@danielgarcia.info>
|
|
||||||
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
|
||||||
Bhiraj Butala <abhiraj.butala@gmail.com>
|
|
||||||
Faiz Khan <faizkhan00@gmail.com>
|
|
||||||
Victor Lyuboslavsky <victor@victoreda.com>
|
|
||||||
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
|
|
||||||
Matthew Mueller <mattmuelle@gmail.com>
|
|
||||||
<mosoni@ebay.com> <mohitsoni1989@gmail.com>
|
|
||||||
Shih-Yuan Lee <fourdollars@gmail.com>
|
|
||||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com> root <root@vagrant-ubuntu-12.10.vagrantup.com>
|
|
||||||
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
|
|
||||||
<proppy@google.com> <proppy@aminche.com>
|
|
||||||
<michael@docker.com> <michael@crosbymichael.com>
|
|
||||||
<michael@docker.com> <crosby.michael@gmail.com>
|
|
||||||
<michael@docker.com> <crosbymichael@gmail.com>
|
|
||||||
<github@developersupport.net> <github@metaliveblog.com>
|
|
||||||
<brandon@ifup.org> <brandon@ifup.co>
|
|
||||||
<dano@spotify.com> <daniel.norberg@gmail.com>
|
|
||||||
<danny@codeaholics.org> <Danny.Yates@mailonline.co.uk>
|
|
||||||
<gurjeet@singh.im> <singh.gurjeet@gmail.com>
|
|
||||||
<shawn@churchofgit.com> <shawnlandden@gmail.com>
|
|
||||||
<sjoerd-github@linuxonly.nl> <sjoerd@byte.nl>
|
|
||||||
<solomon@docker.com> <solomon.hykes@dotcloud.com>
|
|
||||||
<solomon@docker.com> <solomon@dotcloud.com>
|
|
||||||
<solomon@docker.com> <s@docker.com>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@fosiki.com>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@docker.com>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au> <¨SvenDowideit@home.org.au¨>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
|
|
||||||
Sven Dowideit <SvenDowideit@home.org.au> <sven@t440s.home.gateway>
|
|
||||||
<alexl@redhat.com> <alexander.larsson@gmail.com>
|
|
||||||
Alexandr Morozov <lk4d4math@gmail.com>
|
|
||||||
<git.nivoc@neverbox.com> <kuehnle@online.de>
|
|
||||||
O.S. Tezer <ostezer@gmail.com>
|
|
||||||
<ostezer@gmail.com> <ostezer@users.noreply.github.com>
|
|
||||||
Roberto G. Hashioka <roberto.hashioka@docker.com> <roberto_hashioka@hotmail.com>
|
|
||||||
<justin.p.simonelis@gmail.com> <justin.simonelis@PTS-JSIMON2.toronto.exclamation.com>
|
|
||||||
<taim@bosboot.org> <maztaim@users.noreply.github.com>
|
|
||||||
<viktor.vojnovski@amadeus.com> <vojnovski@gmail.com>
|
|
||||||
<vbatts@redhat.com> <vbatts@hashbangbash.com>
|
|
||||||
<altsysrq@gmail.com> <iamironbob@gmail.com>
|
|
||||||
Sridhar Ratnakumar <sridharr@activestate.com>
|
|
||||||
Sridhar Ratnakumar <sridharr@activestate.com> <github@srid.name>
|
|
||||||
Liang-Chi Hsieh <viirya@gmail.com>
|
|
||||||
Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
Will Weaver <monkey@buildingbananas.com>
|
|
||||||
Timothy Hobbs <timothyhobbs@seznam.cz>
|
|
||||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathan.leclaire@gmail.com>
|
|
||||||
Nathan LeClaire <nathan.leclaire@docker.com> <nathanleclaire@gmail.com>
|
|
||||||
<github@hollensbe.org> <erik+github@hollensbe.org>
|
|
||||||
<github@albersweb.de> <albers@users.noreply.github.com>
|
|
||||||
<lsm5@fedoraproject.org> <lsm5@redhat.com>
|
|
||||||
<marc@marc-abramowitz.com> <msabramo@gmail.com>
|
|
||||||
Matthew Heon <mheon@redhat.com> <mheon@mheonlaptop.redhat.com>
|
|
||||||
<bernat@luffy.cx> <vincent@bernat.im>
|
|
||||||
<p@pwaller.net> <peter@scraperwiki.com>
|
|
||||||
<andrew.weiss@outlook.com> <andrew.weiss@microsoft.com>
|
|
||||||
Francisco Carriedo <fcarriedo@gmail.com>
|
|
||||||
<julienbordellier@gmail.com> <git@julienbordellier.com>
|
|
||||||
<ahmetb@microsoft.com> <ahmetalpbalkan@gmail.com>
|
|
||||||
<lk4d4@docker.com> <lk4d4math@gmail.com>
|
|
||||||
<arnaud.porterie@docker.com> <icecrime@gmail.com>
|
|
||||||
<baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
|
||||||
Brian Goff <cpuguy83@gmail.com>
|
|
||||||
<cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
|
|
||||||
<ewindisch@docker.com> <eric@windisch.us>
|
|
||||||
<frank.rosquin+github@gmail.com> <frank.rosquin@gmail.com>
|
|
||||||
Hollie Teal <hollie@docker.com>
|
|
||||||
<hollie@docker.com> <hollie.teal@docker.com>
|
|
||||||
<hollie@docker.com> <hollietealok@users.noreply.github.com>
|
|
||||||
<huu@prismskylabs.com> <whoshuu@gmail.com>
|
|
||||||
Jessica Frazelle <jess@docker.com> Jessie Frazelle <jfrazelle@users.noreply.github.com>
|
|
||||||
<jess@docker.com> <jfrazelle@users.noreply.github.com>
|
|
||||||
<konrad.wilhelm.kleine@gmail.com> <kwk@users.noreply.github.com>
|
|
||||||
<tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
|
|
||||||
<estesp@linux.vnet.ibm.com> <estesp@gmail.com>
|
|
||||||
<github@gone.nl> <thaJeztah@users.noreply.github.com>
|
|
||||||
Thomas LEVEIL <thomasleveil@gmail.com> Thomas LÉVEIL <thomasleveil@users.noreply.github.com>
|
|
||||||
<oi@truffles.me.uk> <timruffles@googlemail.com>
|
|
||||||
<Vincent.Bernat@exoscale.ch> <bernat@luffy.cx>
|
|
||||||
Antonio Murdaca <antonio.murdaca@gmail.com> <me@runcom.ninja>
|
|
||||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@linux.com>
|
|
||||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@users.noreply.github.com>
|
|
||||||
Darren Shepherd <darren.s.shepherd@gmail.com> <darren@rancher.com>
|
|
||||||
Deshi Xiao <dxiao@redhat.com> <dsxiao@dataman-inc.com>
|
|
||||||
Deshi Xiao <dxiao@redhat.com> <xiaods@gmail.com>
|
|
||||||
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
|
|
||||||
Jacob Atzen <jacob@jacobatzen.dk> <jatzen@gmail.com>
|
|
||||||
Jeff Nickoloff <jeff.nickoloff@gmail.com> <jeff@allingeek.com>
|
|
||||||
<jess@docker.com> <princess@docker.com>
|
|
||||||
John Howard (VM) <John.Howard@microsoft.com> John Howard <jhoward@microsoft.com>
|
|
||||||
Madhu Venugopal <madhu@socketplane.io> <madhu@docker.com>
|
|
||||||
Mary Anthony <mary.anthony@docker.com> <mary@docker.com>
|
|
||||||
Mary Anthony <mary.anthony@docker.com> moxiegirl <mary@docker.com>
|
|
||||||
Mary Anthony <mary.anthony@docker.com> <moxieandmore@gmail.com>
|
|
||||||
mattyw <mattyw@me.com> <gh@mattyw.net>
|
|
||||||
resouer <resouer@163.com> <resouer@gmail.com>
|
|
||||||
AJ Bowen <aj@gandi.net> soulshake <amy@gandi.net>
|
|
||||||
AJ Bowen <aj@gandi.net> soulshake <aj@gandi.net>
|
|
||||||
Tibor Vass <teabee89@gmail.com> <tibor@docker.com>
|
|
||||||
Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com>
|
|
||||||
Vincent Bernat <bernat@luffy.cx> <Vincent.Bernat@exoscale.ch>
|
|
||||||
Yestin Sun <sunyi0804@gmail.com> <yestin.sun@polyera.com>
|
|
||||||
bin liu <liubin0329@users.noreply.github.com> <liubin0329@gmail.com>
|
|
||||||
John Howard (VM) <John.Howard@microsoft.com> jhowardmsft <jhoward@microsoft.com>
|
|
||||||
Ankush Agarwal <ankushagarwal11@gmail.com> <ankushagarwal@users.noreply.github.com>
|
|
||||||
Tangi COLIN <tangicolin@gmail.com> tangicolin <tangicolin@gmail.com>
|
|
||||||
|
|||||||
+330
-1730
File diff suppressed because it is too large
Load Diff
+67
-401
@@ -1,435 +1,101 @@
|
|||||||
# Contributing to Docker
|
# Contributing to Docker
|
||||||
|
|
||||||
Want to hack on Docker? Awesome! We have a contributor's guide that explains
|
Want to hack on Docker? Awesome! Here are instructions to get you started. They are probably not perfect, please let us know if anything feels
|
||||||
[setting up a Docker development environment and the contribution
|
wrong or incomplete.
|
||||||
process](https://docs.docker.com/project/who-written-for/).
|
|
||||||
|
|
||||||

|
## Contribution guidelines
|
||||||
|
|
||||||
This page contains information about reporting issues as well as some tips and
|
### Pull requests are always welcome
|
||||||
guidelines useful to experienced open source contributors. Finally, make sure
|
|
||||||
you read our [community guidelines](#docker-community-guidelines) before you
|
|
||||||
start participating.
|
|
||||||
|
|
||||||
## Topics
|
We are always thrilled to receive pull requests, and do our best to
|
||||||
|
process them as fast as possible. Not sure if that typo is worth a pull
|
||||||
|
request? Do it! We will appreciate it.
|
||||||
|
|
||||||
* [Reporting Security Issues](#reporting-security-issues)
|
If your pull request is not accepted on the first try, don't be
|
||||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
discouraged! If there's a problem with the implementation, hopefully you
|
||||||
* [Reporting Issues](#reporting-other-issues)
|
received feedback on what to improve.
|
||||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
|
||||||
* [Community Guidelines](#docker-community-guidelines)
|
|
||||||
|
|
||||||
## Reporting security issues
|
We're trying very hard to keep Docker lean and focused. We don't want it
|
||||||
|
to do everything for everybody. This means that we might decide against
|
||||||
|
incorporating a new feature. However, there might be a way to implement
|
||||||
|
that feature *on top of* docker.
|
||||||
|
|
||||||
The Docker maintainers take security seriously. If you discover a security
|
### Discuss your design on the mailing list
|
||||||
issue, please bring it to their attention right away!
|
|
||||||
|
|
||||||
Please **DO NOT** file a public issue, instead send your report privately to
|
We recommend discussing your plans [on the mailing
|
||||||
[security@docker.com](mailto:security@docker.com),
|
list](https://groups.google.com/forum/?fromgroups#!forum/docker-dev)
|
||||||
|
before starting to code - especially for more ambitious contributions.
|
||||||
|
This gives other contributors a chance to point you in the right
|
||||||
|
direction, give feedback on your design, and maybe point out if someone
|
||||||
|
else is working on the same thing.
|
||||||
|
|
||||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
### Create issues...
|
||||||
We also like to send gifts—if you're into Docker schwag, make sure to let
|
|
||||||
us know. We currently do not offer a paid security bounty program, but are not
|
|
||||||
ruling it out in the future.
|
|
||||||
|
|
||||||
|
Any significant improvement should be documented as [a github
|
||||||
|
issue](https://github.com/dotcloud/docker/issues) before anybody
|
||||||
|
starts working on it.
|
||||||
|
|
||||||
## Reporting other issues
|
### ...but check for existing issues first!
|
||||||
|
|
||||||
A great way to contribute to the project is to send a detailed report when you
|
|
||||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
|
||||||
and will thank you for it!
|
|
||||||
|
|
||||||
Check that [our issue database](https://github.com/docker/docker/issues)
|
|
||||||
doesn't already include that problem or suggestion before submitting an issue.
|
|
||||||
If you find a match, add a quick "+1" or "I have this problem too." Doing this
|
|
||||||
helps prioritize the most common problems and requests.
|
|
||||||
|
|
||||||
When reporting issues, please include your host OS (Ubuntu 12.04, Fedora 19,
|
|
||||||
etc). Please include:
|
|
||||||
|
|
||||||
* The output of `uname -a`.
|
|
||||||
* The output of `docker version`.
|
|
||||||
* The output of `docker -D info`.
|
|
||||||
|
|
||||||
Please also include the steps required to reproduce the problem if possible and
|
|
||||||
applicable. This information will help us review and fix your issue faster.
|
|
||||||
|
|
||||||
**Issue Report Template**:
|
|
||||||
|
|
||||||
```
|
|
||||||
Description of problem:
|
|
||||||
|
|
||||||
|
|
||||||
`docker version`:
|
|
||||||
|
|
||||||
|
|
||||||
`docker info`:
|
|
||||||
|
|
||||||
|
|
||||||
`uname -a`:
|
|
||||||
|
|
||||||
|
|
||||||
Environment details (AWS, VirtualBox, physical, etc.):
|
|
||||||
|
|
||||||
|
|
||||||
How reproducible:
|
|
||||||
|
|
||||||
|
|
||||||
Steps to Reproduce:
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
|
|
||||||
Actual Results:
|
|
||||||
|
|
||||||
|
|
||||||
Expected Results:
|
|
||||||
|
|
||||||
|
|
||||||
Additional info:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##Quick contribution tips and guidelines
|
|
||||||
|
|
||||||
This section gives the experienced contributor some tips and guidelines.
|
|
||||||
|
|
||||||
###Pull requests are always welcome
|
|
||||||
|
|
||||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
|
||||||
it? Do it! We will appreciate it. Any significant improvement should be
|
|
||||||
documented as [a GitHub issue](https://github.com/docker/docker/issues) before
|
|
||||||
anybody starts working on it.
|
|
||||||
|
|
||||||
We are always thrilled to receive pull requests. We do our best to process them
|
|
||||||
quickly. If your pull request is not accepted on the first try,
|
|
||||||
don't get discouraged! Our contributor's guide explains [the review process we
|
|
||||||
use for simple changes](https://docs.docker.com/project/make-a-contribution/).
|
|
||||||
|
|
||||||
### Design and cleanup proposals
|
|
||||||
|
|
||||||
You can propose new designs for existing Docker features. You can also design
|
|
||||||
entirely new features. We really appreciate contributors who want to refactor or
|
|
||||||
otherwise cleanup our project. For information on making these types of
|
|
||||||
contributions, see [the advanced contribution
|
|
||||||
section](https://docs.docker.com/project/advanced-contributing/) in the
|
|
||||||
contributors guide.
|
|
||||||
|
|
||||||
We try hard to keep Docker lean and focused. Docker can't do everything for
|
|
||||||
everybody. This means that we might decide against incorporating a new feature.
|
|
||||||
However, there might be a way to implement that feature *on top of* Docker.
|
|
||||||
|
|
||||||
### Talking to other Docker users and contributors
|
|
||||||
|
|
||||||
<table class="tg">
|
|
||||||
<col width="45%">
|
|
||||||
<col width="65%">
|
|
||||||
<tr>
|
|
||||||
<td>Internet Relay Chat (IRC)</td>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
IRC a direct line to our most knowledgeable Docker users; we have
|
|
||||||
both the <code>#docker</code> and <code>#docker-dev</code> group on
|
|
||||||
<strong>irc.freenode.net</strong>.
|
|
||||||
IRC is a rich chat protocol but it can overwhelm new users. You can search
|
|
||||||
<a href="https://botbot.me/freenode/docker/#" target="_blank">our chat archives</a>.
|
|
||||||
</p>
|
|
||||||
Read our <a href="https://docs.docker.com/project/get-help/#irc-quickstart" target="_blank">IRC quickstart guide</a> for an easy way to get started.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Google Groups</td>
|
|
||||||
<td>
|
|
||||||
There are two groups.
|
|
||||||
<a href="https://groups.google.com/forum/#!forum/docker-user" target="_blank">Docker-user</a>
|
|
||||||
is for people using Docker containers.
|
|
||||||
The <a href="https://groups.google.com/forum/#!forum/docker-dev" target="_blank">docker-dev</a>
|
|
||||||
group is for contributors and other people contributing to the Docker
|
|
||||||
project.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Twitter</td>
|
|
||||||
<td>
|
|
||||||
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
|
|
||||||
to get updates on our products. You can also tweet us questions or just
|
|
||||||
share blogs or stories.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Stack Overflow</td>
|
|
||||||
<td>
|
|
||||||
Stack Overflow has over 7000K Docker questions listed. We regularly
|
|
||||||
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
|
|
||||||
and so do many other knowledgeable Docker users.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
Please take a moment to check that an issue doesn't already exist
|
||||||
|
documenting your bug report or improvement proposal. If it does, it
|
||||||
|
never hurts to add a quick "+1" or "I have this problem too". This will
|
||||||
|
help prioritize the most common problems and requests.
|
||||||
|
|
||||||
### Conventions
|
### Conventions
|
||||||
|
|
||||||
Fork the repository and make changes on your fork in a feature branch:
|
Fork the repo and make changes on your fork in a feature branch:
|
||||||
|
|
||||||
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
- If it's a bugfix branch, name it XXX-something where XXX is the number of the
|
||||||
the issue.
|
issue
|
||||||
- If it's a feature branch, create an enhancement issue to announce
|
- If it's a feature branch, create an enhancement issue to announce your
|
||||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
intentions, and name it XXX-something where XXX is the number of the issue.
|
||||||
issue.
|
|
||||||
|
|
||||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||||
it! Take a look at existing tests for inspiration. [Run the full test
|
it! Take a look at existing tests for inspiration. Run the full test suite on
|
||||||
suite](https://docs.docker.com/project/test-and-docs/) on your branch before
|
your branch before submitting a pull request.
|
||||||
submitting a pull request.
|
|
||||||
|
|
||||||
Update the documentation when creating or modifying features. Test your
|
Make sure you include relevant updates or additions to documentation when
|
||||||
documentation changes for clarity, concision, and correctness, as well as a
|
creating or modifying features.
|
||||||
clean documentation build. See our contributors guide for [our style
|
|
||||||
guide](https://docs.docker.com/project/doc-style) and instructions on [building
|
|
||||||
the documentation](https://docs.docker.com/project/test-and-docs/#build-and-test-the-documentation).
|
|
||||||
|
|
||||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
and maintenance. Always run `go fmt` before committing your changes. Most
|
||||||
committing your changes. Most editors have plug-ins that do this automatically.
|
editors have plugins that do this automatically, and there's also a git
|
||||||
|
pre-commit hook:
|
||||||
|
|
||||||
Pull request descriptions should be as clear as possible and include a reference
|
```
|
||||||
to all the issues that they address.
|
curl -o .git/hooks/pre-commit https://raw.github.com/edsrzf/gofmt-git-hook/master/fmt-check && chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
Pull requests descriptions should be as clear as possible and include a
|
||||||
written in the imperative, followed by an optional, more detailed explanatory
|
reference to all the issues that they address.
|
||||||
text which is separated from the summary by an empty line.
|
|
||||||
|
|
||||||
Code review comments may be added to your pull request. Discuss, then make the
|
Code review comments may be added to your pull request. Discuss, then make the
|
||||||
suggested modifications and push additional commits to your feature branch. Post
|
suggested modifications and push additional commits to your feature branch. Be
|
||||||
a comment after pushing. New commits show up in the pull request automatically,
|
sure to post a comment after pushing. The new commits will show up in the pull
|
||||||
but the reviewers are notified only when you comment.
|
request automatically, but the reviewers will not be notified unless you
|
||||||
|
comment.
|
||||||
|
|
||||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
Before the pull request is merged, make sure that you squash your commits into
|
||||||
mixed into the PR.
|
logical units of work using `git rebase -i` and `git push -f`. After every
|
||||||
|
commit the test suite should be passing. Include documentation changes in the
|
||||||
|
same commit so that a revert would remove all traces of the feature or fix.
|
||||||
|
|
||||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
Commits that fix or close an issue should include a reference like `Closes #XXX`
|
||||||
feature branch to update your pull request rather than `merge master`.
|
or `Fixes #XXX`, which will automatically close the issue when merged.
|
||||||
|
|
||||||
Before you make a pull request, squash your commits into logical units of work
|
Add your name to the AUTHORS file, but make sure the list is sorted and your
|
||||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
name and email address match your git configuration. The AUTHORS file is
|
||||||
set of patches that should be reviewed together: for example, upgrading the
|
regenerated occasionally from the git commit history, so a mismatch may result
|
||||||
version of a vendored dependency and taking advantage of its now available new
|
in your changes being overwritten.
|
||||||
feature constitute two separate units of work. Implementing a new function and
|
|
||||||
calling it in another file constitute a single logical unit of work. The very
|
|
||||||
high majority of submissions should have a single commit, so if in doubt: squash
|
|
||||||
down to one.
|
|
||||||
|
|
||||||
After every commit, [make sure the test suite passes]
|
|
||||||
((https://docs.docker.com/project/test-and-docs/)). Include documentation
|
|
||||||
changes in the same pull request so that a revert would remove all traces of
|
|
||||||
the feature or fix.
|
|
||||||
|
|
||||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that
|
|
||||||
close an issue. Including references automatically closes the issue on a merge.
|
|
||||||
|
|
||||||
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
|
||||||
from the Git history.
|
|
||||||
|
|
||||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
|
||||||
|
|
||||||
### Merge approval
|
|
||||||
|
|
||||||
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
|
||||||
indicate acceptance.
|
|
||||||
|
|
||||||
A change requires LGTMs from an absolute majority of the maintainers of each
|
|
||||||
component affected. For example, if a change affects `docs/` and `registry/`, it
|
|
||||||
needs an absolute majority from the maintainers of `docs/` AND, separately, an
|
|
||||||
absolute majority of the maintainers of `registry/`.
|
|
||||||
|
|
||||||
For more details, see the [MAINTAINERS](MAINTAINERS) page.
|
|
||||||
|
|
||||||
### Sign your work
|
|
||||||
|
|
||||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
|
||||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
|
||||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
|
||||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
|
||||||
|
|
||||||
```
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you just add a line to every git commit message:
|
|
||||||
|
|
||||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
|
||||||
|
|
||||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
|
||||||
|
|
||||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
|
||||||
commit automatically with `git commit -s`.
|
|
||||||
|
|
||||||
Note that the old-style `Docker-DCO-1.1-Signed-off-by: ...` format is still
|
|
||||||
accepted, so there is no need to update outstanding pull requests to the new
|
|
||||||
format right away, but please do adjust your processes for future contributions.
|
|
||||||
|
|
||||||
### How can I become a maintainer?
|
### How can I become a maintainer?
|
||||||
|
|
||||||
* Step 1: Learn the component inside out
|
* Step 1: learn the component inside out
|
||||||
* Step 2: Make yourself useful by contributing code, bug fixes, support etc.
|
* Step 2: make yourself useful by contributing code, bugfixes, support etc.
|
||||||
* Step 3: Volunteer on the IRC channel (#docker at Freenode)
|
* Step 3: volunteer on the irc channel (#docker@freenode)
|
||||||
* Step 4: Propose yourself at a scheduled docker meeting in #docker-dev
|
|
||||||
|
|
||||||
Don't forget: being a maintainer is a time investment. Make sure you
|
Don't forget: being a maintainer is a time investment. Make sure you will have time to make yourself available.
|
||||||
will have time to make yourself available. You don't have to be a
|
You don't have to be a maintainer to make a difference on the project!
|
||||||
maintainer to make a difference on the project!
|
|
||||||
|
|
||||||
### IRC meetings
|
|
||||||
|
|
||||||
There are two monthly meetings taking place on #docker-dev IRC to accommodate all
|
|
||||||
timezones. Anybody can propose a topic for discussion prior to the meeting.
|
|
||||||
|
|
||||||
If you feel the conversation is going off-topic, feel free to point it out.
|
|
||||||
|
|
||||||
For the exact dates and times, have a look at [the irc-minutes
|
|
||||||
repo](https://github.com/docker/irc-minutes). The minutes also contain all the
|
|
||||||
notes from previous meetings.
|
|
||||||
|
|
||||||
## Docker community guidelines
|
|
||||||
|
|
||||||
We want to keep the Docker community awesome, growing and collaborative. We need
|
|
||||||
your help to keep it that way. To help with this we've come up with some general
|
|
||||||
guidelines for the community as a whole:
|
|
||||||
|
|
||||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
|
||||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
|
||||||
nice people way better than mean ones!
|
|
||||||
|
|
||||||
* Encourage diversity and participation: Make everyone in our community feel
|
|
||||||
welcome, regardless of their background and the extent of their
|
|
||||||
contributions, and do everything possible to encourage participation in
|
|
||||||
our community.
|
|
||||||
|
|
||||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
|
||||||
you own, do not share private or sensitive information, and don't break
|
|
||||||
the law.
|
|
||||||
|
|
||||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
|
||||||
avoid off-topic discussions. Remember when you update an issue or respond
|
|
||||||
to an email you are potentially sending to a large number of people. Please
|
|
||||||
consider this before you update. Also remember that nobody likes spam.
|
|
||||||
|
|
||||||
### Guideline violations — 3 strikes method
|
|
||||||
|
|
||||||
The point of this section is not to find opportunities to punish people, but we
|
|
||||||
do need a fair way to deal with people who are making our community suck.
|
|
||||||
|
|
||||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
|
||||||
behavior is inappropriate according to our guidelines.
|
|
||||||
|
|
||||||
2. Second occurrence: We will send you a private message with a warning that
|
|
||||||
any additional violations will result in removal from the community.
|
|
||||||
|
|
||||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
|
||||||
your account.
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
|
|
||||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
|
||||||
have spam all over the place.
|
|
||||||
|
|
||||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
|
||||||
grudge.
|
|
||||||
|
|
||||||
* People who commit minor infractions will get some education, rather than
|
|
||||||
hammering them in the 3 strikes process.
|
|
||||||
|
|
||||||
* The rules apply equally to everyone in the community, no matter how much
|
|
||||||
you've contributed.
|
|
||||||
|
|
||||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
|
||||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
|
||||||
|
|
||||||
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
|
|
||||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
|
||||||
fair solution if there has been a misunderstanding.
|
|
||||||
|
|
||||||
## Coding Style
|
|
||||||
|
|
||||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
|
||||||
community. While some of these standards may seem arbitrary, they somehow seem
|
|
||||||
to result in a solid, consistent codebase.
|
|
||||||
|
|
||||||
It is possible that the code base does not currently comply with these
|
|
||||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
|
||||||
goes against the spirit of the guidelines. All new contributions should make a
|
|
||||||
best effort to clean up and make the code base better than they left it.
|
|
||||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
|
||||||
code base easier for humans to navigate and understand. Always keep that in
|
|
||||||
mind when nudging others to comply.
|
|
||||||
|
|
||||||
The rules:
|
|
||||||
|
|
||||||
1. All code should be formatted with `gofmt -s`.
|
|
||||||
2. All code should pass the default levels of
|
|
||||||
[`golint`](https://github.com/golang/lint).
|
|
||||||
3. All code should follow the guidelines covered in [Effective
|
|
||||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
|
||||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
|
||||||
4. Comment the code. Tell us the why, the history and the context.
|
|
||||||
5. Document _all_ declarations and methods, even private ones. Declare
|
|
||||||
expectations, caveats and anything else that may be important. If a type
|
|
||||||
gets exported, having the comments already there will ensure it's ready.
|
|
||||||
6. Variable name length should be proportional to it's context and no longer.
|
|
||||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
|
||||||
In practice, short methods will have short variable names and globals will
|
|
||||||
have longer names.
|
|
||||||
7. No underscores in package names. If you need a compound name, step back,
|
|
||||||
and re-examine why you need a compound name. If you still think you need a
|
|
||||||
compound name, lose the underscore.
|
|
||||||
8. No utils or helpers packages. If a function is not general enough to
|
|
||||||
warrant it's own package, it has not been written generally enough to be a
|
|
||||||
part of a util package. Just leave it unexported and well-documented.
|
|
||||||
9. All tests should run with `go test` and outside tooling should not be
|
|
||||||
required. No, we don't need another unit testing framework. Assertion
|
|
||||||
packages are acceptable if they provide _real_ incremental value.
|
|
||||||
10. Even though we call these "rules" above, they are actually just
|
|
||||||
guidelines. Since you've read all the rules, you now know that.
|
|
||||||
|
|
||||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
|
||||||
reading through [Effective Go](http://golang.org/doc/effective_go.html). The
|
|
||||||
[Go Blog](http://blog.golang.org/) is also a great resource. Drinking the
|
|
||||||
kool-aid is a lot easier than going thirsty.
|
|
||||||
|
|||||||
+37
-188
@@ -4,214 +4,63 @@
|
|||||||
#
|
#
|
||||||
# # Assemble the full dev environment. This is slow the first time.
|
# # Assemble the full dev environment. This is slow the first time.
|
||||||
# docker build -t docker .
|
# docker build -t docker .
|
||||||
|
# # Apparmor messes with privileged mode: disable it
|
||||||
|
# /etc/init.d/apparmor stop ; /etc/init.d/apparmor teardown
|
||||||
#
|
#
|
||||||
# # Mount your source in an interactive container for quick testing:
|
# # Mount your source in an interactive container for quick testing:
|
||||||
# docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash
|
# docker run -v `pwd`:/go/src/github.com/dotcloud/docker -privileged -lxc-conf=lxc.aa_profile=unconfined -i -t docker bash
|
||||||
|
#
|
||||||
#
|
#
|
||||||
# # Run the test suite:
|
# # Run the test suite:
|
||||||
# docker run --privileged docker hack/make.sh test
|
# docker run -privileged -lxc-conf=lxc.aa_profile=unconfined docker go test -v
|
||||||
#
|
#
|
||||||
# # Publish a release:
|
# # Publish a release:
|
||||||
# docker run --privileged \
|
# docker run -privileged -lxc-conf=lxc.aa_profile=unconfined \
|
||||||
# -e AWS_S3_BUCKET=baz \
|
# -e AWS_S3_BUCKET=baz \
|
||||||
# -e AWS_ACCESS_KEY=foo \
|
# -e AWS_ACCESS_KEY=foo \
|
||||||
# -e AWS_SECRET_KEY=bar \
|
# -e AWS_SECRET_KEY=bar \
|
||||||
# -e GPG_PASSPHRASE=gloubiboulga \
|
# -e GPG_PASSPHRASE=gloubiboulga \
|
||||||
# docker hack/release.sh
|
# docker hack/release.sh
|
||||||
#
|
#
|
||||||
# Note: AppArmor used to mess with privileged mode, but this is no longer
|
|
||||||
# the case. Therefore, you don't have to disable it anymore.
|
|
||||||
#
|
|
||||||
|
|
||||||
FROM ubuntu:14.04
|
docker-version 0.6.1
|
||||||
MAINTAINER Tianon Gravi <admwiggin@gmail.com> (@tianon)
|
from ubuntu:12.04
|
||||||
|
maintainer Solomon Hykes <solomon@dotcloud.com>
|
||||||
|
|
||||||
RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys E871F18B51E0147C77796AC81196BA81F6B0FC61
|
# Build dependencies
|
||||||
RUN echo deb http://ppa.launchpad.net/zfs-native/stable/ubuntu trusty main > /etc/apt/sources.list.d/zfs.list
|
run echo 'deb http://archive.ubuntu.com/ubuntu precise main universe' > /etc/apt/sources.list
|
||||||
|
run apt-get update
|
||||||
# Packaged dependencies
|
run apt-get install -y -q curl
|
||||||
RUN apt-get update && apt-get install -y \
|
run apt-get install -y -q git
|
||||||
apparmor \
|
run apt-get install -y -q mercurial
|
||||||
aufs-tools \
|
run apt-get install -y -q build-essential libsqlite3-dev
|
||||||
automake \
|
|
||||||
bash-completion \
|
|
||||||
btrfs-tools \
|
|
||||||
build-essential \
|
|
||||||
createrepo \
|
|
||||||
curl \
|
|
||||||
dpkg-sig \
|
|
||||||
git \
|
|
||||||
iptables \
|
|
||||||
libapparmor-dev \
|
|
||||||
libcap-dev \
|
|
||||||
libsqlite3-dev \
|
|
||||||
mercurial \
|
|
||||||
parallel \
|
|
||||||
python-mock \
|
|
||||||
python-pip \
|
|
||||||
python-websocket \
|
|
||||||
reprepro \
|
|
||||||
ruby1.9.1 \
|
|
||||||
ruby1.9.1-dev \
|
|
||||||
s3cmd=1.1.0* \
|
|
||||||
ubuntu-zfs \
|
|
||||||
libzfs-dev \
|
|
||||||
--no-install-recommends
|
|
||||||
|
|
||||||
# Get lvm2 source for compiling statically
|
|
||||||
RUN git clone -b v2_02_103 https://git.fedorahosted.org/git/lvm2.git /usr/local/lvm2
|
|
||||||
# see https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags
|
|
||||||
|
|
||||||
# Compile and install lvm2
|
|
||||||
RUN cd /usr/local/lvm2 \
|
|
||||||
&& ./configure --enable-static_link \
|
|
||||||
&& make device-mapper \
|
|
||||||
&& make install_device-mapper
|
|
||||||
# see https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL
|
|
||||||
|
|
||||||
# Install lxc
|
|
||||||
ENV LXC_VERSION 1.1.2
|
|
||||||
RUN mkdir -p /usr/src/lxc \
|
|
||||||
&& curl -sSL https://linuxcontainers.org/downloads/lxc/lxc-${LXC_VERSION}.tar.gz | tar -v -C /usr/src/lxc/ -xz --strip-components=1
|
|
||||||
RUN cd /usr/src/lxc \
|
|
||||||
&& ./configure \
|
|
||||||
&& make \
|
|
||||||
&& make install \
|
|
||||||
&& ldconfig
|
|
||||||
|
|
||||||
# Install Go
|
# Install Go
|
||||||
ENV GO_VERSION 1.4.2
|
run curl -s https://go.googlecode.com/files/go1.2rc1.src.tar.gz | tar -v -C /usr/local -xz
|
||||||
RUN curl -sSL https://golang.org/dl/go${GO_VERSION}.src.tar.gz | tar -v -C /usr/local -xz \
|
run cd /usr/local/go/src && ./make.bash && go install -ldflags '-w -linkmode external -extldflags "-static -Wl,--unresolved-symbols=ignore-in-shared-libs"' -tags netgo -a std
|
||||||
&& mkdir -p /go/bin
|
env PATH /usr/local/go/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
env GOPATH /go:/go/src/github.com/dotcloud/docker/vendor
|
||||||
ENV GOPATH /go:/go/src/github.com/docker/docker/vendor
|
run cd /tmp && echo 'package main' > t.go && go test -a -i -v
|
||||||
RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1
|
# Ubuntu stuff
|
||||||
|
run apt-get install -y -q ruby1.9.3 rubygems libffi-dev
|
||||||
|
run gem install --no-rdoc --no-ri fpm
|
||||||
|
run apt-get install -y -q reprepro dpkg-sig
|
||||||
|
|
||||||
# Compile Go for cross compilation
|
# Install s3cmd 1.0.1 (earlier versions don't support env variables in the config)
|
||||||
ENV DOCKER_CROSSPLATFORMS \
|
run apt-get install -y -q python-pip
|
||||||
linux/386 linux/arm \
|
run pip install s3cmd
|
||||||
darwin/amd64 darwin/386 \
|
run pip install python-magic
|
||||||
freebsd/amd64 freebsd/386 freebsd/arm \
|
run /bin/echo -e '[default]\naccess_key=$AWS_ACCESS_KEY\nsecret_key=$AWS_SECRET_KEY\n' > /.s3cfg
|
||||||
windows/amd64 windows/386
|
|
||||||
|
|
||||||
# (set an explicit GOARM of 5 for maximum compatibility)
|
# Runtime dependencies
|
||||||
ENV GOARM 5
|
run apt-get install -y -q iptables
|
||||||
RUN cd /usr/local/go/src \
|
run apt-get install -y -q lxc
|
||||||
&& set -x \
|
|
||||||
&& for platform in $DOCKER_CROSSPLATFORMS; do \
|
|
||||||
GOOS=${platform%/*} \
|
|
||||||
GOARCH=${platform##*/} \
|
|
||||||
./make.bash --no-clean 2>&1; \
|
|
||||||
done
|
|
||||||
|
|
||||||
# This has been commented out and kept as reference because we don't support compiling with older Go anymore.
|
volume /var/lib/docker
|
||||||
# ENV GOFMT_VERSION 1.3.3
|
workdir /go/src/github.com/dotcloud/docker
|
||||||
# RUN curl -sSL https://storage.googleapis.com/golang/go${GOFMT_VERSION}.$(go env GOOS)-$(go env GOARCH).tar.gz | tar -C /go/bin -xz --strip-components=2 go/bin/gofmt
|
|
||||||
|
|
||||||
# Update this sha when we upgrade to go 1.5.0
|
|
||||||
ENV GO_TOOLS_COMMIT 069d2f3bcb68257b627205f0486d6cc69a231ff9
|
|
||||||
# Grab Go's cover tool for dead-simple code coverage testing
|
|
||||||
# Grab Go's vet tool for examining go code to find suspicious constructs
|
|
||||||
# and help prevent errors that the compiler might not catch
|
|
||||||
RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
|
||||||
&& (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) \
|
|
||||||
&& go install -v golang.org/x/tools/cmd/cover \
|
|
||||||
&& go install -v golang.org/x/tools/cmd/vet
|
|
||||||
# Grab Go's lint tool
|
|
||||||
ENV GO_LINT_COMMIT f42f5c1c440621302702cb0741e9d2ca547ae80f
|
|
||||||
RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \
|
|
||||||
&& (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \
|
|
||||||
&& go install -v github.com/golang/lint/golint
|
|
||||||
|
|
||||||
# TODO replace FPM with some very minimal debhelper stuff
|
|
||||||
RUN gem install --no-rdoc --no-ri fpm --version 1.3.2
|
|
||||||
|
|
||||||
# Install registry
|
|
||||||
ENV REGISTRY_COMMIT 2317f721a3d8428215a2b65da4ae85212ed473b4
|
|
||||||
RUN set -x \
|
|
||||||
&& export GOPATH="$(mktemp -d)" \
|
|
||||||
&& git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \
|
|
||||||
&& (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \
|
|
||||||
&& GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \
|
|
||||||
go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \
|
|
||||||
&& rm -rf "$GOPATH"
|
|
||||||
|
|
||||||
# Install notary server
|
|
||||||
ENV NOTARY_COMMIT 8e8122eb5528f621afcd4e2854c47302f17392f7
|
|
||||||
RUN set -x \
|
|
||||||
&& export GOPATH="$(mktemp -d)" \
|
|
||||||
&& git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \
|
|
||||||
&& (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_COMMIT") \
|
|
||||||
&& GOPATH="$GOPATH/src/github.com/docker/notary/Godeps/_workspace:$GOPATH" \
|
|
||||||
go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \
|
|
||||||
&& rm -rf "$GOPATH"
|
|
||||||
|
|
||||||
# Get the "docker-py" source so we can run their integration tests
|
|
||||||
ENV DOCKER_PY_COMMIT 8a87001d09852058f08a807ab6e8491d57ca1e88
|
|
||||||
RUN git clone https://github.com/docker/docker-py.git /docker-py \
|
|
||||||
&& cd /docker-py \
|
|
||||||
&& git checkout -q $DOCKER_PY_COMMIT
|
|
||||||
|
|
||||||
# Setup s3cmd config
|
|
||||||
RUN { \
|
|
||||||
echo '[default]'; \
|
|
||||||
echo 'access_key=$AWS_ACCESS_KEY'; \
|
|
||||||
echo 'secret_key=$AWS_SECRET_KEY'; \
|
|
||||||
} > ~/.s3cfg
|
|
||||||
|
|
||||||
# Set user.email so crosbymichael's in-container merge commits go smoothly
|
|
||||||
RUN git config --global user.email 'docker-dummy@example.com'
|
|
||||||
|
|
||||||
# Add an unprivileged user to be used for tests which need it
|
|
||||||
RUN groupadd -r docker
|
|
||||||
RUN useradd --create-home --gid docker unprivilegeduser
|
|
||||||
|
|
||||||
VOLUME /var/lib/docker
|
|
||||||
WORKDIR /go/src/github.com/docker/docker
|
|
||||||
ENV DOCKER_BUILDTAGS apparmor selinux
|
|
||||||
|
|
||||||
# Let us use a .bashrc file
|
|
||||||
RUN ln -sfv $PWD/.bashrc ~/.bashrc
|
|
||||||
|
|
||||||
# Register Docker's bash completion.
|
|
||||||
RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker
|
|
||||||
|
|
||||||
# Get useful and necessary Hub images so we can "docker load" locally instead of pulling
|
|
||||||
COPY contrib/download-frozen-image.sh /go/src/github.com/docker/docker/contrib/
|
|
||||||
RUN ./contrib/download-frozen-image.sh /docker-frozen-images \
|
|
||||||
busybox:latest@8c2e06607696bd4afb3d03b687e361cc43cf8ec1a4a725bc96e39f05ba97dd55 \
|
|
||||||
hello-world:frozen@91c95931e552b11604fea91c2f537284149ec32fff0f700a4769cfd31d7696ae \
|
|
||||||
jess/unshare@5c9f6ea50341a2a8eb6677527f2bdedbf331ae894a41714fda770fb130f3314d
|
|
||||||
# see also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is)
|
|
||||||
|
|
||||||
# Download man page generator
|
|
||||||
RUN set -x \
|
|
||||||
&& export GOPATH="$(mktemp -d)" \
|
|
||||||
&& git clone -b v1.0.3 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
|
|
||||||
&& git clone -b v1.2 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
|
|
||||||
&& go get -v -d github.com/cpuguy83/go-md2man \
|
|
||||||
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
|
|
||||||
&& rm -rf "$GOPATH"
|
|
||||||
|
|
||||||
# Download toml validator
|
|
||||||
ENV TOMLV_COMMIT 9baf8a8a9f2ed20a8e54160840c492f937eeaf9a
|
|
||||||
RUN set -x \
|
|
||||||
&& export GOPATH="$(mktemp -d)" \
|
|
||||||
&& git clone https://github.com/BurntSushi/toml.git "$GOPATH/src/github.com/BurntSushi/toml" \
|
|
||||||
&& (cd "$GOPATH/src/github.com/BurntSushi/toml" && git checkout -q "$TOMLV_COMMIT") \
|
|
||||||
&& go build -v -o /usr/local/bin/tomlv github.com/BurntSushi/toml/cmd/tomlv \
|
|
||||||
&& rm -rf "$GOPATH"
|
|
||||||
|
|
||||||
# Build/install the tool for embedding resources in Windows binaries
|
|
||||||
ENV RSRC_COMMIT e48dbf1b7fc464a9e85fcec450dddf80816b76e0
|
|
||||||
RUN set -x \
|
|
||||||
&& git clone https://github.com/akavel/rsrc.git /go/src/github.com/akavel/rsrc \
|
|
||||||
&& cd /go/src/github.com/akavel/rsrc \
|
|
||||||
&& git checkout -q $RSRC_COMMIT \
|
|
||||||
&& go install -v
|
|
||||||
|
|
||||||
# Wrap all commands in the "docker-in-docker" script to allow nested containers
|
# Wrap all commands in the "docker-in-docker" script to allow nested containers
|
||||||
ENTRYPOINT ["hack/dind"]
|
entrypoint ["hack/dind"]
|
||||||
|
|
||||||
# Upload docker source
|
# Upload docker source
|
||||||
COPY . /go/src/github.com/docker/docker
|
add . /go/src/github.com/dotcloud/docker
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# docker build -t docker:simple -f Dockerfile.simple .
|
|
||||||
# docker run --rm docker:simple hack/make.sh dynbinary
|
|
||||||
# docker run --rm --privileged docker:simple hack/dind hack/make.sh test-unit
|
|
||||||
# docker run --rm --privileged -v /var/lib/docker docker:simple hack/dind hack/make.sh dynbinary test-integration-cli
|
|
||||||
|
|
||||||
# This represents the bare minimum required to build and test Docker.
|
|
||||||
|
|
||||||
FROM debian:jessie
|
|
||||||
|
|
||||||
# compile and runtime deps
|
|
||||||
# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#build-dependencies
|
|
||||||
# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#runtime-dependencies
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
btrfs-tools \
|
|
||||||
curl \
|
|
||||||
gcc \
|
|
||||||
git \
|
|
||||||
golang \
|
|
||||||
libdevmapper-dev \
|
|
||||||
libsqlite3-dev \
|
|
||||||
\
|
|
||||||
ca-certificates \
|
|
||||||
e2fsprogs \
|
|
||||||
iptables \
|
|
||||||
procps \
|
|
||||||
xz-utils \
|
|
||||||
\
|
|
||||||
aufs-tools \
|
|
||||||
lxc \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV AUTO_GOPATH 1
|
|
||||||
WORKDIR /usr/src/docker
|
|
||||||
COPY . /usr/src/docker
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
## FIXME
|
||||||
|
|
||||||
|
This file is a loose collection of things to improve in the codebase, for the internal
|
||||||
|
use of the maintainers.
|
||||||
|
|
||||||
|
They are not big enough to be in the roadmap, not user-facing enough to be github issues,
|
||||||
|
and not important enough to be discussed in the mailing list.
|
||||||
|
|
||||||
|
They are just like FIXME comments in the source code, except we're not sure where in the source
|
||||||
|
to put them - so we put them here :)
|
||||||
|
|
||||||
|
|
||||||
|
* Merge Runtime, Server and Builder into Runtime
|
||||||
|
* Run linter on codebase
|
||||||
|
* Unify build commands and regular commands
|
||||||
|
* Move source code into src/ subdir for clarity
|
||||||
|
* Clean up the Makefile, it's a mess
|
||||||
|
* docker build: on non-existent local path for ADD, don't show full absolute path on the host
|
||||||
|
* mount into /dockerinit rather than /sbin/init
|
||||||
|
* docker tag foo REPO:TAG
|
||||||
|
* use size header for progress bar in pull
|
||||||
|
* Clean up context upload in build!!!
|
||||||
|
* Parallel pull
|
||||||
|
* Ensure /proc/sys/net/ipv4/ip_forward is 1
|
||||||
|
* Force DNS to public!
|
||||||
|
* Always generate a resolv.conf per container, to avoid changing resolv.conf under thne container's feet
|
||||||
|
* Save metadata with import/export
|
||||||
|
* Upgrade dockerd without stopping containers
|
||||||
|
* bring back git revision info, looks like it was lost
|
||||||
|
* Simple command to remove all untagged images
|
||||||
|
* Simple command to clean up containers for disk space
|
||||||
|
* Caching after an ADD
|
||||||
|
* entry point config
|
||||||
|
* bring back git revision info, looks like it was lost
|
||||||
|
* Clean up the ProgressReader api, it's a PITA to use
|
||||||
|
* Use netlink instead of iproute2/iptables (#925)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
https://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
@@ -176,13 +176,24 @@
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
Copyright 2013-2015 Docker, Inc.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
https://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|||||||
+6
-618
@@ -1,618 +1,6 @@
|
|||||||
# Docker maintainers file
|
Solomon Hykes <solomon@dotcloud.com> (@shykes)
|
||||||
#
|
Guillaume Charmes <guillaume@dotcloud.com> (@creack)
|
||||||
# This file describes who runs the Docker project and how.
|
Victor Vieux <victor@dotcloud.com> (@vieux)
|
||||||
# This is a living document - if you see something out of date or missing,
|
Michael Crosby <michael@crosbymichael.com> (@crosbymichael)
|
||||||
# speak up!
|
api.go: Victor Vieux <victor@dotcloud.com> (@vieux)
|
||||||
#
|
Vagrantfile: Daniel Mizyrycki <daniel@dotcloud.com> (@mzdaniel)
|
||||||
# It is structured to be consumable by both humans and programs.
|
|
||||||
# To extract its contents programmatically, use any TOML-compliant
|
|
||||||
# parser.
|
|
||||||
|
|
||||||
[Rules]
|
|
||||||
|
|
||||||
[Rules.maintainers]
|
|
||||||
|
|
||||||
title = "What is a maintainer?"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
There are different types of maintainers, with different responsibilities, but
|
|
||||||
all maintainers have 3 things in common:
|
|
||||||
|
|
||||||
1) They share responsibility in the project's success.
|
|
||||||
2) They have made a long-term, recurring time investment to improve the project.
|
|
||||||
3) They spend that time doing whatever needs to be done, not necessarily what
|
|
||||||
is the most interesting or fun.
|
|
||||||
|
|
||||||
Maintainers are often under-appreciated, because their work is harder to appreciate.
|
|
||||||
It's easy to appreciate a really cool and technically advanced feature. It's harder
|
|
||||||
to appreciate the absence of bugs, the slow but steady improvement in stability,
|
|
||||||
or the reliability of a release process. But those things distinguish a good
|
|
||||||
project from a great one.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.bdfl]
|
|
||||||
|
|
||||||
title = "The Benevolent dictator for life (BDFL)"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
Docker follows the timeless, highly efficient and totally unfair system
|
|
||||||
known as [Benevolent dictator for
|
|
||||||
life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with
|
|
||||||
yours truly, Solomon Hykes, in the role of BDFL. This means that all
|
|
||||||
decisions are made, by default, by Solomon. Since making every decision
|
|
||||||
myself would be highly un-scalable, in practice decisions are spread
|
|
||||||
across multiple maintainers.
|
|
||||||
|
|
||||||
Ideally, the BDFL role is like the Queen of England: awesome crown, but not
|
|
||||||
an actual operational role day-to-day. The real job of a BDFL is to NEVER GO AWAY.
|
|
||||||
Every other rule can change, perhaps drastically so, but the BDFL will always
|
|
||||||
be there, preserving the philosophy and principles of the project, and keeping
|
|
||||||
ultimate authority over its fate. This gives us great flexibility in experimenting
|
|
||||||
with various governance models, knowing that we can always press the "reset" button
|
|
||||||
without fear of fragmentation or deadlock. See the US congress for a counter-example.
|
|
||||||
|
|
||||||
BDFL daily routine:
|
|
||||||
|
|
||||||
* Is the project governance stuck in a deadlock or irreversibly fragmented?
|
|
||||||
* If yes: refactor the project governance
|
|
||||||
* Are there issues or conflicts escalated by core?
|
|
||||||
* If yes: resolve them
|
|
||||||
* Go back to polishing that crown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.decisions]
|
|
||||||
|
|
||||||
title = "How are decisions made?"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
Short answer: EVERYTHING IS A PULL REQUEST.
|
|
||||||
|
|
||||||
Docker is an open-source project with an open design philosophy. This
|
|
||||||
means that the repository is the source of truth for EVERY aspect of the
|
|
||||||
project, including its philosophy, design, road map, and APIs. *If it's
|
|
||||||
part of the project, it's in the repo. If it's in the repo, it's part of
|
|
||||||
the project.*
|
|
||||||
|
|
||||||
As a result, all decisions can be expressed as changes to the
|
|
||||||
repository. An implementation change is a change to the source code. An
|
|
||||||
API change is a change to the API specification. A philosophy change is
|
|
||||||
a change to the philosophy manifesto, and so on.
|
|
||||||
|
|
||||||
All decisions affecting Docker, big and small, follow the same 3 steps:
|
|
||||||
|
|
||||||
* Step 1: Open a pull request. Anyone can do this.
|
|
||||||
|
|
||||||
* Step 2: Discuss the pull request. Anyone can do this.
|
|
||||||
|
|
||||||
* Step 3: Merge or refuse the pull request. Who does this depends on the nature
|
|
||||||
of the pull request and which areas of the project it affects. See *review flow*
|
|
||||||
for details.
|
|
||||||
|
|
||||||
Because Docker is such a large and active project, it's important for everyone to know
|
|
||||||
who is responsible for deciding what. That is determined by a precise set of rules.
|
|
||||||
|
|
||||||
* For every *decision* in the project, the rules should designate, in a deterministic way,
|
|
||||||
who should *decide*.
|
|
||||||
|
|
||||||
* For every *problem* in the project, the rules should designate, in a deterministic way,
|
|
||||||
who should be responsible for *fixing* it.
|
|
||||||
|
|
||||||
* For every *question* in the project, the rules should designate, in a deterministic way,
|
|
||||||
who should be expected to have the *answer*.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.review]
|
|
||||||
|
|
||||||
title = "Review flow"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
Pull requests should be processed according to the following flow:
|
|
||||||
|
|
||||||
* For each subsystem affected by the change, the maintainers of the subsystem must approve or refuse it.
|
|
||||||
It is the responsibility of the subsystem maintainers to process patches affecting them in a timely
|
|
||||||
manner.
|
|
||||||
|
|
||||||
* If the change affects areas of the code which are not part of a subsystem,
|
|
||||||
or if subsystem maintainers are unable to reach a timely decision, it must be approved by
|
|
||||||
the core maintainers.
|
|
||||||
|
|
||||||
* If the change affects the UI or public APIs, or if it represents a major change in architecture,
|
|
||||||
the architects must approve or refuse it.
|
|
||||||
|
|
||||||
* If the change affects the operations of the project, it must be approved or rejected by
|
|
||||||
the relevant operators.
|
|
||||||
|
|
||||||
* If the change affects the governance, philosophy, goals or principles of the project,
|
|
||||||
it must be approved by BDFL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.DCO]
|
|
||||||
|
|
||||||
title = "Helping contributors with the DCO"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
The [DCO or `Sign your work`](
|
|
||||||
https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work)
|
|
||||||
requirement is not intended as a roadblock or speed bump.
|
|
||||||
|
|
||||||
Some Docker contributors are not as familiar with `git`, or have used a web based
|
|
||||||
editor, and thus asking them to `git commit --amend -s` is not the best way forward.
|
|
||||||
|
|
||||||
In this case, maintainers can update the commits based on clause (c) of the DCO. The
|
|
||||||
most trivial way for a contributor to allow the maintainer to do this, is to add
|
|
||||||
a DCO signature in a Pull Requests's comment, or a maintainer can simply note that
|
|
||||||
the change is sufficiently trivial that it does not substantivly change the existing
|
|
||||||
contribution - i.e., a spelling change.
|
|
||||||
|
|
||||||
When you add someone's DCO, please also add your own to keep a log.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.holiday]
|
|
||||||
|
|
||||||
title = "I'm a maintainer, and I'm going on holiday"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
Please let your co-maintainers and other contributors know by raising a pull
|
|
||||||
request that comments out your `MAINTAINERS` file entry using a `#`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules."no direct push"]
|
|
||||||
|
|
||||||
title = "I'm a maintainer. Should I make pull requests too?"
|
|
||||||
|
|
||||||
text = """
|
|
||||||
Yes. Nobody should ever push to master directly. All changes should be
|
|
||||||
made through a pull request.
|
|
||||||
"""
|
|
||||||
|
|
||||||
[Rules.meta]
|
|
||||||
|
|
||||||
title = "How is this process changed?"
|
|
||||||
|
|
||||||
text = "Just like everything else: by making a pull request :)"
|
|
||||||
|
|
||||||
# Current project organization
|
|
||||||
[Org]
|
|
||||||
|
|
||||||
bdfl = "shykes"
|
|
||||||
|
|
||||||
# The chief architect is responsible for the overall integrity of the technical architecture
|
|
||||||
# across all subsystems, and the consistency of APIs and UI.
|
|
||||||
#
|
|
||||||
# Changes to UI, public APIs and overall architecture (for example a plugin system) must
|
|
||||||
# be approved by the chief architect.
|
|
||||||
"Chief Architect" = "shykes"
|
|
||||||
|
|
||||||
# The Chief Operator is responsible for the day-to-day operations of the project including:
|
|
||||||
# - facilitating communications amongst all the contributors;
|
|
||||||
# - tracking release schedules;
|
|
||||||
# - managing the relationship with downstream distributions and upstream dependencies;
|
|
||||||
# - helping new contributors to get involved and become successful contributors and maintainers
|
|
||||||
#
|
|
||||||
# The role is also responsible for managing and measuring the success of the overall project
|
|
||||||
# and ensuring it is governed properly working in concert with the Docker Governance Advisory Board (DGAB).
|
|
||||||
"Chief Operator" = "spf13"
|
|
||||||
|
|
||||||
[Org.Operators]
|
|
||||||
|
|
||||||
# The operators make sure the trains run on time. They are responsible for overall operations
|
|
||||||
# of the project. This includes facilitating communication between all the participants; helping
|
|
||||||
# newcomers get involved and become successful contributors and maintainers; tracking the schedule
|
|
||||||
# of releases; managing the relationship with downstream distributions and upstream dependencies;
|
|
||||||
# define measures of success for the project and measure progress; Devise and implement tools and
|
|
||||||
# processes which make contributors and maintainers happier and more efficient.
|
|
||||||
|
|
||||||
|
|
||||||
[Org.Operators.security]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"erw",
|
|
||||||
"diogomonica",
|
|
||||||
"nathanmccauley"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Operators."monthly meetings"]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"sven",
|
|
||||||
"tianon"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Operators.infrastructure]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"jfrazelle",
|
|
||||||
"crosbymichael"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Operators.community]
|
|
||||||
people = [
|
|
||||||
"theadactyl"
|
|
||||||
]
|
|
||||||
|
|
||||||
# The chief maintainer is responsible for all aspects of quality for the project including
|
|
||||||
# code reviews, usability, stability, security, performance, etc.
|
|
||||||
# The most important function of the chief maintainer is to lead by example. On the first
|
|
||||||
# day of a new maintainer, the best advice should be "follow the C.M.'s example and you'll
|
|
||||||
# be fine".
|
|
||||||
"Chief Maintainer" = "crosbymichael"
|
|
||||||
|
|
||||||
# The community manager is responsible for serving the project community, including users,
|
|
||||||
# contributors and partners. This involves:
|
|
||||||
# - facilitating communication between maintainers, contributors and users
|
|
||||||
# - organizing contributor and maintainer events
|
|
||||||
# - helping new contributors get involved
|
|
||||||
# - anything the project community needs to be successful
|
|
||||||
#
|
|
||||||
# The community manager is a point of contact for any contributor who has questions, concerns
|
|
||||||
# or feedback about project operations.
|
|
||||||
"Community Manager" = "theadactyl"
|
|
||||||
|
|
||||||
[Org."Core maintainers"]
|
|
||||||
|
|
||||||
# The Core maintainers are the ghostbusters of the project: when there's a problem others
|
|
||||||
# can't solve, they show up and fix it with bizarre devices and weaponry.
|
|
||||||
# They have final say on technical implementation and coding style.
|
|
||||||
# They are ultimately responsible for quality in all its forms: usability polish,
|
|
||||||
# bugfixes, performance, stability, etc. When ownership can cleanly be passed to
|
|
||||||
# a subsystem, they are responsible for doing so and holding the
|
|
||||||
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
|
|
||||||
|
|
||||||
# For each release (including minor releases), a "release captain" is assigned from the
|
|
||||||
# pool of core maintainers. Rotation is encouraged across all maintainers, to ensure
|
|
||||||
# the release process is clear and up-to-date.
|
|
||||||
#
|
|
||||||
# It is common for core maintainers to "branch out" to join or start a subsystem.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"calavera",
|
|
||||||
"crosbymichael",
|
|
||||||
"erikh",
|
|
||||||
"estesp",
|
|
||||||
"icecrime",
|
|
||||||
"jfrazelle",
|
|
||||||
"lk4d4",
|
|
||||||
"runcom",
|
|
||||||
"tibor",
|
|
||||||
"unclejack",
|
|
||||||
"vbatts",
|
|
||||||
"vieux",
|
|
||||||
"vishh"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[Org.Subsystems]
|
|
||||||
|
|
||||||
# As the project grows, it gets separated into well-defined subsystems. Each subsystem
|
|
||||||
# has a dedicated group of maintainers, which are dedicated to that subsytem and responsible
|
|
||||||
# for its quality.
|
|
||||||
# This "cellular division" is the primary mechanism for scaling maintenance of the project as it grows.
|
|
||||||
#
|
|
||||||
# The maintainers of each subsytem are responsible for:
|
|
||||||
#
|
|
||||||
# 1. Exposing a clear road map for improving their subsystem.
|
|
||||||
# 2. Deliver prompt feedback and decisions on pull requests affecting their subsystem.
|
|
||||||
# 3. Be available to anyone with questions, bug reports, criticism etc.
|
|
||||||
# on their component. This includes IRC, GitHub requests and the mailing
|
|
||||||
# list.
|
|
||||||
# 4. Make sure their subsystem respects the philosophy, design and
|
|
||||||
# road map of the project.
|
|
||||||
#
|
|
||||||
# #### How to review patches to your subsystem
|
|
||||||
#
|
|
||||||
# Accepting pull requests:
|
|
||||||
#
|
|
||||||
# - If the pull request appears to be ready to merge, give it a `LGTM`, which
|
|
||||||
# stands for "Looks Good To Me".
|
|
||||||
# - If the pull request has some small problems that need to be changed, make
|
|
||||||
# a comment adressing the issues.
|
|
||||||
# - If the changes needed to a PR are small, you can add a "LGTM once the
|
|
||||||
# following comments are adressed..." this will reduce needless back and
|
|
||||||
# forth.
|
|
||||||
# - If the PR only needs a few changes before being merged, any MAINTAINER can
|
|
||||||
# make a replacement PR that incorporates the existing commits and fixes the
|
|
||||||
# problems before a fast track merge.
|
|
||||||
#
|
|
||||||
# Closing pull requests:
|
|
||||||
#
|
|
||||||
# - If a PR appears to be abandoned, after having attempted to contact the
|
|
||||||
# original contributor, then a replacement PR may be made. Once the
|
|
||||||
# replacement PR is made, any contributor may close the original one.
|
|
||||||
# - If you are not sure if the pull request implements a good feature or you
|
|
||||||
# do not understand the purpose of the PR, ask the contributor to provide
|
|
||||||
# more documentation. If the contributor is not able to adequately explain
|
|
||||||
# the purpose of the PR, the PR may be closed by any MAINTAINER.
|
|
||||||
# - If a MAINTAINER feels that the pull request is sufficiently architecturally
|
|
||||||
# flawed, or if the pull request needs significantly more design discussion
|
|
||||||
# before being considered, the MAINTAINER should close the pull request with
|
|
||||||
# a short explanation of what discussion still needs to be had. It is
|
|
||||||
# important not to leave such pull requests open, as this will waste both the
|
|
||||||
# MAINTAINER's time and the contributor's time. It is not good to string a
|
|
||||||
# contributor on for weeks or months, having them make many changes to a PR
|
|
||||||
# that will eventually be rejected.
|
|
||||||
|
|
||||||
[Org.Subsystems.Documentation]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"fredlf",
|
|
||||||
"james",
|
|
||||||
"moxiegirl",
|
|
||||||
"thaJeztah",
|
|
||||||
"jamtur01",
|
|
||||||
"spf13",
|
|
||||||
"sven"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystems.libcontainer]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"crosbymichael",
|
|
||||||
"jnagal",
|
|
||||||
"lk4d4",
|
|
||||||
"mpatel",
|
|
||||||
"vmarmol"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystems.registry]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"dmcg",
|
|
||||||
"dmp42",
|
|
||||||
"jlhawn",
|
|
||||||
"samalba",
|
|
||||||
"sday",
|
|
||||||
"vbatts"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystems."build tools"]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"shykes",
|
|
||||||
"tianon"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystem."remote api"]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"vieux"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystem.swarm]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"aluzzardi",
|
|
||||||
"vieux"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystem.machine]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"bfirsh",
|
|
||||||
"ehazlett"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystem.compose]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"aanand"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Subsystem.builder]
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"duglin",
|
|
||||||
"erikh",
|
|
||||||
"tibor"
|
|
||||||
]
|
|
||||||
|
|
||||||
[Org.Curators]
|
|
||||||
|
|
||||||
# The curators help ensure that incoming issues and pull requests are properly triaged and
|
|
||||||
# that our various contribution and reviewing processes are respected. With their knowledge of
|
|
||||||
# the repository activity, they can also guide contributors to relevant material or
|
|
||||||
# discussions.
|
|
||||||
#
|
|
||||||
# They are neither code nor docs reviewers, so they are never expected to merge. They can
|
|
||||||
# however:
|
|
||||||
# - close an issue or pull request when it's an exact duplicate
|
|
||||||
# - close an issue or pull request when it's inappropriate or off-topic
|
|
||||||
|
|
||||||
people = [
|
|
||||||
"thajeztah"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[people]
|
|
||||||
|
|
||||||
# A reference list of all people associated with the project.
|
|
||||||
# All other sections should refer to people by their canonical key
|
|
||||||
# in the people section.
|
|
||||||
|
|
||||||
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
|
||||||
|
|
||||||
[people.aanand]
|
|
||||||
Name = "Aanand Prasad"
|
|
||||||
Email = "aanand@docker.com"
|
|
||||||
GitHub = "aanand"
|
|
||||||
|
|
||||||
[people.aluzzardi]
|
|
||||||
Name = "Andrea Luzzardi"
|
|
||||||
Email = "aluzzardi@docker.com"
|
|
||||||
GitHub = "aluzzardi"
|
|
||||||
|
|
||||||
[people.bfirsh]
|
|
||||||
Name = "Ben Firshman"
|
|
||||||
Email = "ben@firshman.co.uk"
|
|
||||||
GitHub = "bfirsh"
|
|
||||||
|
|
||||||
[people.calavera]
|
|
||||||
Name = "David Calavera"
|
|
||||||
Email = "david.calavera@gmail.com"
|
|
||||||
GitHub = "calavera"
|
|
||||||
|
|
||||||
[people.cpuguy83]
|
|
||||||
Name = "Brian Goff"
|
|
||||||
Email = "cpuguy83@gmail.com"
|
|
||||||
Github = "cpuguy83"
|
|
||||||
|
|
||||||
[people.crosbymichael]
|
|
||||||
Name = "Michael Crosby"
|
|
||||||
Email = "crosbymichael@gmail.com"
|
|
||||||
GitHub = "crosbymichael"
|
|
||||||
|
|
||||||
[people.diogomonica]
|
|
||||||
Name = "Diogo Monica"
|
|
||||||
Email = "diogo@docker.com"
|
|
||||||
GitHub = "diogomonica"
|
|
||||||
|
|
||||||
[people.duglin]
|
|
||||||
Name = "Doug Davis"
|
|
||||||
Email = "dug@us.ibm.com"
|
|
||||||
GitHub = "duglin"
|
|
||||||
|
|
||||||
[people.dmcg]
|
|
||||||
Name = "Derek McGowan"
|
|
||||||
Email = "derek@docker.com"
|
|
||||||
Github = "dmcgowan"
|
|
||||||
|
|
||||||
[people.dmp42]
|
|
||||||
Name = "Olivier Gambier"
|
|
||||||
Email = "olivier@docker.com"
|
|
||||||
Github = "dmp42"
|
|
||||||
|
|
||||||
[people.ehazlett]
|
|
||||||
Name = "Evan Hazlett"
|
|
||||||
Email = "ejhazlett@gmail.com"
|
|
||||||
GitHub = "ehazlett"
|
|
||||||
|
|
||||||
[people.erikh]
|
|
||||||
Name = "Erik Hollensbe"
|
|
||||||
Email = "erik@docker.com"
|
|
||||||
GitHub = "erikh"
|
|
||||||
|
|
||||||
[people.erw]
|
|
||||||
Name = "Eric Windisch"
|
|
||||||
Email = "eric@windisch.us"
|
|
||||||
GitHub = "ewindisch"
|
|
||||||
|
|
||||||
[people.estesp]
|
|
||||||
Name = "Phil Estes"
|
|
||||||
Email = "estesp@linux.vnet.ibm.com"
|
|
||||||
GitHub = "estesp"
|
|
||||||
|
|
||||||
[people.fredlf]
|
|
||||||
Name = "Fred Lifton"
|
|
||||||
Email = "fred.lifton@docker.com"
|
|
||||||
GitHub = "fredlf"
|
|
||||||
|
|
||||||
[people.icecrime]
|
|
||||||
Name = "Arnaud Porterie"
|
|
||||||
Email = "arnaud@docker.com"
|
|
||||||
GitHub = "icecrime"
|
|
||||||
|
|
||||||
[people.jfrazelle]
|
|
||||||
Name = "Jessie Frazelle"
|
|
||||||
Email = "j@docker.com"
|
|
||||||
GitHub = "jfrazelle"
|
|
||||||
|
|
||||||
[people.jlhawn]
|
|
||||||
Name = "Josh Hawn"
|
|
||||||
Email = "josh.hawn@docker.com"
|
|
||||||
Github = "jlhawn"
|
|
||||||
|
|
||||||
[people.lk4d4]
|
|
||||||
Name = "Alexander Morozov"
|
|
||||||
Email = "lk4d4@docker.com"
|
|
||||||
GitHub = "lk4d4"
|
|
||||||
|
|
||||||
[people.moxiegirl]
|
|
||||||
Name = "Mary Anthony"
|
|
||||||
Email = "mary.anthony@docker.com"
|
|
||||||
GitHub = "moxiegirl"
|
|
||||||
|
|
||||||
[people.nathanmccauley]
|
|
||||||
Name = "Nathan McCauley"
|
|
||||||
Email = "nathan.mccauley@docker.com"
|
|
||||||
GitHub = "nathanmccauley"
|
|
||||||
|
|
||||||
[people.runcom]
|
|
||||||
Name = "Antonio Murdaca"
|
|
||||||
Email = "me@runcom.ninja"
|
|
||||||
GitHub = "runcom"
|
|
||||||
|
|
||||||
[people.sday]
|
|
||||||
Name = "Stephen Day"
|
|
||||||
Email = "stephen.day@docker.com"
|
|
||||||
Github = "stevvooe"
|
|
||||||
|
|
||||||
[people.shykes]
|
|
||||||
Name = "Solomon Hykes"
|
|
||||||
Email = "solomon@docker.com"
|
|
||||||
GitHub = "shykes"
|
|
||||||
|
|
||||||
[people.spf13]
|
|
||||||
Name = "Steve Francia"
|
|
||||||
Email = "steve.francia@gmail.com"
|
|
||||||
GitHub = "spf13"
|
|
||||||
|
|
||||||
[people.sven]
|
|
||||||
Name = "Sven Dowideit"
|
|
||||||
Email = "SvenDowideit@home.org.au"
|
|
||||||
GitHub = "SvenDowideit"
|
|
||||||
|
|
||||||
[people.thajeztah]
|
|
||||||
Name = "Sebastiaan van Stijn"
|
|
||||||
Email = "github@gone.nl"
|
|
||||||
GitHub = "thaJeztah"
|
|
||||||
|
|
||||||
[people.theadactyl]
|
|
||||||
Name = "Thea Lamkin"
|
|
||||||
Email = "thea@docker.com"
|
|
||||||
GitHub = "theadactyl"
|
|
||||||
|
|
||||||
[people.tianon]
|
|
||||||
Name = "Tianon Gravi"
|
|
||||||
Email = "admwiggin@gmail.com"
|
|
||||||
GitHub = "tianon"
|
|
||||||
|
|
||||||
[people.tibor]
|
|
||||||
Name = "Tibor Vass"
|
|
||||||
Email = "tibor@docker.com"
|
|
||||||
GitHub = "tiborvass"
|
|
||||||
|
|
||||||
[people.vbatts]
|
|
||||||
Name = "Vincent Batts"
|
|
||||||
Email = "vbatts@redhat.com"
|
|
||||||
GitHub = "vbatts"
|
|
||||||
|
|
||||||
[people.vieux]
|
|
||||||
Name = "Victor Vieux"
|
|
||||||
Email = "vieux@docker.com"
|
|
||||||
GitHub = "vieux"
|
|
||||||
|
|
||||||
[people.vmarmol]
|
|
||||||
Name = "Victor Marmol"
|
|
||||||
Email = "vmarmol@google.com"
|
|
||||||
GitHub = "vmarmol"
|
|
||||||
|
|
||||||
[people.jnagal]
|
|
||||||
Name = "Rohit Jnagal"
|
|
||||||
Email = "jnagal@google.com"
|
|
||||||
GitHub = "rjnagal"
|
|
||||||
|
|
||||||
[people.mpatel]
|
|
||||||
Name = "Mrunal Patel"
|
|
||||||
Email = "mpatel@redhat.com"
|
|
||||||
GitHub = "mrunalp"
|
|
||||||
|
|
||||||
[people.unclejack]
|
|
||||||
Name = "Cristian Staretu"
|
|
||||||
Email = "cristian.staretu@gmail.com"
|
|
||||||
GitHub = "unclejack"
|
|
||||||
|
|
||||||
[people.vishh]
|
|
||||||
Name = "Vishnu Kannan"
|
|
||||||
Email = "vishnuk@google.com"
|
|
||||||
GitHub = "vishh"
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration-cli test-docker-py validate
|
|
||||||
|
|
||||||
# env vars passed through directly to Docker's build scripts
|
|
||||||
# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily
|
|
||||||
# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these
|
|
||||||
DOCKER_ENVS := \
|
|
||||||
-e BUILDFLAGS \
|
|
||||||
-e DOCKER_CLIENTONLY \
|
|
||||||
-e DOCKER_DEBUG \
|
|
||||||
-e DOCKER_EXECDRIVER \
|
|
||||||
-e DOCKER_EXPERIMENTAL \
|
|
||||||
-e DOCKER_GRAPHDRIVER \
|
|
||||||
-e DOCKER_STORAGE_OPTS \
|
|
||||||
-e DOCKER_USERLANDPROXY \
|
|
||||||
-e TESTDIRS \
|
|
||||||
-e TESTFLAGS \
|
|
||||||
-e TIMEOUT
|
|
||||||
# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds
|
|
||||||
|
|
||||||
# to allow `make BIND_DIR=. shell` or `make BIND_DIR= test`
|
|
||||||
# (default to no bind mount if DOCKER_HOST is set)
|
|
||||||
# note: BINDDIR is supported for backwards-compatibility here
|
|
||||||
BIND_DIR := $(if $(BINDDIR),$(BINDDIR),$(if $(DOCKER_HOST),,bundles))
|
|
||||||
DOCKER_MOUNT := $(if $(BIND_DIR),-v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/docker/docker/$(BIND_DIR)")
|
|
||||||
|
|
||||||
|
|
||||||
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
||||||
DOCKER_IMAGE := docker-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
|
||||||
DOCKER_DOCS_IMAGE := docker-docs$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
|
||||||
|
|
||||||
DOCKER_RUN_DOCKER := docker run --rm -it --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) "$(DOCKER_IMAGE)"
|
|
||||||
|
|
||||||
DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE
|
|
||||||
|
|
||||||
# for some docs workarounds (see below in "docs-build" target)
|
|
||||||
GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
|
||||||
|
|
||||||
default: binary
|
|
||||||
|
|
||||||
all: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh
|
|
||||||
|
|
||||||
binary: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary
|
|
||||||
|
|
||||||
cross: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary cross
|
|
||||||
|
|
||||||
deb: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary build-deb
|
|
||||||
|
|
||||||
rpm: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary build-rpm
|
|
||||||
|
|
||||||
test: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary cross test-unit test-integration-cli test-docker-py
|
|
||||||
|
|
||||||
test-unit: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh test-unit
|
|
||||||
|
|
||||||
test-integration-cli: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary test-integration-cli
|
|
||||||
|
|
||||||
test-docker-py: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh binary test-docker-py
|
|
||||||
|
|
||||||
validate: build
|
|
||||||
$(DOCKER_RUN_DOCKER) hack/make.sh validate-dco validate-gofmt validate-pkg validate-lint validate-test validate-toml validate-vet
|
|
||||||
|
|
||||||
shell: build
|
|
||||||
$(DOCKER_RUN_DOCKER) bash
|
|
||||||
|
|
||||||
build: bundles
|
|
||||||
docker build -t "$(DOCKER_IMAGE)" .
|
|
||||||
|
|
||||||
bundles:
|
|
||||||
mkdir bundles
|
|
||||||
|
|
||||||
docs:
|
|
||||||
$(MAKE) -C docs docs
|
|
||||||
@@ -1,19 +1,43 @@
|
|||||||
Docker
|
Docker
|
||||||
Copyright 2012-2015 Docker, Inc.
|
Copyright 2012-2013 dotCloud, inc.
|
||||||
|
|
||||||
This product includes software developed at Docker, Inc. (https://www.docker.com).
|
This product includes software developed at dotCloud,
|
||||||
|
inc. (http://www.dotcloud.com).
|
||||||
|
|
||||||
This product contains software (https://github.com/kr/pty) developed
|
This product contains software (https://github.com/kr/pty) developed
|
||||||
by Keith Rarick, licensed under the MIT License.
|
by Keith Rarick, licensed under the MIT License.
|
||||||
|
|
||||||
The following is courtesy of our legal counsel:
|
The following is courtesy of our legal counsel:
|
||||||
|
|
||||||
|
Transfers of Docker shall be in accordance with applicable export
|
||||||
|
controls of any country and all other applicable legal requirements.
|
||||||
|
Docker shall not be distributed or downloaded to or in Cuba, Iran,
|
||||||
|
North Korea, Sudan or Syria and shall not be distributed or downloaded
|
||||||
|
to any person on the Denied Persons List administered by the U.S.
|
||||||
|
Department of Commerce.
|
||||||
|
|
||||||
Use and transfer of Docker may be subject to certain restrictions by the
|
What does that mean?
|
||||||
United States and other governments.
|
Here is a further explanation from our legal counsel:
|
||||||
It is your responsibility to ensure that your use and/or transfer does not
|
|
||||||
violate applicable laws.
|
|
||||||
|
|
||||||
For more information, please see https://www.bis.doc.gov
|
Like all software products that utilize cryptography, the export and
|
||||||
|
use of Docker is subject to the U.S. Commerce Department's Export
|
||||||
|
Administration Regulations (EAR) because it uses or contains
|
||||||
|
cryptography (see
|
||||||
|
http://www.bis.doc.gov/index.php/policy-guidance/encryption). Certain
|
||||||
|
free and open source software projects have a lightweight set of
|
||||||
|
requirements, which can generally be met by providing email notice to
|
||||||
|
the appropriate U.S. government agencies that their source code is
|
||||||
|
available on a publicly available repository and making the
|
||||||
|
appropriate statements in the README.
|
||||||
|
|
||||||
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
|
The restrictions of the EAR apply to certain denied locations
|
||||||
|
(currently Iran, Sudan, Syria, North Korea, or Cuba) and those
|
||||||
|
individuals on the Denied Persons List, which is available here:
|
||||||
|
http://www.bis.doc.gov/index.php/policy-guidance/lists-of-parties-of-concern/denied-persons-list.
|
||||||
|
If you are incorporating Docker into a new open source project, the
|
||||||
|
EAR restrictions apply to your incorporation of Docker into your
|
||||||
|
project in the same manner as other cryptography-enabled projects,
|
||||||
|
such as OpenSSL, almost all Linux distributions, etc.
|
||||||
|
|
||||||
|
For more information, see http://www.apache.org/dev/crypto.html and/or
|
||||||
|
seek legal counsel.
|
||||||
|
|||||||
@@ -2,38 +2,32 @@ Docker: the Linux container engine
|
|||||||
==================================
|
==================================
|
||||||
|
|
||||||
Docker is an open source project to pack, ship and run any application
|
Docker is an open source project to pack, ship and run any application
|
||||||
as a lightweight container.
|
as a lightweight container
|
||||||
|
|
||||||
Docker containers are both *hardware-agnostic* and *platform-agnostic*.
|
Docker containers are both *hardware-agnostic* and
|
||||||
This means they can run anywhere, from your laptop to the largest
|
*platform-agnostic*. This means that they can run anywhere, from your
|
||||||
EC2 compute instance and everything in between - and they don't require
|
laptop to the largest EC2 compute instance and everything in between -
|
||||||
you to use a particular language, framework or packaging system. That
|
and they don't require that you use a particular language, framework
|
||||||
makes them great building blocks for deploying and scaling web apps,
|
or packaging system. That makes them great building blocks for
|
||||||
databases, and backend services without depending on a particular stack
|
deploying and scaling web apps, databases and backend services without
|
||||||
or provider.
|
depending on a particular stack or provider.
|
||||||
|
|
||||||
Docker began as an open-source implementation of the deployment engine which
|
Docker is an open-source implementation of the deployment engine which
|
||||||
powers [dotCloud](https://www.dotcloud.com), a popular Platform-as-a-Service.
|
powers [dotCloud](http://dotcloud.com), a popular
|
||||||
It benefits directly from the experience accumulated over several years
|
Platform-as-a-Service. It benefits directly from the experience
|
||||||
of large-scale operation and support of hundreds of thousands of
|
accumulated over several years of large-scale operation and support of
|
||||||
applications and databases.
|
hundreds of thousands of applications and databases.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Security Disclosure
|
|
||||||
|
|
||||||
Security is very important to us. If you have any issue regarding security,
|
|
||||||
please disclose the information responsibly by sending an email to
|
|
||||||
security@docker.com and not by creating a github issue.
|
|
||||||
|
|
||||||
## Better than VMs
|
## Better than VMs
|
||||||
|
|
||||||
A common method for distributing applications and sandboxing their
|
A common method for distributing applications and sandbox their
|
||||||
execution is to use virtual machines, or VMs. Typical VM formats are
|
execution is to use virtual machines, or VMs. Typical VM formats are
|
||||||
VMware's vmdk, Oracle VirtualBox's vdi, and Amazon EC2's ami. In theory
|
VMWare's vmdk, Oracle Virtualbox's vdi, and Amazon EC2's ami. In
|
||||||
these formats should allow every developer to automatically package
|
theory these formats should allow every developer to automatically
|
||||||
their application into a "machine" for easy distribution and deployment.
|
package their application into a "machine" for easy distribution and
|
||||||
In practice, that almost never happens, for a few reasons:
|
deployment. In practice, that almost never happens, for a few reasons:
|
||||||
|
|
||||||
* *Size*: VMs are very large which makes them impractical to store
|
* *Size*: VMs are very large which makes them impractical to store
|
||||||
and transfer.
|
and transfer.
|
||||||
@@ -53,37 +47,39 @@ In practice, that almost never happens, for a few reasons:
|
|||||||
service discovery.
|
service discovery.
|
||||||
|
|
||||||
By contrast, Docker relies on a different sandboxing method known as
|
By contrast, Docker relies on a different sandboxing method known as
|
||||||
*containerization*. Unlike traditional virtualization, containerization
|
*containerization*. Unlike traditional virtualization,
|
||||||
takes place at the kernel level. Most modern operating system kernels
|
containerization takes place at the kernel level. Most modern
|
||||||
now support the primitives necessary for containerization, including
|
operating system kernels now support the primitives necessary for
|
||||||
Linux with [openvz](https://openvz.org),
|
containerization, including Linux with [openvz](http://openvz.org),
|
||||||
[vserver](http://linux-vserver.org) and more recently
|
[vserver](http://linux-vserver.org) and more recently
|
||||||
[lxc](https://linuxcontainers.org/), Solaris with
|
[lxc](http://lxc.sourceforge.net), Solaris with
|
||||||
[zones](https://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc),
|
[zones](http://docs.oracle.com/cd/E26502_01/html/E29024/preface-1.html#scrolltoc)
|
||||||
and FreeBSD with
|
and FreeBSD with
|
||||||
[Jails](https://www.freebsd.org/doc/handbook/jails.html).
|
[Jails](http://www.freebsd.org/doc/handbook/jails.html).
|
||||||
|
|
||||||
Docker builds on top of these low-level primitives to offer developers a
|
Docker builds on top of these low-level primitives to offer developers
|
||||||
portable format and runtime environment that solves all four problems.
|
a portable format and runtime environment that solves all 4
|
||||||
Docker containers are small (and their transfer can be optimized with
|
problems. Docker containers are small (and their transfer can be
|
||||||
layers), they have basically zero memory and cpu overhead, they are
|
optimized with layers), they have basically zero memory and cpu
|
||||||
completely portable, and are designed from the ground up with an
|
overhead, they are completely portable and are designed from the
|
||||||
application-centric design.
|
ground up with an application-centric design.
|
||||||
|
|
||||||
Perhaps best of all, because Docker operates at the OS level, it can still be
|
The best part: because ``docker`` operates at the OS level, it can
|
||||||
run inside a VM!
|
still be run inside a VM!
|
||||||
|
|
||||||
## Plays well with others
|
## Plays well with others
|
||||||
|
|
||||||
Docker does not require you to buy into a particular programming
|
Docker does not require that you buy into a particular programming
|
||||||
language, framework, packaging system, or configuration language.
|
language, framework, packaging system or configuration language.
|
||||||
|
|
||||||
Is your application a Unix process? Does it use files, tcp connections,
|
Is your application a Unix process? Does it use files, tcp
|
||||||
environment variables, standard Unix streams and command-line arguments
|
connections, environment variables, standard Unix streams and
|
||||||
as inputs and outputs? Then Docker can run it.
|
command-line arguments as inputs and outputs? Then ``docker`` can run
|
||||||
|
it.
|
||||||
|
|
||||||
Can your application's build be expressed as a sequence of such
|
Can your application's build be expressed as a sequence of such
|
||||||
commands? Then Docker can build it.
|
commands? Then ``docker`` can build it.
|
||||||
|
|
||||||
|
|
||||||
## Escape dependency hell
|
## Escape dependency hell
|
||||||
|
|
||||||
@@ -100,22 +96,22 @@ This is usually difficult for several reasons:
|
|||||||
typically don't work well with each other, requiring awkward
|
typically don't work well with each other, requiring awkward
|
||||||
custom integrations.
|
custom integrations.
|
||||||
|
|
||||||
* *Conflicting dependencies*. Different applications may depend on
|
* Conflicting dependencies. Different applications may depend on
|
||||||
different versions of the same dependency. Packaging tools handle
|
different versions of the same dependency. Packaging tools handle
|
||||||
these situations with various degrees of ease - but they all
|
these situations with various degrees of ease - but they all
|
||||||
handle them in different and incompatible ways, which again forces
|
handle them in different and incompatible ways, which again forces
|
||||||
the developer to do extra work.
|
the developer to do extra work.
|
||||||
|
|
||||||
* *Custom dependencies*. A developer may need to prepare a custom
|
* Custom dependencies. A developer may need to prepare a custom
|
||||||
version of their application's dependency. Some packaging systems
|
version of their application's dependency. Some packaging systems
|
||||||
can handle custom versions of a dependency, others can't - and all
|
can handle custom versions of a dependency, others can't - and all
|
||||||
of them handle it differently.
|
of them handle it differently.
|
||||||
|
|
||||||
|
|
||||||
Docker solves the problem of dependency hell by giving the developer a simple
|
Docker solves dependency hell by giving the developer a simple way to
|
||||||
way to express *all* their application's dependencies in one place, while
|
express *all* their application's dependencies in one place, and
|
||||||
streamlining the process of assembling them. If this makes you think of
|
streamline the process of assembling them. If this makes you think of
|
||||||
[XKCD 927](https://xkcd.com/927/), don't worry. Docker doesn't
|
[XKCD 927](http://xkcd.com/927/), don't worry. Docker doesn't
|
||||||
*replace* your favorite packaging systems. It simply orchestrates
|
*replace* your favorite packaging systems. It simply orchestrates
|
||||||
their use in a simple and repeatable way. How does it do that? With
|
their use in a simple and repeatable way. How does it do that? With
|
||||||
layers.
|
layers.
|
||||||
@@ -130,10 +126,14 @@ build command inherits the result of the previous commands, the
|
|||||||
Here's a typical Docker build process:
|
Here's a typical Docker build process:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
FROM ubuntu:12.04
|
from ubuntu:12.10
|
||||||
RUN apt-get update && apt-get install -y python python-pip curl
|
run apt-get update
|
||||||
RUN curl -sSL https://github.com/shykes/helloflask/archive/master.tar.gz | tar -xzv
|
run DEBIAN_FRONTEND=noninteractive apt-get install -q -y python
|
||||||
RUN cd helloflask-master && pip install -r requirements.txt
|
run DEBIAN_FRONTEND=noninteractive apt-get install -q -y python-pip
|
||||||
|
run pip install django
|
||||||
|
run DEBIAN_FRONTEND=noninteractive apt-get install -q -y curl
|
||||||
|
run curl -L https://github.com/shykes/helloflask/archive/master.tar.gz | tar -xzv
|
||||||
|
run cd helloflask-master && pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that Docker doesn't care *how* dependencies are built - as long
|
Note that Docker doesn't care *how* dependencies are built - as long
|
||||||
@@ -143,24 +143,22 @@ as they can be built by running a Unix command in a container.
|
|||||||
Getting started
|
Getting started
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Docker can be installed on your local machine as well as servers - both
|
Docker can be installed on your local machine as well as servers - both bare metal and virtualized.
|
||||||
bare metal and virtualized. It is available as a binary on most modern
|
It is available as a binary on most modern Linux systems, or as a VM on Windows, Mac and other systems.
|
||||||
Linux systems, or as a VM on Windows, Mac and other systems.
|
|
||||||
|
|
||||||
We also offer an [interactive tutorial](https://www.docker.com/tryit/)
|
We also offer an interactive tutorial for quickly learning the basics of using Docker.
|
||||||
for quickly learning the basics of using Docker.
|
|
||||||
|
|
||||||
|
For up-to-date install instructions and online tutorials, see the [Getting Started page](http://www.docker.io/gettingstarted/).
|
||||||
|
|
||||||
For up-to-date install instructions, see the [Docs](https://docs.docker.com).
|
|
||||||
|
|
||||||
Usage examples
|
Usage examples
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Docker can be used to run short-lived commands, long-running daemons
|
Docker can be used to run short-lived commands, long-running daemons (app servers, databases etc.),
|
||||||
(app servers, databases, etc.), interactive shell sessions, etc.
|
interactive shell sessions, etc.
|
||||||
|
|
||||||
You can find a [list of real-world
|
You can find a [list of real-world examples](http://docs.docker.io/en/latest/examples/) in the documentation.
|
||||||
examples](https://docs.docker.com/examples/) in the
|
|
||||||
documentation.
|
|
||||||
|
|
||||||
Under the hood
|
Under the hood
|
||||||
--------------
|
--------------
|
||||||
@@ -168,128 +166,37 @@ Under the hood
|
|||||||
Under the hood, Docker is built on the following components:
|
Under the hood, Docker is built on the following components:
|
||||||
|
|
||||||
* The
|
* The
|
||||||
[cgroups](https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt)
|
[cgroup](http://blog.dotcloud.com/kernel-secrets-from-the-paas-garage-part-24-c)
|
||||||
and
|
and
|
||||||
[namespaces](http://man7.org/linux/man-pages/man7/namespaces.7.html)
|
[namespacing](http://blog.dotcloud.com/under-the-hood-linux-kernels-on-dotcloud-part)
|
||||||
capabilities of the Linux kernel
|
capabilities of the Linux kernel;
|
||||||
* The [Go](https://golang.org) programming language
|
* [AUFS](http://aufs.sourceforge.net/aufs.html), a powerful union
|
||||||
* The [Docker Image Specification](https://github.com/docker/docker/blob/master/image/spec/v1.md)
|
filesystem with copy-on-write capabilities;
|
||||||
* The [Libcontainer Specification](https://github.com/docker/libcontainer/blob/master/SPEC.md)
|
* The [Go](http://golang.org) programming language;
|
||||||
|
* [lxc](http://lxc.sourceforge.net/), a set of convenience scripts to
|
||||||
|
simplify the creation of Linux containers.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Contributing to Docker
|
Contributing to Docker
|
||||||
======================
|
======================
|
||||||
|
|
||||||
[](https://godoc.org/github.com/docker/docker)
|
Want to hack on Docker? Awesome! There are instructions to get you
|
||||||
[](https://jenkins.dockerproject.org/job/Docker%20Master/)
|
started [here](CONTRIBUTING.md).
|
||||||
|
|
||||||
Want to hack on Docker? Awesome! We have [instructions to help you get
|
They are probably not perfect, please let us know if anything feels
|
||||||
started contributing code or documentation](https://docs.docker.com/project/who-written-for/).
|
wrong or incomplete.
|
||||||
|
|
||||||
These instructions are probably not perfect, please let us know if anything
|
|
||||||
feels wrong or incomplete. Better yet, submit a PR and improve them yourself.
|
|
||||||
|
|
||||||
Getting the development builds
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Want to run Docker from a master build? You can download
|
|
||||||
master builds at [master.dockerproject.org](https://master.dockerproject.org).
|
|
||||||
They are updated with each commit merged into the master branch.
|
|
||||||
|
|
||||||
Don't know how to use that super cool new feature in the master build? Check
|
|
||||||
out the master docs at
|
|
||||||
[docs.master.dockerproject.org](http://docs.master.dockerproject.org).
|
|
||||||
|
|
||||||
How the project is run
|
|
||||||
======================
|
|
||||||
|
|
||||||
Docker is a very, very active project. If you want to learn more about how it is run,
|
|
||||||
or want to get more involved, the best place to start is [the project directory](https://github.com/docker/docker/tree/master/project).
|
|
||||||
|
|
||||||
We are always open to suggestions on process improvements, and are always looking for more maintainers.
|
|
||||||
|
|
||||||
### Talking to other Docker users and contributors
|
|
||||||
|
|
||||||
<table class="tg">
|
|
||||||
<col width="45%">
|
|
||||||
<col width="65%">
|
|
||||||
<tr>
|
|
||||||
<td>Internet Relay Chat (IRC)</td>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
IRC a direct line to our most knowledgeable Docker users; we have
|
|
||||||
both the <code>#docker</code> and <code>#docker-dev</code> group on
|
|
||||||
<strong>irc.freenode.net</strong>.
|
|
||||||
IRC is a rich chat protocol but it can overwhelm new users. You can search
|
|
||||||
<a href="https://botbot.me/freenode/docker/#" target="_blank">our chat archives</a>.
|
|
||||||
</p>
|
|
||||||
Read our <a href="https://docs.docker.com/project/get-help/#irc-quickstart" target="_blank">IRC quickstart guide</a> for an easy way to get started.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Google Groups</td>
|
|
||||||
<td>
|
|
||||||
There are two groups.
|
|
||||||
<a href="https://groups.google.com/forum/#!forum/docker-user" target="_blank">Docker-user</a>
|
|
||||||
is for people using Docker containers.
|
|
||||||
The <a href="https://groups.google.com/forum/#!forum/docker-dev" target="_blank">docker-dev</a>
|
|
||||||
group is for contributors and other people contributing to the Docker
|
|
||||||
project.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Twitter</td>
|
|
||||||
<td>
|
|
||||||
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
|
|
||||||
to get updates on our products. You can also tweet us questions or just
|
|
||||||
share blogs or stories.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Stack Overflow</td>
|
|
||||||
<td>
|
|
||||||
Stack Overflow has over 7000K Docker questions listed. We regularly
|
|
||||||
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
|
|
||||||
and so do many other knowledgeable Docker users.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
|
|
||||||
*Brought to you courtesy of our legal counsel. For more context,
|
*Brought to you courtesy of our legal counsel. For more context,
|
||||||
please see the [NOTICE](https://github.com/docker/docker/blob/master/NOTICE) document in this repo.*
|
please see the Notice document.*
|
||||||
|
|
||||||
Use and transfer of Docker may be subject to certain restrictions by the
|
Transfers of Docker shall be in accordance with applicable export
|
||||||
United States and other governments.
|
controls of any country and all other applicable legal requirements.
|
||||||
|
Docker shall not be distributed or downloaded to or in Cuba, Iran,
|
||||||
|
North Korea, Sudan or Syria and shall not be distributed or downloaded
|
||||||
|
to any person on the Denied Persons List administered by the U.S.
|
||||||
|
Department of Commerce.
|
||||||
|
|
||||||
It is your responsibility to ensure that your use and/or transfer does not
|
|
||||||
violate applicable laws.
|
|
||||||
|
|
||||||
For more information, please see https://www.bis.doc.gov
|
|
||||||
|
|
||||||
|
|
||||||
Licensing
|
|
||||||
=========
|
|
||||||
Docker is licensed under the Apache License, Version 2.0. See
|
|
||||||
[LICENSE](https://github.com/docker/docker/blob/master/LICENSE) for the full
|
|
||||||
license text.
|
|
||||||
|
|
||||||
Other Docker Related Projects
|
|
||||||
=============================
|
|
||||||
There are a number of projects under development that are based on Docker's
|
|
||||||
core technology. These projects expand the tooling built around the
|
|
||||||
Docker platform to broaden its application and utility.
|
|
||||||
|
|
||||||
* [Docker Registry](https://github.com/docker/distribution): Registry
|
|
||||||
server for Docker (hosting/delivery of repositories and images)
|
|
||||||
* [Docker Machine](https://github.com/docker/machine): Machine management
|
|
||||||
for a container-centric world
|
|
||||||
* [Docker Swarm](https://github.com/docker/swarm): A Docker-native clustering
|
|
||||||
system
|
|
||||||
* [Docker Compose](https://github.com/docker/compose) (formerly Fig):
|
|
||||||
Define and run multi-container apps
|
|
||||||
* [Kitematic](https://github.com/kitematic/kitematic): The easiest way to use
|
|
||||||
Docker on Mac and Windows
|
|
||||||
|
|
||||||
If you know of another project underway that should be listed here, please help
|
|
||||||
us keep this list up-to-date by submitting a PR.
|
|
||||||
|
|||||||
-183
@@ -1,183 +0,0 @@
|
|||||||
Docker Engine Roadmap
|
|
||||||
=====================
|
|
||||||
|
|
||||||
### How should I use this document?
|
|
||||||
|
|
||||||
This document provides description of items that the project decided to prioritize. This should
|
|
||||||
serve as a reference point for Docker contributors to understand where the project is going, and
|
|
||||||
help determine if a contribution could be conflicting with some longer terms plans.
|
|
||||||
|
|
||||||
The fact that a feature isn't listed here doesn't mean that a patch for it will automatically be
|
|
||||||
refused (except for those mentioned as "frozen features" below)! We are always happy to receive
|
|
||||||
patches for new cool features we haven't thought about, or didn't judge priority. Please however
|
|
||||||
understand that such patches might take longer for us to review.
|
|
||||||
|
|
||||||
### How can I help?
|
|
||||||
|
|
||||||
Short term objectives are listed in the [wiki](https://github.com/docker/docker/wiki) and described
|
|
||||||
in [Issues](https://github.com/docker/docker/issues?q=is%3Aopen+is%3Aissue+label%3Aroadmap). Our
|
|
||||||
goal is to split down the workload in such way that anybody can jump in and help. Please comment on
|
|
||||||
issues if you want to take it to avoid duplicating effort! Similarly, if a maintainer is already
|
|
||||||
assigned on an issue you'd like to participate in, pinging him on IRC or GitHub to offer your help is
|
|
||||||
the best way to go.
|
|
||||||
|
|
||||||
### How can I add something to the roadmap?
|
|
||||||
|
|
||||||
The roadmap process is new to the Docker Engine: we are only beginning to structure and document the
|
|
||||||
project objectives. Our immediate goal is to be more transparent, and work with our community to
|
|
||||||
focus our efforts on fewer prioritized topics.
|
|
||||||
|
|
||||||
We hope to offer in the near future a process allowing anyone to propose a topic to the roadmap, but
|
|
||||||
we are not quite there yet. For the time being, the BDFL remains the keeper of the roadmap, and we
|
|
||||||
won't be accepting pull requests adding or removing items from this file.
|
|
||||||
|
|
||||||
# 1. Features and refactoring
|
|
||||||
|
|
||||||
## 1.1 Security
|
|
||||||
|
|
||||||
Security is a top objective for the Docker Engine. The most notable items we intend to provide in
|
|
||||||
the near future are:
|
|
||||||
|
|
||||||
- Trusted distribution of images: the effort is driven by the [distribution](https://github.com/docker/distribution)
|
|
||||||
group but will have significant impact on the Engine
|
|
||||||
- [User namespaces](https://github.com/docker/docker/pull/12648)
|
|
||||||
- [Seccomp support](https://github.com/docker/libcontainer/pull/613)
|
|
||||||
|
|
||||||
## 1.2 Plumbing project
|
|
||||||
|
|
||||||
We define a plumbing tool as a standalone piece of software usable and meaningful on its own. In
|
|
||||||
the current state of the Docker Engine, most subsystems provide independent functionalities (such
|
|
||||||
the builder, pushing and pulling images, running applications in a containerized environment, etc)
|
|
||||||
but all are coupled in a single binary. We want to offer the users to flexibility to use only the
|
|
||||||
pieces they need, and we will also gain in maintainability by splitting the project among multiple
|
|
||||||
repositories.
|
|
||||||
|
|
||||||
As it currently stands, the rough design outlines is to have:
|
|
||||||
- Low level plumbing tools, each dealing with one responsibility (e.g., [runC](https://runc.io))
|
|
||||||
- Docker subsystems services, each exposing an elementary concept over an API, and relying on one or
|
|
||||||
multiple lower level plumbing tools for their implementation (e.g., network management)
|
|
||||||
- Docker Engine to expose higher level actions (e.g., create a container with volume `V` and network
|
|
||||||
`N`), while still providing pass-through access to the individual subsystems.
|
|
||||||
|
|
||||||
The architectural details are still being worked on, but one thing we know for sure is that we need
|
|
||||||
to technically decouple the pieces.
|
|
||||||
|
|
||||||
### 1.2.1 Runtime
|
|
||||||
|
|
||||||
A Runtime tool already exists today in the form of [runC](https://github.com/opencontainers/runc).
|
|
||||||
We intend to modify the Engine to directly call out to a binary implementing the Open Containers
|
|
||||||
Specification such as runC rather than relying on libcontainer to set the container runtime up.
|
|
||||||
|
|
||||||
This plan will deprecate the existing [`execdriver`](https://github.com/docker/docker/tree/master/daemon/execdriver)
|
|
||||||
as different runtime backends will be implemented as separated binaries instead of being compiled
|
|
||||||
into the Engine.
|
|
||||||
|
|
||||||
### 1.2.2 Builder
|
|
||||||
|
|
||||||
The Builder (i.e., the ability to build an image from a Dockerfile) is already nicely decoupled,
|
|
||||||
but would benefit from being entirely separated from the Engine, and rely on the standard Engine
|
|
||||||
API for its operations.
|
|
||||||
|
|
||||||
### 1.2.3 Distribution
|
|
||||||
|
|
||||||
Distribution already has a [dedicated repository](https://github.com/docker/distribution) which
|
|
||||||
holds the implementation for Registry v2 and client libraries. We could imagine going further by
|
|
||||||
having the Engine call out to a binary providing image distribution related functionalities.
|
|
||||||
|
|
||||||
There are two short term goals related to image distribution. The first is stabilize and simplify
|
|
||||||
the push/pull code. Following that is the conversion to the more secure Registry V2 protocol.
|
|
||||||
|
|
||||||
### 1.2.4 Networking
|
|
||||||
|
|
||||||
Most of networking related code was already decoupled today in [libnetwork](https://github.com/docker/libnetwork).
|
|
||||||
As with other ingredients, we might want to take it a step further and make it a meaningful utility
|
|
||||||
that the Engine would call out to instead of a library.
|
|
||||||
|
|
||||||
## 1.3 Plugins
|
|
||||||
|
|
||||||
An initiative around plugins started with Docker 1.7.0, with the goal of allowing for out of
|
|
||||||
process extensibility of some Docker functionalities, starting with volumes and networking. The
|
|
||||||
approach is to provide specific extension points rather than generic hooking facilities. We also
|
|
||||||
deliberately keep the extensions API the simplest possible, expanding as we discover valid use
|
|
||||||
cases that cannot be implemented.
|
|
||||||
|
|
||||||
At the time of writing:
|
|
||||||
|
|
||||||
- Plugin support is merged as an experimental feature: real world use cases and user feedback will
|
|
||||||
help us refine the UX to make the feature more user friendly.
|
|
||||||
- There are no immediate plans to expand on the number of pluggable subsystems.
|
|
||||||
- Golang 1.5 might add language support for [plugins](https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ)
|
|
||||||
which we consider supporting as an alternative to JSON/HTTP.
|
|
||||||
|
|
||||||
## 1.4 Volume management
|
|
||||||
|
|
||||||
Volumes are not a first class citizen in the Engine today: we would like better volume management,
|
|
||||||
similar to the way network are managed in the new [CNM](https://github.com/docker/docker/issues/9983).
|
|
||||||
|
|
||||||
## 1.5 Better API implementation
|
|
||||||
|
|
||||||
The current Engine API is insufficiently typed, versioned, and ultimately hard to maintain. We
|
|
||||||
also suffer from the lack of a common implementation with [Swarm](https://github.com/docker/swarm).
|
|
||||||
|
|
||||||
## 1.6 Checkpoint/restore
|
|
||||||
|
|
||||||
Support for checkpoint/restore was [merged](https://github.com/docker/libcontainer/pull/479) in
|
|
||||||
[libcontainer](https://github.com/docker/libcontainer) and made available through [runC](https://runc.io):
|
|
||||||
we intend to take advantage of it in the Engine.
|
|
||||||
|
|
||||||
# 2 Frozen features
|
|
||||||
|
|
||||||
## 2.1 Docker exec
|
|
||||||
|
|
||||||
We won't accept patches expanding the surface of `docker exec`, which we intend to keep as a
|
|
||||||
*debugging* feature, as well as being strongly dependent on the the Runtime ingredient effort.
|
|
||||||
|
|
||||||
## 2.2 Dockerfile syntax
|
|
||||||
|
|
||||||
The Dockerfile syntax as we know it is simple, and has proven succesful in supporting all our
|
|
||||||
[official images](https://github.com/docker-library/official-images). Although this is *not* a
|
|
||||||
definitive move, we temporarily won't accept more patches to the Dockerfile syntax for several
|
|
||||||
reasons:
|
|
||||||
|
|
||||||
- Long term impact of syntax changes is a sensitive matter that require an amount of attention
|
|
||||||
the volume of Engine codebase and activity today doesn't allow us to provide.
|
|
||||||
- Allowing the Builder to be implemented as a separate utility consuming the Engine's API will
|
|
||||||
open the door for many possibilities, such as offering alternate syntaxes or DSL for existing
|
|
||||||
languages without cluttering the Engine's codebase.
|
|
||||||
- A standalone Builder will also offer the opportunity for a better dedicated group of maintainers
|
|
||||||
to own the Dockerfile syntax and decide collectively on the direction to give it.
|
|
||||||
- Our experience with official images tend to show that no new instruction or syntax expansion is
|
|
||||||
*strictly* necessary for the majority of use cases, and although we are aware many things are still
|
|
||||||
lacking for many, we cannot make it a priority yet for the above reasons.
|
|
||||||
|
|
||||||
Again, this is not about saying that the Dockerfile syntax is done, it's about making choices about
|
|
||||||
what we want to do first!
|
|
||||||
|
|
||||||
## 2.3 Remote Registry Operations
|
|
||||||
|
|
||||||
A large amount of work is ongoing in the area of image distribution and
|
|
||||||
provenance. This includes moving to the V2 Registry API and heavily
|
|
||||||
refactoring the code that powers these features. The desired result is more
|
|
||||||
secure, reliable and easier to use image distribution.
|
|
||||||
|
|
||||||
Part of the problem with this part of the code base is the lack of a stable
|
|
||||||
and flexible interface. If new features are added that access the registry
|
|
||||||
without solidifying these interfaces, achieving feature parity will continue
|
|
||||||
to be elusive. While we get a handle on this situation, we are imposing a
|
|
||||||
moratorium on new code that accesses the Registry API in commands that don't
|
|
||||||
already make remote calls.
|
|
||||||
|
|
||||||
Currently, only the following commands cause interaction with a remote
|
|
||||||
registry:
|
|
||||||
|
|
||||||
- push
|
|
||||||
- pull
|
|
||||||
- run
|
|
||||||
- build
|
|
||||||
- search
|
|
||||||
- login
|
|
||||||
|
|
||||||
In the interest of stabilizing the registry access model during this ongoing
|
|
||||||
work, we are not accepting additions to other commands that will cause remote
|
|
||||||
interaction with the Registry API. This moratorium will lift when the goals of
|
|
||||||
the distribution project have been met.
|
|
||||||
Vendored
+96
@@ -0,0 +1,96 @@
|
|||||||
|
# -*- mode: ruby -*-
|
||||||
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
|
BOX_NAME = ENV['BOX_NAME'] || "ubuntu"
|
||||||
|
BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64.box"
|
||||||
|
VF_BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64_vmware_fusion.box"
|
||||||
|
AWS_REGION = ENV['AWS_REGION'] || "us-east-1"
|
||||||
|
AWS_AMI = ENV['AWS_AMI'] || "ami-d0f89fb9"
|
||||||
|
FORWARD_DOCKER_PORTS = ENV['FORWARD_DOCKER_PORTS']
|
||||||
|
|
||||||
|
Vagrant::Config.run do |config|
|
||||||
|
# Setup virtual machine box. This VM configuration code is always executed.
|
||||||
|
config.vm.box = BOX_NAME
|
||||||
|
config.vm.box_url = BOX_URI
|
||||||
|
|
||||||
|
config.ssh.forward_agent = true
|
||||||
|
|
||||||
|
# Provision docker and new kernel if deployment was not done.
|
||||||
|
# It is assumed Vagrant can successfully launch the provider instance.
|
||||||
|
if Dir.glob("#{File.dirname(__FILE__)}/.vagrant/machines/default/*/id").empty?
|
||||||
|
# Add lxc-docker package
|
||||||
|
pkg_cmd = "wget -q -O - https://get.docker.io/gpg | apt-key add -;" \
|
||||||
|
"echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list;" \
|
||||||
|
"apt-get update -qq; apt-get install -q -y --force-yes lxc-docker; "
|
||||||
|
# Add Ubuntu raring backported kernel
|
||||||
|
pkg_cmd << "apt-get update -qq; apt-get install -q -y linux-image-generic-lts-raring; "
|
||||||
|
# Add guest additions if local vbox VM. As virtualbox is the default provider,
|
||||||
|
# it is assumed it won't be explicitly stated.
|
||||||
|
if ENV["VAGRANT_DEFAULT_PROVIDER"].nil? && ARGV.none? { |arg| arg.downcase.start_with?("--provider") }
|
||||||
|
pkg_cmd << "apt-get install -q -y linux-headers-generic-lts-raring dkms; " \
|
||||||
|
"echo 'Downloading VBox Guest Additions...'; " \
|
||||||
|
"wget -q http://dlc.sun.com.edgesuite.net/virtualbox/4.2.12/VBoxGuestAdditions_4.2.12.iso; "
|
||||||
|
# Prepare the VM to add guest additions after reboot
|
||||||
|
pkg_cmd << "echo -e 'mount -o loop,ro /home/vagrant/VBoxGuestAdditions_4.2.12.iso /mnt\n" \
|
||||||
|
"echo yes | /mnt/VBoxLinuxAdditions.run\numount /mnt\n" \
|
||||||
|
"rm /root/guest_additions.sh; ' > /root/guest_additions.sh; " \
|
||||||
|
"chmod 700 /root/guest_additions.sh; " \
|
||||||
|
"sed -i -E 's#^exit 0#[ -x /root/guest_additions.sh ] \\&\\& /root/guest_additions.sh#' /etc/rc.local; " \
|
||||||
|
"echo 'Installation of VBox Guest Additions is proceeding in the background.'; " \
|
||||||
|
"echo '\"vagrant reload\" can be used in about 2 minutes to activate the new guest additions.'; "
|
||||||
|
end
|
||||||
|
# Activate new kernel
|
||||||
|
pkg_cmd << "shutdown -r +1; "
|
||||||
|
config.vm.provision :shell, :inline => pkg_cmd
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Providers were added on Vagrant >= 1.1.0
|
||||||
|
Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config|
|
||||||
|
config.vm.provider :aws do |aws, override|
|
||||||
|
aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"]
|
||||||
|
aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
||||||
|
aws.keypair_name = ENV["AWS_KEYPAIR_NAME"]
|
||||||
|
override.ssh.private_key_path = ENV["AWS_SSH_PRIVKEY"]
|
||||||
|
override.ssh.username = "ubuntu"
|
||||||
|
aws.region = AWS_REGION
|
||||||
|
aws.ami = AWS_AMI
|
||||||
|
aws.instance_type = "t1.micro"
|
||||||
|
end
|
||||||
|
|
||||||
|
config.vm.provider :rackspace do |rs|
|
||||||
|
config.ssh.private_key_path = ENV["RS_PRIVATE_KEY"]
|
||||||
|
rs.username = ENV["RS_USERNAME"]
|
||||||
|
rs.api_key = ENV["RS_API_KEY"]
|
||||||
|
rs.public_key_path = ENV["RS_PUBLIC_KEY"]
|
||||||
|
rs.flavor = /512MB/
|
||||||
|
rs.image = /Ubuntu/
|
||||||
|
end
|
||||||
|
|
||||||
|
config.vm.provider :vmware_fusion do |f, override|
|
||||||
|
override.vm.box = BOX_NAME
|
||||||
|
override.vm.box_url = VF_BOX_URI
|
||||||
|
override.vm.synced_folder ".", "/vagrant", disabled: true
|
||||||
|
f.vmx["displayName"] = "docker"
|
||||||
|
end
|
||||||
|
|
||||||
|
config.vm.provider :virtualbox do |vb|
|
||||||
|
config.vm.box = BOX_NAME
|
||||||
|
config.vm.box_url = BOX_URI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if !FORWARD_DOCKER_PORTS.nil?
|
||||||
|
Vagrant::VERSION < "1.1.0" and Vagrant::Config.run do |config|
|
||||||
|
(49000..49900).each do |port|
|
||||||
|
config.vm.forward_port port, port
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config|
|
||||||
|
(49000..49900).each do |port|
|
||||||
|
config.vm.network :forwarded_port, :host => port, :guest => port
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
This directory contains code pertaining to the Docker API:
|
|
||||||
|
|
||||||
- Used by the docker client when communicating with the docker daemon
|
|
||||||
|
|
||||||
- Used by third party tools wishing to interface with the docker daemon
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJsonContentType(t *testing.T) {
|
|
||||||
if !MatchesContentType("application/json", "application/json") {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !MatchesContentType("application/json; charset=utf-8", "application/json") {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
if MatchesContentType("dockerapplication/json", "application/json") {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdAttach attaches to a running container.
|
|
||||||
//
|
|
||||||
// Usage: docker attach [OPTIONS] CONTAINER
|
|
||||||
func (cli *DockerCli) CmdAttach(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("attach", []string{"CONTAINER"}, "Attach to a running container", true)
|
|
||||||
noStdin := cmd.Bool([]string{"#nostdin", "-no-stdin"}, false, "Do not attach STDIN")
|
|
||||||
proxy := cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy all received signals to the process")
|
|
||||||
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var c types.ContainerJSON
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.State.Running {
|
|
||||||
return fmt.Errorf("You cannot attach to a stopped container, start it first")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cli.CheckTtyInput(!*noStdin, c.Config.Tty); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Config.Tty && cli.isTerminalOut {
|
|
||||||
if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
|
|
||||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var in io.ReadCloser
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("stream", "1")
|
|
||||||
if !*noStdin && c.Config.OpenStdin {
|
|
||||||
v.Set("stdin", "1")
|
|
||||||
in = cli.in
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("stdout", "1")
|
|
||||||
v.Set("stderr", "1")
|
|
||||||
|
|
||||||
if *proxy && !c.Config.Tty {
|
|
||||||
sigc := cli.forwardAllSignals(cmd.Arg(0))
|
|
||||||
defer signal.StopCatch(sigc)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), c.Config.Tty, in, cli.out, cli.err, nil, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, status, err := getExitCode(cli, cmd.Arg(0))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if status != 0 {
|
|
||||||
return Cli.StatusError{StatusCode: status}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,634 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"bufio"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/graph/tags"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
|
||||||
"github.com/docker/docker/pkg/fileutils"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/progressreader"
|
|
||||||
"github.com/docker/docker/pkg/streamformatter"
|
|
||||||
"github.com/docker/docker/pkg/symlink"
|
|
||||||
"github.com/docker/docker/pkg/ulimit"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
"github.com/docker/docker/pkg/urlutil"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/docker/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tarHeaderSize = 512
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdBuild builds a new image from the source code at a given path.
|
|
||||||
//
|
|
||||||
// If '-' is provided instead of a path or URL, Docker will build an image from either a Dockerfile or tar archive read from STDIN.
|
|
||||||
//
|
|
||||||
// Usage: docker build [OPTIONS] PATH | URL | -
|
|
||||||
func (cli *DockerCli) CmdBuild(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, "Build a new image from the source code at PATH", true)
|
|
||||||
tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) for the image")
|
|
||||||
suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers")
|
|
||||||
noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image")
|
|
||||||
rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build")
|
|
||||||
forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers")
|
|
||||||
pull := cmd.Bool([]string{"-pull"}, false, "Always attempt to pull a newer version of the image")
|
|
||||||
dockerfileName := cmd.String([]string{"f", "-file"}, "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
|
|
||||||
flMemoryString := cmd.String([]string{"m", "-memory"}, "", "Memory limit")
|
|
||||||
flMemorySwap := cmd.String([]string{"-memory-swap"}, "", "Total memory (memory + swap), '-1' to disable swap")
|
|
||||||
flCPUShares := cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)")
|
|
||||||
flCpuPeriod := cmd.Int64([]string{"-cpu-period"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
|
|
||||||
flCpuQuota := cmd.Int64([]string{"-cpu-quota"}, 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
|
|
||||||
flCPUSetCpus := cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)")
|
|
||||||
flCPUSetMems := cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)")
|
|
||||||
flCgroupParent := cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container")
|
|
||||||
|
|
||||||
ulimits := make(map[string]*ulimit.Ulimit)
|
|
||||||
flUlimits := opts.NewUlimitOpt(&ulimits)
|
|
||||||
cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options")
|
|
||||||
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
// For trusted pull on "FROM <image>" instruction.
|
|
||||||
addTrustedFlags(cmd, true)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
context io.ReadCloser
|
|
||||||
isRemote bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err = exec.LookPath("git")
|
|
||||||
hasGit := err == nil
|
|
||||||
|
|
||||||
specifiedContext := cmd.Arg(0)
|
|
||||||
|
|
||||||
var (
|
|
||||||
contextDir string
|
|
||||||
tempDir string
|
|
||||||
relDockerfile string
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case specifiedContext == "-":
|
|
||||||
tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName)
|
|
||||||
case urlutil.IsGitURL(specifiedContext) && hasGit:
|
|
||||||
tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName)
|
|
||||||
case urlutil.IsURL(specifiedContext):
|
|
||||||
tempDir, relDockerfile, err = getContextFromURL(cli.out, specifiedContext, *dockerfileName)
|
|
||||||
default:
|
|
||||||
contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to prepare context: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tempDir != "" {
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
contextDir = tempDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the FROM lines in the Dockerfile to trusted digest references
|
|
||||||
// using Notary. On a successful build, we must tag the resolved digests
|
|
||||||
// to the original name specified in the Dockerfile.
|
|
||||||
newDockerfile, resolvedTags, err := rewriteDockerfileFrom(filepath.Join(contextDir, relDockerfile), cli.trustedReference)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to process Dockerfile: %v", err)
|
|
||||||
}
|
|
||||||
defer newDockerfile.Close()
|
|
||||||
|
|
||||||
// And canonicalize dockerfile name to a platform-independent one
|
|
||||||
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var includes = []string{"."}
|
|
||||||
|
|
||||||
excludes, err := utils.ReadDockerIgnore(path.Join(contextDir, ".dockerignore"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := utils.ValidateContextDirectory(contextDir, excludes); err != nil {
|
|
||||||
return fmt.Errorf("Error checking context: '%s'.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If .dockerignore mentions .dockerignore or the Dockerfile
|
|
||||||
// then make sure we send both files over to the daemon
|
|
||||||
// because Dockerfile is, obviously, needed no matter what, and
|
|
||||||
// .dockerignore is needed to know if either one needs to be
|
|
||||||
// removed. The deamon will remove them for us, if needed, after it
|
|
||||||
// parses the Dockerfile. Ignore errors here, as they will have been
|
|
||||||
// caught by ValidateContextDirectory above.
|
|
||||||
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
|
|
||||||
keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
|
|
||||||
if keepThem1 || keepThem2 {
|
|
||||||
includes = append(includes, ".dockerignore", relDockerfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
context, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
|
||||||
Compression: archive.Uncompressed,
|
|
||||||
ExcludePatterns: excludes,
|
|
||||||
IncludeFiles: includes,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
|
|
||||||
// Dockerfile which uses trusted pulls.
|
|
||||||
context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile)
|
|
||||||
|
|
||||||
// Setup an upload progress bar
|
|
||||||
// FIXME: ProgressReader shouldn't be this annoying to use
|
|
||||||
sf := streamformatter.NewStreamFormatter()
|
|
||||||
var body io.Reader = progressreader.New(progressreader.Config{
|
|
||||||
In: context,
|
|
||||||
Out: cli.out,
|
|
||||||
Formatter: sf,
|
|
||||||
NewLines: true,
|
|
||||||
ID: "",
|
|
||||||
Action: "Sending build context to Docker daemon",
|
|
||||||
})
|
|
||||||
|
|
||||||
var memory int64
|
|
||||||
if *flMemoryString != "" {
|
|
||||||
parsedMemory, err := units.RAMInBytes(*flMemoryString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
memory = parsedMemory
|
|
||||||
}
|
|
||||||
|
|
||||||
var memorySwap int64
|
|
||||||
if *flMemorySwap != "" {
|
|
||||||
if *flMemorySwap == "-1" {
|
|
||||||
memorySwap = -1
|
|
||||||
} else {
|
|
||||||
parsedMemorySwap, err := units.RAMInBytes(*flMemorySwap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
memorySwap = parsedMemorySwap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Send the build context
|
|
||||||
v := &url.Values{}
|
|
||||||
|
|
||||||
//Check if the given image name can be resolved
|
|
||||||
if *tag != "" {
|
|
||||||
repository, tag := parsers.ParseRepositoryTag(*tag)
|
|
||||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(tag) > 0 {
|
|
||||||
if err := tags.ValidateTagName(tag); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("t", *tag)
|
|
||||||
|
|
||||||
if *suppressOutput {
|
|
||||||
v.Set("q", "1")
|
|
||||||
}
|
|
||||||
if isRemote {
|
|
||||||
v.Set("remote", cmd.Arg(0))
|
|
||||||
}
|
|
||||||
if *noCache {
|
|
||||||
v.Set("nocache", "1")
|
|
||||||
}
|
|
||||||
if *rm {
|
|
||||||
v.Set("rm", "1")
|
|
||||||
} else {
|
|
||||||
v.Set("rm", "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *forceRm {
|
|
||||||
v.Set("forcerm", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *pull {
|
|
||||||
v.Set("pull", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("cpusetcpus", *flCPUSetCpus)
|
|
||||||
v.Set("cpusetmems", *flCPUSetMems)
|
|
||||||
v.Set("cpushares", strconv.FormatInt(*flCPUShares, 10))
|
|
||||||
v.Set("cpuquota", strconv.FormatInt(*flCpuQuota, 10))
|
|
||||||
v.Set("cpuperiod", strconv.FormatInt(*flCpuPeriod, 10))
|
|
||||||
v.Set("memory", strconv.FormatInt(memory, 10))
|
|
||||||
v.Set("memswap", strconv.FormatInt(memorySwap, 10))
|
|
||||||
v.Set("cgroupparent", *flCgroupParent)
|
|
||||||
|
|
||||||
v.Set("dockerfile", relDockerfile)
|
|
||||||
|
|
||||||
ulimitsVar := flUlimits.GetList()
|
|
||||||
ulimitsJson, err := json.Marshal(ulimitsVar)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Set("ulimits", string(ulimitsJson))
|
|
||||||
|
|
||||||
headers := http.Header(make(map[string][]string))
|
|
||||||
buf, err := json.Marshal(cli.configFile.AuthConfigs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
|
|
||||||
headers.Set("Content-Type", "application/tar")
|
|
||||||
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
in: body,
|
|
||||||
out: cli.out,
|
|
||||||
headers: headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.stream("POST", fmt.Sprintf("/build?%s", v.Encode()), sopts)
|
|
||||||
|
|
||||||
// Windows: show error message about modified file permissions.
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
h, err := httputils.ParseServerHeader(serverResp.header.Get("Server"))
|
|
||||||
if err == nil {
|
|
||||||
if h.OS != "windows" {
|
|
||||||
fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
|
||||||
// If no error code is set, default to 1
|
|
||||||
if jerr.Code == 0 {
|
|
||||||
jerr.Code = 1
|
|
||||||
}
|
|
||||||
return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the build was successful, now we must tag any of the resolved
|
|
||||||
// images from the above Dockerfile rewrite.
|
|
||||||
for _, resolved := range resolvedTags {
|
|
||||||
if err := cli.tagTrusted(resolved.repoInfo, resolved.digestRef, resolved.tagRef); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDockerfileRelPath uses the given context directory for a `docker build`
|
|
||||||
// and returns the absolute path to the context directory, the relative path of
|
|
||||||
// the dockerfile in that context directory, and a non-nil error on success.
|
|
||||||
func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) {
|
|
||||||
if absContextDir, err = filepath.Abs(givenContextDir); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to get absolute context directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The context dir might be a symbolic link, so follow it to the actual
|
|
||||||
// target directory.
|
|
||||||
absContextDir, err = filepath.EvalSymlinks(absContextDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stat, err := os.Lstat(absContextDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stat.IsDir() {
|
|
||||||
return "", "", fmt.Errorf("context must be a directory: %s", absContextDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
absDockerfile := givenDockerfile
|
|
||||||
if absDockerfile == "" {
|
|
||||||
// No -f/--file was specified so use the default relative to the
|
|
||||||
// context directory.
|
|
||||||
absDockerfile = filepath.Join(absContextDir, api.DefaultDockerfileName)
|
|
||||||
|
|
||||||
// Just to be nice ;-) look for 'dockerfile' too but only
|
|
||||||
// use it if we found it, otherwise ignore this check
|
|
||||||
if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) {
|
|
||||||
altPath := filepath.Join(absContextDir, strings.ToLower(api.DefaultDockerfileName))
|
|
||||||
if _, err = os.Lstat(altPath); err == nil {
|
|
||||||
absDockerfile = altPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not already an absolute path, the Dockerfile path should be joined to
|
|
||||||
// the base directory.
|
|
||||||
if !filepath.IsAbs(absDockerfile) {
|
|
||||||
absDockerfile = filepath.Join(absContextDir, absDockerfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that 'filename' is within the build context
|
|
||||||
absDockerfile, err = symlink.FollowSymlinkInScope(absDockerfile, absContextDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Lstat(absDockerfile); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return "", "", fmt.Errorf("Cannot locate Dockerfile: absDockerfile: %q", absDockerfile)
|
|
||||||
}
|
|
||||||
return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return absContextDir, relDockerfile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeToFile copies from the given reader and writes it to a file with the
|
|
||||||
// given filename.
|
|
||||||
func writeToFile(r io.Reader, filename string) error {
|
|
||||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create file: %v", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(file, r); err != nil {
|
|
||||||
return fmt.Errorf("unable to write file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getContextFromReader will read the contents of the given reader as either a
|
|
||||||
// Dockerfile or tar archive to be extracted to a temporary directory used as
|
|
||||||
// the context directory. Returns the absolute path to the temporary context
|
|
||||||
// directory, the relative path of the dockerfile in that context directory,
|
|
||||||
// and a non-nil error on success.
|
|
||||||
func getContextFromReader(r io.Reader, dockerfileName string) (absContextDir, relDockerfile string, err error) {
|
|
||||||
buf := bufio.NewReader(r)
|
|
||||||
|
|
||||||
magic, err := buf.Peek(tarHeaderSize)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return "", "", fmt.Errorf("failed to peek context header from STDIN: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if absContextDir, err = ioutil.TempDir("", "docker-build-context-"); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unbale to create temporary context directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func(d string) {
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(d)
|
|
||||||
}
|
|
||||||
}(absContextDir)
|
|
||||||
|
|
||||||
if !archive.IsArchive(magic) { // Input should be read as a Dockerfile.
|
|
||||||
// -f option has no meaning when we're reading it from stdin,
|
|
||||||
// so just use our default Dockerfile name
|
|
||||||
relDockerfile = api.DefaultDockerfileName
|
|
||||||
|
|
||||||
return absContextDir, relDockerfile, writeToFile(buf, filepath.Join(absContextDir, relDockerfile))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := archive.Untar(buf, absContextDir, nil); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to extract stdin to temporary context direcotry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDockerfileRelPath(absContextDir, dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getContextFromGitURL uses a Git URL as context for a `docker build`. The
|
|
||||||
// git repo is cloned into a temporary directory used as the context directory.
|
|
||||||
// Returns the absolute path to the temporary context directory, the relative
|
|
||||||
// path of the dockerfile in that context directory, and a non-nil error on
|
|
||||||
// success.
|
|
||||||
func getContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) {
|
|
||||||
if absContextDir, err = utils.GitClone(gitURL); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDockerfileRelPath(absContextDir, dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getContextFromURL uses a remote URL as context for a `docker build`. The
|
|
||||||
// remote resource is downloaded as either a Dockerfile or a context tar
|
|
||||||
// archive and stored in a temporary directory used as the context directory.
|
|
||||||
// Returns the absolute path to the temporary context directory, the relative
|
|
||||||
// path of the dockerfile in that context directory, and a non-nil error on
|
|
||||||
// success.
|
|
||||||
func getContextFromURL(out io.Writer, remoteURL, dockerfileName string) (absContextDir, relDockerfile string, err error) {
|
|
||||||
response, err := httputils.Download(remoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
// Pass the response body through a progress reader.
|
|
||||||
progReader := &progressreader.Config{
|
|
||||||
In: response.Body,
|
|
||||||
Out: out,
|
|
||||||
Formatter: streamformatter.NewStreamFormatter(),
|
|
||||||
Size: int(response.ContentLength),
|
|
||||||
NewLines: true,
|
|
||||||
ID: "",
|
|
||||||
Action: fmt.Sprintf("Downloading build context from remote url: %s", remoteURL),
|
|
||||||
}
|
|
||||||
|
|
||||||
return getContextFromReader(progReader, dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getContextFromLocalDir uses the given local directory as context for a
|
|
||||||
// `docker build`. Returns the absolute path to the local context directory,
|
|
||||||
// the relative path of the dockerfile in that context directory, and a non-nil
|
|
||||||
// error on success.
|
|
||||||
func getContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) {
|
|
||||||
// When using a local context directory, when the Dockerfile is specified
|
|
||||||
// with the `-f/--file` option then it is considered relative to the
|
|
||||||
// current directory and not the context directory.
|
|
||||||
if dockerfileName != "" {
|
|
||||||
if dockerfileName, err = filepath.Abs(dockerfileName); err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDockerfileRelPath(localDir, dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
|
|
||||||
|
|
||||||
type trustedDockerfile struct {
|
|
||||||
*os.File
|
|
||||||
size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (td *trustedDockerfile) Close() error {
|
|
||||||
td.File.Close()
|
|
||||||
return os.Remove(td.File.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolvedTag records the repository, tag, and resolved digest reference
|
|
||||||
// from a Dockerfile rewrite.
|
|
||||||
type resolvedTag struct {
|
|
||||||
repoInfo *registry.RepositoryInfo
|
|
||||||
digestRef, tagRef registry.Reference
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
|
|
||||||
// "FROM <image>" instructions to a digest reference. `translator` is a
|
|
||||||
// function that takes a repository name and tag reference and returns a
|
|
||||||
// trusted digest reference.
|
|
||||||
func rewriteDockerfileFrom(dockerfileName string, translator func(string, registry.Reference) (registry.Reference, error)) (newDockerfile *trustedDockerfile, resolvedTags []*resolvedTag, err error) {
|
|
||||||
dockerfile, err := os.Open(dockerfileName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("unable to open Dockerfile: %v", err)
|
|
||||||
}
|
|
||||||
defer dockerfile.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(dockerfile)
|
|
||||||
|
|
||||||
// Make a tempfile to store the rewritten Dockerfile.
|
|
||||||
tempFile, err := ioutil.TempFile("", "trusted-dockerfile-")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("unable to make temporary trusted Dockerfile: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
trustedFile := &trustedDockerfile{
|
|
||||||
File: tempFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// Close the tempfile if there was an error during Notary lookups.
|
|
||||||
// Otherwise the caller should close it.
|
|
||||||
trustedFile.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Scan the lines of the Dockerfile, looking for a "FROM" line.
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
matches := dockerfileFromLinePattern.FindStringSubmatch(line)
|
|
||||||
if matches != nil && matches[1] != "scratch" {
|
|
||||||
// Replace the line with a resolved "FROM repo@digest"
|
|
||||||
repo, tag := parsers.ParseRepositoryTag(matches[1])
|
|
||||||
if tag == "" {
|
|
||||||
tag = tags.DEFAULTTAG
|
|
||||||
}
|
|
||||||
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("unable to parse repository info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ref := registry.ParseReference(tag)
|
|
||||||
|
|
||||||
if !ref.HasDigest() && isTrusted() {
|
|
||||||
trustedRef, err := translator(repo, ref)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.ImageName(repo)))
|
|
||||||
resolvedTags = append(resolvedTags, &resolvedTag{
|
|
||||||
repoInfo: repoInfo,
|
|
||||||
digestRef: trustedRef,
|
|
||||||
tagRef: ref,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := fmt.Fprintln(tempFile, line)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
trustedFile.size += int64(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFile.Seek(0, os.SEEK_SET)
|
|
||||||
|
|
||||||
return trustedFile, resolvedTags, scanner.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// replaceDockerfileTarWrapper wraps the given input tar archive stream and
|
|
||||||
// replaces the entry with the given Dockerfile name with the contents of the
|
|
||||||
// new Dockerfile. Returns a new tar archive stream with the replaced
|
|
||||||
// Dockerfile.
|
|
||||||
func replaceDockerfileTarWrapper(inputTarStream io.ReadCloser, newDockerfile *trustedDockerfile, dockerfileName string) io.ReadCloser {
|
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
tarReader := tar.NewReader(inputTarStream)
|
|
||||||
tarWriter := tar.NewWriter(pipeWriter)
|
|
||||||
|
|
||||||
defer inputTarStream.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
hdr, err := tarReader.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
// Signals end of archive.
|
|
||||||
tarWriter.Close()
|
|
||||||
pipeWriter.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
pipeWriter.CloseWithError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var content io.Reader = tarReader
|
|
||||||
|
|
||||||
if hdr.Name == dockerfileName {
|
|
||||||
// This entry is the Dockerfile. Since the tar archive was
|
|
||||||
// generated from a directory on the local filesystem, the
|
|
||||||
// Dockerfile will only appear once in the archive.
|
|
||||||
hdr.Size = newDockerfile.size
|
|
||||||
content = newDockerfile
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tarWriter.WriteHeader(hdr); err != nil {
|
|
||||||
pipeWriter.CloseWithError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.Copy(tarWriter, content); err != nil {
|
|
||||||
pipeWriter.CloseWithError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return pipeReader
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
"github.com/docker/docker/pkg/sockets"
|
|
||||||
"github.com/docker/docker/pkg/term"
|
|
||||||
"github.com/docker/docker/pkg/tlsconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DockerCli represents the docker command line client.
|
|
||||||
// Instances of the client can be returned from NewDockerCli.
|
|
||||||
type DockerCli struct {
|
|
||||||
// initializing closure
|
|
||||||
init func() error
|
|
||||||
|
|
||||||
// proto holds the client protocol i.e. unix.
|
|
||||||
proto string
|
|
||||||
// addr holds the client address.
|
|
||||||
addr string
|
|
||||||
// basePath holds the path to prepend to the requests
|
|
||||||
basePath string
|
|
||||||
|
|
||||||
// configFile has the client configuration file
|
|
||||||
configFile *cliconfig.ConfigFile
|
|
||||||
// in holds the input stream and closer (io.ReadCloser) for the client.
|
|
||||||
in io.ReadCloser
|
|
||||||
// out holds the output stream (io.Writer) for the client.
|
|
||||||
out io.Writer
|
|
||||||
// err holds the error stream (io.Writer) for the client.
|
|
||||||
err io.Writer
|
|
||||||
// keyFile holds the key file as a string.
|
|
||||||
keyFile string
|
|
||||||
// tlsConfig holds the TLS configuration for the client, and will
|
|
||||||
// set the scheme to https in NewDockerCli if present.
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
// scheme holds the scheme of the client i.e. https.
|
|
||||||
scheme string
|
|
||||||
// inFd holds the file descriptor of the client's STDIN (if valid).
|
|
||||||
inFd uintptr
|
|
||||||
// outFd holds file descriptor of the client's STDOUT (if valid).
|
|
||||||
outFd uintptr
|
|
||||||
// isTerminalIn indicates whether the client's STDIN is a TTY
|
|
||||||
isTerminalIn bool
|
|
||||||
// isTerminalOut dindicates whether the client's STDOUT is a TTY
|
|
||||||
isTerminalOut bool
|
|
||||||
// transport holds the client transport instance.
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) Initialize() error {
|
|
||||||
if cli.init == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cli.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckTtyInput checks if we are trying to attach to a container tty
|
|
||||||
// from a non-tty client input stream, and if so, returns an error.
|
|
||||||
func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
|
|
||||||
// In order to attach to a container tty, input stream for the client must
|
|
||||||
// be a tty itself: redirecting or piping the client standard input is
|
|
||||||
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
|
|
||||||
if ttyMode && attachStdin && !cli.isTerminalIn {
|
|
||||||
return errors.New("cannot enable tty mode on non tty input")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) PsFormat() string {
|
|
||||||
return cli.configFile.PsFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
|
||||||
// The key file, protocol (i.e. unix) and address are passed in as strings, along with the tls.Config. If the tls.Config
|
|
||||||
// is set the client scheme will be set to https.
|
|
||||||
// The client will be given a 32-second timeout (see https://github.com/docker/docker/pull/8035).
|
|
||||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientFlags) *DockerCli {
|
|
||||||
cli := &DockerCli{
|
|
||||||
in: in,
|
|
||||||
out: out,
|
|
||||||
err: err,
|
|
||||||
keyFile: clientFlags.Common.TrustKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
cli.init = func() error {
|
|
||||||
clientFlags.PostParse()
|
|
||||||
|
|
||||||
hosts := clientFlags.Common.Hosts
|
|
||||||
|
|
||||||
switch len(hosts) {
|
|
||||||
case 0:
|
|
||||||
defaultHost := os.Getenv("DOCKER_HOST")
|
|
||||||
if defaultHost == "" {
|
|
||||||
defaultHost = opts.DefaultHost
|
|
||||||
}
|
|
||||||
defaultHost, err := opts.ValidateHost(defaultHost)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hosts = []string{defaultHost}
|
|
||||||
case 1:
|
|
||||||
// only accept one host to talk to
|
|
||||||
default:
|
|
||||||
return errors.New("Please specify only one -H")
|
|
||||||
}
|
|
||||||
|
|
||||||
protoAddrParts := strings.SplitN(hosts[0], "://", 2)
|
|
||||||
cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1]
|
|
||||||
|
|
||||||
if cli.proto == "tcp" {
|
|
||||||
// error is checked in pkg/parsers already
|
|
||||||
parsed, _ := url.Parse("tcp://" + cli.addr)
|
|
||||||
cli.addr = parsed.Host
|
|
||||||
cli.basePath = parsed.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
if clientFlags.Common.TLSOptions != nil {
|
|
||||||
cli.scheme = "https"
|
|
||||||
var e error
|
|
||||||
cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions)
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cli.scheme = "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
if cli.in != nil {
|
|
||||||
cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in)
|
|
||||||
}
|
|
||||||
if cli.out != nil {
|
|
||||||
cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The transport is created here for reuse during the client session.
|
|
||||||
cli.transport = &http.Transport{
|
|
||||||
TLSClientConfig: cli.tlsConfig,
|
|
||||||
}
|
|
||||||
sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr)
|
|
||||||
|
|
||||||
configFile, e := cliconfig.Load(cliconfig.ConfigDir())
|
|
||||||
if e != nil {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
|
|
||||||
}
|
|
||||||
cli.configFile = configFile
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cli
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Package client provides a command-line interface for Docker.
|
|
||||||
//
|
|
||||||
// Run "docker help SUBCOMMAND" or "docker SUBCOMMAND --help" to see more information on any Docker subcommand, including the full list of options supported for the subcommand.
|
|
||||||
// See https://docs.docker.com/installation/ for instructions on installing Docker.
|
|
||||||
package client
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdCommit creates a new image from a container's changes.
|
|
||||||
//
|
|
||||||
// Usage: docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
|
|
||||||
func (cli *DockerCli) CmdCommit(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("commit", []string{"CONTAINER [REPOSITORY[:TAG]]"}, "Create a new image from a container's changes", true)
|
|
||||||
flPause := cmd.Bool([]string{"p", "-pause"}, true, "Pause container during commit")
|
|
||||||
flComment := cmd.String([]string{"m", "-message"}, "", "Commit message")
|
|
||||||
flAuthor := cmd.String([]string{"a", "#author", "-author"}, "", "Author (e.g., \"John Hannibal Smith <hannibal@a-team.com>\")")
|
|
||||||
flChanges := opts.NewListOpts(nil)
|
|
||||||
cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image")
|
|
||||||
// FIXME: --run is deprecated, it will be replaced with inline Dockerfile commands.
|
|
||||||
flConfig := cmd.String([]string{"#run", "#-run"}, "", "This option is deprecated and will be removed in a future version in favor of inline Dockerfile-compatible commands")
|
|
||||||
cmd.Require(flag.Max, 2)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
name = cmd.Arg(0)
|
|
||||||
repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1))
|
|
||||||
)
|
|
||||||
|
|
||||||
//Check if the given image name can be resolved
|
|
||||||
if repository != "" {
|
|
||||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("container", name)
|
|
||||||
v.Set("repo", repository)
|
|
||||||
v.Set("tag", tag)
|
|
||||||
v.Set("comment", *flComment)
|
|
||||||
v.Set("author", *flAuthor)
|
|
||||||
for _, change := range flChanges.GetAll() {
|
|
||||||
v.Add("changes", change)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flPause != true {
|
|
||||||
v.Set("pause", "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
config *runconfig.Config
|
|
||||||
response types.ContainerCommitResponse
|
|
||||||
)
|
|
||||||
|
|
||||||
if *flConfig != "" {
|
|
||||||
config = &runconfig.Config{}
|
|
||||||
if err := json.Unmarshal([]byte(*flConfig), config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serverResp, err := cli.call("POST", "/commit?"+v.Encode(), config, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(cli.out, response.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
type copyDirection int
|
|
||||||
|
|
||||||
const (
|
|
||||||
fromContainer copyDirection = (1 << iota)
|
|
||||||
toContainer
|
|
||||||
acrossContainers = fromContainer | toContainer
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdCp copies files/folders to or from a path in a container.
|
|
||||||
//
|
|
||||||
// When copying from a container, if LOCALPATH is '-' the data is written as a
|
|
||||||
// tar archive file to STDOUT.
|
|
||||||
//
|
|
||||||
// When copying to a container, if LOCALPATH is '-' the data is read as a tar
|
|
||||||
// archive file from STDIN, and the destination CONTAINER:PATH, must specify
|
|
||||||
// a directory.
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// docker cp CONTAINER:PATH LOCALPATH|-
|
|
||||||
// docker cp LOCALPATH|- CONTAINER:PATH
|
|
||||||
func (cli *DockerCli) CmdCp(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd(
|
|
||||||
"cp",
|
|
||||||
[]string{"CONTAINER:PATH LOCALPATH|-", "LOCALPATH|- CONTAINER:PATH"},
|
|
||||||
strings.Join([]string{
|
|
||||||
"Copy files/folders between a container and your host.\n",
|
|
||||||
"Use '-' as the source to read a tar archive from stdin\n",
|
|
||||||
"and extract it to a directory destination in a container.\n",
|
|
||||||
"Use '-' as the destination to stream a tar archive of a\n",
|
|
||||||
"container source to stdout.",
|
|
||||||
}, ""),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd.Require(flag.Exact, 2)
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
if cmd.Arg(0) == "" {
|
|
||||||
return fmt.Errorf("source can not be empty")
|
|
||||||
}
|
|
||||||
if cmd.Arg(1) == "" {
|
|
||||||
return fmt.Errorf("destination can not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
srcContainer, srcPath := splitCpArg(cmd.Arg(0))
|
|
||||||
dstContainer, dstPath := splitCpArg(cmd.Arg(1))
|
|
||||||
|
|
||||||
var direction copyDirection
|
|
||||||
if srcContainer != "" {
|
|
||||||
direction |= fromContainer
|
|
||||||
}
|
|
||||||
if dstContainer != "" {
|
|
||||||
direction |= toContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
switch direction {
|
|
||||||
case fromContainer:
|
|
||||||
return cli.copyFromContainer(srcContainer, srcPath, dstPath)
|
|
||||||
case toContainer:
|
|
||||||
return cli.copyToContainer(srcPath, dstContainer, dstPath)
|
|
||||||
case acrossContainers:
|
|
||||||
// Copying between containers isn't supported.
|
|
||||||
return fmt.Errorf("copying between containers is not supported")
|
|
||||||
default:
|
|
||||||
// User didn't specify any container.
|
|
||||||
return fmt.Errorf("must specify at least one container source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
|
||||||
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
|
|
||||||
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
|
|
||||||
// absolute path:
|
|
||||||
// `/path/to/file:name.txt` or `./file:name.txt`
|
|
||||||
//
|
|
||||||
// This is apparently how `scp` handles this as well:
|
|
||||||
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
|
|
||||||
//
|
|
||||||
// We can't simply check for a filepath separator because container names may
|
|
||||||
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
|
|
||||||
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
|
|
||||||
// client, a `:` could be part of an absolute Windows path, in which case it
|
|
||||||
// is immediately proceeded by a backslash.
|
|
||||||
func splitCpArg(arg string) (container, path string) {
|
|
||||||
if filepath.IsAbs(arg) {
|
|
||||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
|
||||||
return "", arg
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(arg, ":", 2)
|
|
||||||
|
|
||||||
if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
|
|
||||||
// Either there's no `:` in the arg
|
|
||||||
// OR it's an explicit local relative path like `./file:name.txt`.
|
|
||||||
return "", arg
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[0], parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
|
|
||||||
var stat types.ContainerPathStat
|
|
||||||
|
|
||||||
query := make(url.Values, 1)
|
|
||||||
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
|
|
||||||
|
|
||||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", containerName, query.Encode())
|
|
||||||
|
|
||||||
response, err := cli.call("HEAD", urlStr, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return stat, err
|
|
||||||
}
|
|
||||||
defer response.body.Close()
|
|
||||||
|
|
||||||
if response.statusCode != http.StatusOK {
|
|
||||||
return stat, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getContainerPathStatFromHeader(response.header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContainerPathStatFromHeader(header http.Header) (types.ContainerPathStat, error) {
|
|
||||||
var stat types.ContainerPathStat
|
|
||||||
|
|
||||||
encodedStat := header.Get("X-Docker-Container-Path-Stat")
|
|
||||||
statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat))
|
|
||||||
|
|
||||||
err := json.NewDecoder(statDecoder).Decode(&stat)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("unable to decode container path stat header: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return stat, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
|
||||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string) (err error) {
|
|
||||||
if dstPath != "-" {
|
|
||||||
// Get an absolute destination path.
|
|
||||||
dstPath, err = resolveLocalPath(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := make(url.Values, 1)
|
|
||||||
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
|
|
||||||
|
|
||||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", srcContainer, query.Encode())
|
|
||||||
|
|
||||||
response, err := cli.call("GET", urlStr, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.body.Close()
|
|
||||||
|
|
||||||
if response.statusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dstPath == "-" {
|
|
||||||
// Send the response to STDOUT.
|
|
||||||
_, err = io.Copy(os.Stdout, response.body)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to get the copy behavior right, we need to know information
|
|
||||||
// about both the source and the destination. The response headers include
|
|
||||||
// stat info about the source that we can use in deciding exactly how to
|
|
||||||
// copy it locally. Along with the stat info about the local destination,
|
|
||||||
// we have everything we need to handle the multiple possibilities there
|
|
||||||
// can be when copying a file/dir from one location to another file/dir.
|
|
||||||
stat, err := getContainerPathStatFromHeader(response.header)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get resource stat from response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare source copy info.
|
|
||||||
srcInfo := archive.CopyInfo{
|
|
||||||
Path: srcPath,
|
|
||||||
Exists: true,
|
|
||||||
IsDir: stat.Mode.IsDir(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// See comments in the implementation of `archive.CopyTo` for exactly what
|
|
||||||
// goes into deciding how and whether the source archive needs to be
|
|
||||||
// altered for the correct copy behavior.
|
|
||||||
return archive.CopyTo(response.body, srcInfo, dstPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string) (err error) {
|
|
||||||
if srcPath != "-" {
|
|
||||||
// Get an absolute source path.
|
|
||||||
srcPath, err = resolveLocalPath(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to get the copy behavior right, we need to know information
|
|
||||||
// about both the source and destination. The API is a simple tar
|
|
||||||
// archive/extract API but we can use the stat info header about the
|
|
||||||
// destination to be more informed about exactly what the destination is.
|
|
||||||
|
|
||||||
// Prepare destination copy info by stat-ing the container path.
|
|
||||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
|
||||||
dstStat, err := cli.statContainerPath(dstContainer, dstPath)
|
|
||||||
|
|
||||||
// If the destination is a symbolic link, we should evaluate it.
|
|
||||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
|
||||||
linkTarget := dstStat.LinkTarget
|
|
||||||
if !filepath.IsAbs(linkTarget) {
|
|
||||||
// Join with the parent directory.
|
|
||||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
|
||||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstInfo.Path = linkTarget
|
|
||||||
dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore any error and assume that the parent directory of the destination
|
|
||||||
// path exists, in which case the copy may still succeed. If there is any
|
|
||||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
|
||||||
// or vice versia) the extraction will fail. If the destination simply did
|
|
||||||
// not exist, but the parent directory does, the extraction will still
|
|
||||||
// succeed.
|
|
||||||
if err == nil {
|
|
||||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
content io.Reader
|
|
||||||
resolvedDstPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
if srcPath == "-" {
|
|
||||||
// Use STDIN.
|
|
||||||
content = os.Stdin
|
|
||||||
resolvedDstPath = dstInfo.Path
|
|
||||||
if !dstInfo.IsDir {
|
|
||||||
return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Prepare source copy info.
|
|
||||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srcArchive, err := archive.TarResource(srcInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer srcArchive.Close()
|
|
||||||
|
|
||||||
// With the stat info about the local source as well as the
|
|
||||||
// destination, we have enough information to know whether we need to
|
|
||||||
// alter the archive that we upload so that when the server extracts
|
|
||||||
// it to the specified directory in the container we get the disired
|
|
||||||
// copy behavior.
|
|
||||||
|
|
||||||
// See comments in the implementation of `archive.PrepareArchiveCopy`
|
|
||||||
// for exactly what goes into deciding how and whether the source
|
|
||||||
// archive needs to be altered for the correct copy behavior when it is
|
|
||||||
// extracted. This function also infers from the source and destination
|
|
||||||
// info which directory to extract to, which may be the parent of the
|
|
||||||
// destination that the user specified.
|
|
||||||
dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer preparedArchive.Close()
|
|
||||||
|
|
||||||
resolvedDstPath = dstDir
|
|
||||||
content = preparedArchive
|
|
||||||
}
|
|
||||||
|
|
||||||
query := make(url.Values, 2)
|
|
||||||
query.Set("path", filepath.ToSlash(resolvedDstPath)) // Normalize the paths used in the API.
|
|
||||||
// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
|
|
||||||
query.Set("noOverwriteDirNonDir", "true")
|
|
||||||
|
|
||||||
urlStr := fmt.Sprintf("/containers/%s/archive?%s", dstContainer, query.Encode())
|
|
||||||
|
|
||||||
response, err := cli.stream("PUT", urlStr, &streamOpts{in: content})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.body.Close()
|
|
||||||
|
|
||||||
if response.statusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/graph/tags"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *DockerCli) pullImage(image string) error {
|
|
||||||
return cli.pullImageCustomOut(image, cli.out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
|
|
||||||
v := url.Values{}
|
|
||||||
repos, tag := parsers.ParseRepositoryTag(image)
|
|
||||||
// pull only the image tagged 'latest' if no tag was specified
|
|
||||||
if tag == "" {
|
|
||||||
tag = tags.DEFAULTTAG
|
|
||||||
}
|
|
||||||
v.Set("fromImage", repos)
|
|
||||||
v.Set("tag", tag)
|
|
||||||
|
|
||||||
// Resolve the Repository name from fqn to RepositoryInfo
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(repos)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the Auth config relevant for this server
|
|
||||||
authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
|
|
||||||
buf, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
registryAuthHeader := []string{
|
|
||||||
base64.URLEncoding.EncodeToString(buf),
|
|
||||||
}
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
out: out,
|
|
||||||
headers: map[string][]string{"X-Registry-Auth": registryAuthHeader},
|
|
||||||
}
|
|
||||||
if _, err := cli.stream("POST", "/images/create?"+v.Encode(), sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type cidFile struct {
|
|
||||||
path string
|
|
||||||
file *os.File
|
|
||||||
written bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCIDFile(path string) (*cidFile, error) {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to create the container ID file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cidFile{path: path, file: f}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runconfig.HostConfig, cidfile, name string) (*types.ContainerCreateResponse, error) {
|
|
||||||
containerValues := url.Values{}
|
|
||||||
if name != "" {
|
|
||||||
containerValues.Set("name", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedConfig := runconfig.MergeConfigs(config, hostConfig)
|
|
||||||
|
|
||||||
var containerIDFile *cidFile
|
|
||||||
if cidfile != "" {
|
|
||||||
var err error
|
|
||||||
if containerIDFile, err = newCIDFile(cidfile); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer containerIDFile.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, tag := parsers.ParseRepositoryTag(config.Image)
|
|
||||||
if tag == "" {
|
|
||||||
tag = tags.DEFAULTTAG
|
|
||||||
}
|
|
||||||
|
|
||||||
ref := registry.ParseReference(tag)
|
|
||||||
var trustedRef registry.Reference
|
|
||||||
|
|
||||||
if isTrusted() && !ref.HasDigest() {
|
|
||||||
var err error
|
|
||||||
trustedRef, err = cli.trustedReference(repo, ref)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Image = trustedRef.ImageName(repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
//create the container
|
|
||||||
serverResp, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil)
|
|
||||||
//if image not found try to pull it
|
|
||||||
if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) {
|
|
||||||
fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.ImageName(repo))
|
|
||||||
|
|
||||||
// we don't want to write to stdout anything apart from container.ID
|
|
||||||
if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if trustedRef != nil && !ref.HasDigest() {
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := cli.tagTrusted(repoInfo, trustedRef, ref); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Retry
|
|
||||||
if serverResp, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var response types.ContainerCreateResponse
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, warning := range response.Warnings {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: %s\n", warning)
|
|
||||||
}
|
|
||||||
if containerIDFile != nil {
|
|
||||||
if err = containerIDFile.Write(response.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdCreate creates a new container from a given image.
|
|
||||||
//
|
|
||||||
// Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
|
|
||||||
func (cli *DockerCli) CmdCreate(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("create", []string{"IMAGE [COMMAND] [ARG...]"}, "Create a new container", true)
|
|
||||||
addTrustedFlags(cmd, true)
|
|
||||||
|
|
||||||
// These are flags not stored in Config/HostConfig
|
|
||||||
var (
|
|
||||||
flName = cmd.String([]string{"-name"}, "", "Assign a name to the container")
|
|
||||||
)
|
|
||||||
|
|
||||||
config, hostConfig, cmd, err := runconfig.Parse(cmd, args)
|
|
||||||
if err != nil {
|
|
||||||
cmd.ReportError(err.Error(), true)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if config.Image == "" {
|
|
||||||
cmd.Usage()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
response, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", response.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdDiff shows changes on a container's filesystem.
|
|
||||||
//
|
|
||||||
// Each changed file is printed on a separate line, prefixed with a single
|
|
||||||
// character that indicates the status of the file: C (modified), A (added),
|
|
||||||
// or D (deleted).
|
|
||||||
//
|
|
||||||
// Usage: docker diff CONTAINER
|
|
||||||
func (cli *DockerCli) CmdDiff(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("diff", []string{"CONTAINER"}, "Inspect changes on a container's filesystem", true)
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
if cmd.Arg(0) == "" {
|
|
||||||
return fmt.Errorf("Container name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
changes := []types.ContainerChange{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&changes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, change := range changes {
|
|
||||||
var kind string
|
|
||||||
switch change.Kind {
|
|
||||||
case archive.ChangeModify:
|
|
||||||
kind = "C"
|
|
||||||
case archive.ChangeAdd:
|
|
||||||
kind = "A"
|
|
||||||
case archive.ChangeDelete:
|
|
||||||
kind = "D"
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "%s %s\n", kind, change.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers/filters"
|
|
||||||
"github.com/docker/docker/pkg/timeutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdEvents prints a live stream of real time events from the server.
|
|
||||||
//
|
|
||||||
// Usage: docker events [OPTIONS]
|
|
||||||
func (cli *DockerCli) CmdEvents(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("events", nil, "Get real time events from the server", true)
|
|
||||||
since := cmd.String([]string{"#since", "-since"}, "", "Show all events created since timestamp")
|
|
||||||
until := cmd.String([]string{"-until"}, "", "Stream events until this timestamp")
|
|
||||||
flFilter := opts.NewListOpts(nil)
|
|
||||||
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
||||||
cmd.Require(flag.Exact, 0)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
v = url.Values{}
|
|
||||||
eventFilterArgs = filters.Args{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Consolidate all filter flags, and sanity check them early.
|
|
||||||
// They'll get process in the daemon/server.
|
|
||||||
for _, f := range flFilter.GetAll() {
|
|
||||||
var err error
|
|
||||||
eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ref := time.Now()
|
|
||||||
if *since != "" {
|
|
||||||
v.Set("since", timeutils.GetTimestamp(*since, ref))
|
|
||||||
}
|
|
||||||
if *until != "" {
|
|
||||||
v.Set("until", timeutils.GetTimestamp(*until, ref))
|
|
||||||
}
|
|
||||||
if len(eventFilterArgs) > 0 {
|
|
||||||
filterJSON, err := filters.ToParam(eventFilterArgs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Set("filters", filterJSON)
|
|
||||||
}
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
out: cli.out,
|
|
||||||
}
|
|
||||||
if _, err := cli.stream("GET", "/events?"+v.Encode(), sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/pkg/promise"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdExec runs a command in a running container.
|
|
||||||
//
|
|
||||||
// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
|
|
||||||
func (cli *DockerCli) CmdExec(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("exec", []string{"CONTAINER COMMAND [ARG...]"}, "Run a command in a running container", true)
|
|
||||||
|
|
||||||
execConfig, err := runconfig.ParseExec(cmd, args)
|
|
||||||
// just in case the ParseExec does not exit
|
|
||||||
if execConfig.Container == "" || err != nil {
|
|
||||||
return Cli.StatusError{StatusCode: 1}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var response types.ContainerExecCreateResponse
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
execID := response.ID
|
|
||||||
|
|
||||||
if execID == "" {
|
|
||||||
fmt.Fprintf(cli.out, "exec ID empty")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//Temp struct for execStart so that we don't need to transfer all the execConfig
|
|
||||||
execStartCheck := &types.ExecStartCheck{
|
|
||||||
Detach: execConfig.Detach,
|
|
||||||
Tty: execConfig.Tty,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !execConfig.Detach {
|
|
||||||
if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execStartCheck, nil)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// For now don't print this - wait for when we support exec wait()
|
|
||||||
// fmt.Fprintf(cli.out, "%s\n", execID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interactive exec requested.
|
|
||||||
var (
|
|
||||||
out, stderr io.Writer
|
|
||||||
in io.ReadCloser
|
|
||||||
hijacked = make(chan io.Closer)
|
|
||||||
errCh chan error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Block the return until the chan gets closed
|
|
||||||
defer func() {
|
|
||||||
logrus.Debugf("End of CmdExec(), Waiting for hijack to finish.")
|
|
||||||
if _, ok := <-hijacked; ok {
|
|
||||||
fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if execConfig.AttachStdin {
|
|
||||||
in = cli.in
|
|
||||||
}
|
|
||||||
if execConfig.AttachStdout {
|
|
||||||
out = cli.out
|
|
||||||
}
|
|
||||||
if execConfig.AttachStderr {
|
|
||||||
if execConfig.Tty {
|
|
||||||
stderr = cli.out
|
|
||||||
} else {
|
|
||||||
stderr = cli.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errCh = promise.Go(func() error {
|
|
||||||
return cli.hijack("POST", "/exec/"+execID+"/start", execConfig.Tty, in, out, stderr, hijacked, execConfig)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Acknowledge the hijack before starting
|
|
||||||
select {
|
|
||||||
case closer := <-hijacked:
|
|
||||||
// Make sure that hijack gets closed when returning. (result
|
|
||||||
// in closing hijack chan and freeing server's goroutines.
|
|
||||||
if closer != nil {
|
|
||||||
defer closer.Close()
|
|
||||||
}
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error hijack: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if execConfig.Tty && cli.isTerminalIn {
|
|
||||||
if err := cli.monitorTtySize(execID, true); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := <-errCh; err != nil {
|
|
||||||
logrus.Debugf("Error hijack: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var status int
|
|
||||||
if _, status, err = getExecExitCode(cli, execID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if status != 0 {
|
|
||||||
return Cli.StatusError{StatusCode: status}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdExport exports a filesystem as a tar archive.
|
|
||||||
//
|
|
||||||
// The tar archive is streamed to STDOUT by default or written to a file.
|
|
||||||
//
|
|
||||||
// Usage: docker export [OPTIONS] CONTAINER
|
|
||||||
func (cli *DockerCli) CmdExport(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("export", []string{"CONTAINER"}, "Export the contents of a container's filesystem as a tar archive", true)
|
|
||||||
outfile := cmd.String([]string{"o", "-output"}, "", "Write to a file, instead of STDOUT")
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
output io.Writer = cli.out
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if *outfile != "" {
|
|
||||||
output, err = os.Create(*outfile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if cli.isTerminalOut {
|
|
||||||
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
|
|
||||||
}
|
|
||||||
|
|
||||||
image := cmd.Arg(0)
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
out: output,
|
|
||||||
}
|
|
||||||
if _, err := cli.stream("GET", "/containers/"+image+"/export", sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/autogen/dockerversion"
|
|
||||||
"github.com/docker/docker/pkg/promise"
|
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
|
||||||
"github.com/docker/docker/pkg/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tlsClientCon struct {
|
|
||||||
*tls.Conn
|
|
||||||
rawConn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *tlsClientCon) CloseWrite() error {
|
|
||||||
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
|
|
||||||
// on its underlying connection.
|
|
||||||
if cwc, ok := c.rawConn.(interface {
|
|
||||||
CloseWrite() error
|
|
||||||
}); ok {
|
|
||||||
return cwc.CloseWrite()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
|
|
||||||
return tlsDialWithDialer(new(net.Dialer), network, addr, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
|
|
||||||
// order to return our custom tlsClientCon struct which holds both the tls.Conn
|
|
||||||
// object _and_ its underlying raw connection. The rationale for this is that
|
|
||||||
// we need to be able to close the write end of the connection when attaching,
|
|
||||||
// which tls.Conn does not provide.
|
|
||||||
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
|
|
||||||
// We want the Timeout and Deadline values from dialer to cover the
|
|
||||||
// whole process: TCP connection and TLS handshake. This means that we
|
|
||||||
// also need to start our own timers now.
|
|
||||||
timeout := dialer.Timeout
|
|
||||||
|
|
||||||
if !dialer.Deadline.IsZero() {
|
|
||||||
deadlineTimeout := dialer.Deadline.Sub(time.Now())
|
|
||||||
if timeout == 0 || deadlineTimeout < timeout {
|
|
||||||
timeout = deadlineTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errChannel chan error
|
|
||||||
|
|
||||||
if timeout != 0 {
|
|
||||||
errChannel = make(chan error, 2)
|
|
||||||
time.AfterFunc(timeout, func() {
|
|
||||||
errChannel <- errors.New("")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
rawConn, err := dialer.Dial(network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// When we set up a TCP connection for hijack, there could be long periods
|
|
||||||
// of inactivity (a long running command with no output) that in certain
|
|
||||||
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
||||||
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
||||||
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
||||||
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
colonPos := strings.LastIndex(addr, ":")
|
|
||||||
if colonPos == -1 {
|
|
||||||
colonPos = len(addr)
|
|
||||||
}
|
|
||||||
hostname := addr[:colonPos]
|
|
||||||
|
|
||||||
// If no ServerName is set, infer the ServerName
|
|
||||||
// from the hostname we're connecting to.
|
|
||||||
if config.ServerName == "" {
|
|
||||||
// Make a copy to avoid polluting argument or default.
|
|
||||||
c := *config
|
|
||||||
c.ServerName = hostname
|
|
||||||
config = &c
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := tls.Client(rawConn, config)
|
|
||||||
|
|
||||||
if timeout == 0 {
|
|
||||||
err = conn.Handshake()
|
|
||||||
} else {
|
|
||||||
go func() {
|
|
||||||
errChannel <- conn.Handshake()
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = <-errChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
rawConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is Docker difference with standard's crypto/tls package: returned a
|
|
||||||
// wrapper which holds both the TLS and raw connections.
|
|
||||||
return &tlsClientCon{conn, rawConn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) dial() (net.Conn, error) {
|
|
||||||
if cli.tlsConfig != nil && cli.proto != "unix" {
|
|
||||||
// Notice this isn't Go standard's tls.Dial function
|
|
||||||
return tlsDial(cli.proto, cli.addr, cli.tlsConfig)
|
|
||||||
}
|
|
||||||
return net.Dial(cli.proto, cli.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
|
|
||||||
defer func() {
|
|
||||||
if started != nil {
|
|
||||||
close(started)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
params, err := cli.encodeData(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(method, fmt.Sprintf("%s/v%s%s", cli.basePath, api.Version, path), params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
|
|
||||||
// then the user can't change OUR headers
|
|
||||||
for k, v := range cli.configFile.HTTPHeaders {
|
|
||||||
req.Header.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")")
|
|
||||||
req.Header.Set("Content-Type", "text/plain")
|
|
||||||
req.Header.Set("Connection", "Upgrade")
|
|
||||||
req.Header.Set("Upgrade", "tcp")
|
|
||||||
req.Host = cli.addr
|
|
||||||
|
|
||||||
dial, err := cli.dial()
|
|
||||||
// When we set up a TCP connection for hijack, there could be long periods
|
|
||||||
// of inactivity (a long running command with no output) that in certain
|
|
||||||
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
||||||
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
||||||
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
||||||
if tcpConn, ok := dial.(*net.TCPConn); ok {
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "connection refused") {
|
|
||||||
return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
clientconn := httputil.NewClientConn(dial, nil)
|
|
||||||
defer clientconn.Close()
|
|
||||||
|
|
||||||
// Server hijacks the connection, error 'connection closed' expected
|
|
||||||
clientconn.Do(req)
|
|
||||||
|
|
||||||
rwc, br := clientconn.Hijack()
|
|
||||||
defer rwc.Close()
|
|
||||||
|
|
||||||
if started != nil {
|
|
||||||
started <- rwc
|
|
||||||
}
|
|
||||||
|
|
||||||
var receiveStdout chan error
|
|
||||||
|
|
||||||
var oldState *term.State
|
|
||||||
|
|
||||||
if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
|
|
||||||
oldState, err = term.SetRawTerminal(cli.inFd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer term.RestoreTerminal(cli.inFd, oldState)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stdout != nil || stderr != nil {
|
|
||||||
receiveStdout = promise.Go(func() (err error) {
|
|
||||||
defer func() {
|
|
||||||
if in != nil {
|
|
||||||
if setRawTerminal && cli.isTerminalIn {
|
|
||||||
term.RestoreTerminal(cli.inFd, oldState)
|
|
||||||
}
|
|
||||||
// For some reason this Close call blocks on darwin..
|
|
||||||
// As the client exists right after, simply discard the close
|
|
||||||
// until we find a better solution.
|
|
||||||
if runtime.GOOS != "darwin" {
|
|
||||||
in.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// When TTY is ON, use regular copy
|
|
||||||
if setRawTerminal && stdout != nil {
|
|
||||||
_, err = io.Copy(stdout, br)
|
|
||||||
} else {
|
|
||||||
_, err = stdcopy.StdCopy(stdout, stderr, br)
|
|
||||||
}
|
|
||||||
logrus.Debugf("[hijack] End of stdout")
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sendStdin := promise.Go(func() error {
|
|
||||||
if in != nil {
|
|
||||||
io.Copy(rwc, in)
|
|
||||||
logrus.Debugf("[hijack] End of stdin")
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn, ok := rwc.(interface {
|
|
||||||
CloseWrite() error
|
|
||||||
}); ok {
|
|
||||||
if err := conn.CloseWrite(); err != nil {
|
|
||||||
logrus.Debugf("Couldn't send EOF: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Discard errors due to pipe interruption
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if stdout != nil || stderr != nil {
|
|
||||||
if err := <-receiveStdout; err != nil {
|
|
||||||
logrus.Debugf("Error receiveStdout: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cli.isTerminalIn {
|
|
||||||
if err := <-sendStdin; err != nil {
|
|
||||||
logrus.Debugf("Error sendStdin: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/stringutils"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdHistory shows the history of an image.
|
|
||||||
//
|
|
||||||
// Usage: docker history [OPTIONS] IMAGE
|
|
||||||
func (cli *DockerCli) CmdHistory(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("history", []string{"IMAGE"}, "Show the history of an image", true)
|
|
||||||
human := cmd.Bool([]string{"H", "-human"}, true, "Print sizes and dates in human readable format")
|
|
||||||
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs")
|
|
||||||
noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/images/"+cmd.Arg(0)+"/history", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
history := []types.ImageHistory{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&history); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
||||||
if !*quiet {
|
|
||||||
fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range history {
|
|
||||||
if *noTrunc {
|
|
||||||
fmt.Fprintf(w, entry.ID)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, stringid.TruncateID(entry.ID))
|
|
||||||
}
|
|
||||||
if !*quiet {
|
|
||||||
if *human {
|
|
||||||
fmt.Fprintf(w, "\t%s ago\t", units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))))
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "\t%s\t", time.Unix(entry.Created, 0).Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *noTrunc {
|
|
||||||
fmt.Fprintf(w, "%s\t", entry.CreatedBy)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "%s\t", stringutils.Truncate(entry.CreatedBy, 45))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *human {
|
|
||||||
fmt.Fprintf(w, "%s\t", units.HumanSize(float64(entry.Size)))
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "%d\t", entry.Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "%s", entry.Comment)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "\n")
|
|
||||||
}
|
|
||||||
w.Flush()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/parsers/filters"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
"github.com/docker/docker/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified.
|
|
||||||
//
|
|
||||||
// Usage: docker images [OPTIONS] [REPOSITORY]
|
|
||||||
func (cli *DockerCli) CmdImages(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("images", []string{"[REPOSITORY]"}, "List images", true)
|
|
||||||
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs")
|
|
||||||
all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
|
|
||||||
noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
|
|
||||||
showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
|
|
||||||
|
|
||||||
flFilter := opts.NewListOpts(nil)
|
|
||||||
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
||||||
cmd.Require(flag.Max, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
// Consolidate all filter flags, and sanity check them early.
|
|
||||||
// They'll get process in the daemon/server.
|
|
||||||
imageFilterArgs := filters.Args{}
|
|
||||||
for _, f := range flFilter.GetAll() {
|
|
||||||
var err error
|
|
||||||
imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matchName := cmd.Arg(0)
|
|
||||||
v := url.Values{}
|
|
||||||
if len(imageFilterArgs) > 0 {
|
|
||||||
filterJSON, err := filters.ToParam(imageFilterArgs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Set("filters", filterJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.NArg() == 1 {
|
|
||||||
// FIXME rename this parameter, to not be confused with the filters flag
|
|
||||||
v.Set("filter", matchName)
|
|
||||||
}
|
|
||||||
if *all {
|
|
||||||
v.Set("all", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/images/json?"+v.Encode(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
images := []types.Image{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&images); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
||||||
if !*quiet {
|
|
||||||
if *showDigests {
|
|
||||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
ID := image.ID
|
|
||||||
if !*noTrunc {
|
|
||||||
ID = stringid.TruncateID(ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
repoTags := image.RepoTags
|
|
||||||
repoDigests := image.RepoDigests
|
|
||||||
|
|
||||||
if len(repoTags) == 1 && repoTags[0] == "<none>:<none>" && len(repoDigests) == 1 && repoDigests[0] == "<none>@<none>" {
|
|
||||||
// dangling image - clear out either repoTags or repoDigsts so we only show it once below
|
|
||||||
repoDigests = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// combine the tags and digests lists
|
|
||||||
tagsAndDigests := append(repoTags, repoDigests...)
|
|
||||||
for _, repoAndRef := range tagsAndDigests {
|
|
||||||
repo, ref := parsers.ParseRepositoryTag(repoAndRef)
|
|
||||||
// default tag and digest to none - if there's a value, it'll be set below
|
|
||||||
tag := "<none>"
|
|
||||||
digest := "<none>"
|
|
||||||
if utils.DigestReference(ref) {
|
|
||||||
digest = ref
|
|
||||||
} else {
|
|
||||||
tag = ref
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*quiet {
|
|
||||||
if *showDigests {
|
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize)))
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(w, ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*quiet {
|
|
||||||
w.Flush()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/urlutil"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdImport creates an empty filesystem image, imports the contents of the tarball into the image, and optionally tags the image.
|
|
||||||
//
|
|
||||||
// The URL argument is the address of a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) file or a path to local file relative to docker client. If the URL is '-', then the tar file is read from STDIN.
|
|
||||||
//
|
|
||||||
// Usage: docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
|
|
||||||
func (cli *DockerCli) CmdImport(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("import", []string{"file|URL|- [REPOSITORY[:TAG]]"}, "Create an empty filesystem image and import the contents of the\ntarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) into it, then\noptionally tag it.", true)
|
|
||||||
flChanges := opts.NewListOpts(nil)
|
|
||||||
cmd.Var(&flChanges, []string{"c", "-change"}, "Apply Dockerfile instruction to the created image")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
v = url.Values{}
|
|
||||||
src = cmd.Arg(0)
|
|
||||||
repository = cmd.Arg(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
v.Set("fromSrc", src)
|
|
||||||
v.Set("repo", repository)
|
|
||||||
for _, change := range flChanges.GetAll() {
|
|
||||||
v.Add("changes", change)
|
|
||||||
}
|
|
||||||
if cmd.NArg() == 3 {
|
|
||||||
fmt.Fprintf(cli.err, "[DEPRECATED] The format 'file|URL|- [REPOSITORY [TAG]]' has been deprecated. Please use file|URL|- [REPOSITORY[:TAG]]\n")
|
|
||||||
v.Set("tag", cmd.Arg(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
if repository != "" {
|
|
||||||
//Check if the given image name can be resolved
|
|
||||||
repo, _ := parsers.ParseRepositoryTag(repository)
|
|
||||||
if err := registry.ValidateRepositoryName(repo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var in io.Reader
|
|
||||||
|
|
||||||
if src == "-" {
|
|
||||||
in = cli.in
|
|
||||||
} else if !urlutil.IsURL(src) {
|
|
||||||
v.Set("fromSrc", "-")
|
|
||||||
file, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
in = file
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
in: in,
|
|
||||||
out: cli.out,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := cli.stream("POST", "/images/create?"+v.Encode(), sopts)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdInfo displays system-wide information.
|
|
||||||
//
|
|
||||||
// Usage: docker info
|
|
||||||
func (cli *DockerCli) CmdInfo(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("info", nil, "Display system-wide information", true)
|
|
||||||
cmd.Require(flag.Exact, 0)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/info", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
info := &types.Info{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(info); err != nil {
|
|
||||||
return fmt.Errorf("Error reading remote info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(cli.out, "Containers: %d\n", info.Containers)
|
|
||||||
fmt.Fprintf(cli.out, "Images: %d\n", info.Images)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Storage Driver: %s\n", info.Driver)
|
|
||||||
if info.DriverStatus != nil {
|
|
||||||
for _, pair := range info.DriverStatus {
|
|
||||||
fmt.Fprintf(cli.out, " %s: %s\n", pair[0], pair[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Execution Driver: %s\n", info.ExecutionDriver)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Logging Driver: %s\n", info.LoggingDriver)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Kernel Version: %s\n", info.KernelVersion)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Operating System: %s\n", info.OperatingSystem)
|
|
||||||
fmt.Fprintf(cli.out, "CPUs: %d\n", info.NCPU)
|
|
||||||
fmt.Fprintf(cli.out, "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal)))
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Name: %s\n", info.Name)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "ID: %s\n", info.ID)
|
|
||||||
|
|
||||||
if info.Debug {
|
|
||||||
fmt.Fprintf(cli.out, "Debug mode (server): %v\n", info.Debug)
|
|
||||||
fmt.Fprintf(cli.out, "File Descriptors: %d\n", info.NFd)
|
|
||||||
fmt.Fprintf(cli.out, "Goroutines: %d\n", info.NGoroutines)
|
|
||||||
fmt.Fprintf(cli.out, "System Time: %s\n", info.SystemTime)
|
|
||||||
fmt.Fprintf(cli.out, "EventsListeners: %d\n", info.NEventsListener)
|
|
||||||
fmt.Fprintf(cli.out, "Init SHA1: %s\n", info.InitSha1)
|
|
||||||
fmt.Fprintf(cli.out, "Init Path: %s\n", info.InitPath)
|
|
||||||
fmt.Fprintf(cli.out, "Docker Root Dir: %s\n", info.DockerRootDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Http Proxy: %s\n", info.HttpProxy)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "Https Proxy: %s\n", info.HttpsProxy)
|
|
||||||
ioutils.FprintfIfNotEmpty(cli.out, "No Proxy: %s\n", info.NoProxy)
|
|
||||||
|
|
||||||
if info.IndexServerAddress != "" {
|
|
||||||
u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
|
|
||||||
if len(u) > 0 {
|
|
||||||
fmt.Fprintf(cli.out, "Username: %v\n", u)
|
|
||||||
fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Only output these warnings if the server supports these features
|
|
||||||
if h, err := httputils.ParseServerHeader(serverResp.header.Get("Server")); err == nil {
|
|
||||||
if h.OS != "windows" {
|
|
||||||
if !info.MemoryLimit {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: No memory limit support\n")
|
|
||||||
}
|
|
||||||
if !info.SwapLimit {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: No swap limit support\n")
|
|
||||||
}
|
|
||||||
if !info.IPv4Forwarding {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: IPv4 forwarding is disabled.\n")
|
|
||||||
}
|
|
||||||
if !info.BridgeNfIptables {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-iptables is disabled\n")
|
|
||||||
}
|
|
||||||
if !info.BridgeNfIp6tables {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: bridge-nf-call-ip6tables is disabled\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Labels != nil {
|
|
||||||
fmt.Fprintln(cli.out, "Labels:")
|
|
||||||
for _, attribute := range info.Labels {
|
|
||||||
fmt.Fprintf(cli.out, " %s\n", attribute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.ExperimentalBuild {
|
|
||||||
fmt.Fprintf(cli.out, "Experimental: true\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
var funcMap = template.FuncMap{
|
|
||||||
"json": func(v interface{}) string {
|
|
||||||
a, _ := json.Marshal(v)
|
|
||||||
return string(a)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdInspect displays low-level information on one or more containers or images.
|
|
||||||
//
|
|
||||||
// Usage: docker inspect [OPTIONS] CONTAINER|IMAGE [CONTAINER|IMAGE...]
|
|
||||||
func (cli *DockerCli) CmdInspect(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("inspect", []string{"CONTAINER|IMAGE [CONTAINER|IMAGE...]"}, "Return low-level information on a container or image", true)
|
|
||||||
tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template")
|
|
||||||
inspectType := cmd.String([]string{"-type"}, "", "Return JSON for specified type, (e.g image or container)")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var tmpl *template.Template
|
|
||||||
var err error
|
|
||||||
var obj []byte
|
|
||||||
|
|
||||||
if *tmplStr != "" {
|
|
||||||
if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil {
|
|
||||||
return Cli.StatusError{StatusCode: 64,
|
|
||||||
Status: "Template parsing error: " + err.Error()}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if *inspectType != "" && *inspectType != "container" && *inspectType != "image" {
|
|
||||||
return fmt.Errorf("%q is not a valid value for --type", *inspectType)
|
|
||||||
}
|
|
||||||
|
|
||||||
indented := new(bytes.Buffer)
|
|
||||||
indented.WriteString("[\n")
|
|
||||||
status := 0
|
|
||||||
isImage := false
|
|
||||||
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
|
|
||||||
if *inspectType == "" || *inspectType == "container" {
|
|
||||||
obj, _, err = readBody(cli.call("GET", "/containers/"+name+"/json", nil, nil))
|
|
||||||
if err != nil && *inspectType == "container" {
|
|
||||||
if strings.Contains(err.Error(), "No such") {
|
|
||||||
fmt.Fprintf(cli.err, "Error: No such container: %s\n", name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.err, "%s", err)
|
|
||||||
}
|
|
||||||
status = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj == nil && (*inspectType == "" || *inspectType == "image") {
|
|
||||||
obj, _, err = readBody(cli.call("GET", "/images/"+name+"/json", nil, nil))
|
|
||||||
isImage = true
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "No such") {
|
|
||||||
if *inspectType == "" {
|
|
||||||
fmt.Fprintf(cli.err, "Error: No such image or container: %s\n", name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.err, "Error: No such image: %s\n", name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.err, "%s", err)
|
|
||||||
}
|
|
||||||
status = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if tmpl == nil {
|
|
||||||
if err := json.Indent(indented, obj, "", " "); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
status = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rdr := bytes.NewReader(obj)
|
|
||||||
dec := json.NewDecoder(rdr)
|
|
||||||
|
|
||||||
if isImage {
|
|
||||||
inspPtr := types.ImageInspect{}
|
|
||||||
if err := dec.Decode(&inspPtr); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
status = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := tmpl.Execute(cli.out, inspPtr); err != nil {
|
|
||||||
rdr.Seek(0, 0)
|
|
||||||
var raw interface{}
|
|
||||||
if err := dec.Decode(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = tmpl.Execute(cli.out, raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
inspPtr := types.ContainerJSON{}
|
|
||||||
if err := dec.Decode(&inspPtr); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
status = 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := tmpl.Execute(cli.out, inspPtr); err != nil {
|
|
||||||
rdr.Seek(0, 0)
|
|
||||||
var raw interface{}
|
|
||||||
if err := dec.Decode(&raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = tmpl.Execute(cli.out, raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cli.out.Write([]byte{'\n'})
|
|
||||||
}
|
|
||||||
indented.WriteString(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
if indented.Len() > 1 {
|
|
||||||
// Remove trailing ','
|
|
||||||
indented.Truncate(indented.Len() - 1)
|
|
||||||
}
|
|
||||||
indented.WriteString("]\n")
|
|
||||||
|
|
||||||
if tmpl == nil {
|
|
||||||
// Note that we will always write "[]" when "-f" isn't specified,
|
|
||||||
// to make sure the output would always be array, see
|
|
||||||
// https://github.com/docker/docker/pull/9500#issuecomment-65846734
|
|
||||||
if _, err := io.Copy(cli.out, indented); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if status != 0 {
|
|
||||||
return Cli.StatusError{StatusCode: status}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdKill kills one or more running container using SIGKILL or a specified signal.
|
|
||||||
//
|
|
||||||
// Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdKill(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("kill", []string{"CONTAINER [CONTAINER...]"}, "Kill a running container using SIGKILL or a specified signal", true)
|
|
||||||
signal := cmd.String([]string{"s", "-signal"}, "KILL", "Signal to send to the container")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/kill?signal=%s", name, *signal), nil, nil)); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to kill containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdLoad loads an image from a tar archive.
|
|
||||||
//
|
|
||||||
// The tar archive is read from STDIN by default, or from a tar archive file.
|
|
||||||
//
|
|
||||||
// Usage: docker load [OPTIONS]
|
|
||||||
func (cli *DockerCli) CmdLoad(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("load", nil, "Load an image from a tar archive or STDIN", true)
|
|
||||||
infile := cmd.String([]string{"i", "-input"}, "", "Read from a tar archive file, instead of STDIN")
|
|
||||||
cmd.Require(flag.Exact, 0)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
input io.Reader = cli.in
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if *infile != "" {
|
|
||||||
input, err = os.Open(*infile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
in: input,
|
|
||||||
out: cli.out,
|
|
||||||
}
|
|
||||||
if _, err := cli.stream("POST", "/images/load", sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/term"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdLogin logs in or registers a user to a Docker registry service.
|
|
||||||
//
|
|
||||||
// If no server is specified, the user will be logged into or registered to the registry's index server.
|
|
||||||
//
|
|
||||||
// Usage: docker login SERVER
|
|
||||||
func (cli *DockerCli) CmdLogin(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("login", []string{"[SERVER]"}, "Register or log in to a Docker registry server, if no server is\nspecified \""+registry.IndexServer+"\" is the default.", true)
|
|
||||||
cmd.Require(flag.Max, 1)
|
|
||||||
|
|
||||||
var username, password, email string
|
|
||||||
|
|
||||||
cmd.StringVar(&username, []string{"u", "-username"}, "", "Username")
|
|
||||||
cmd.StringVar(&password, []string{"p", "-password"}, "", "Password")
|
|
||||||
cmd.StringVar(&email, []string{"e", "-email"}, "", "Email")
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverAddress := registry.IndexServer
|
|
||||||
if len(cmd.Args()) > 0 {
|
|
||||||
serverAddress = cmd.Arg(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
promptDefault := func(prompt string, configDefault string) {
|
|
||||||
if configDefault == "" {
|
|
||||||
fmt.Fprintf(cli.out, "%s: ", prompt)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readInput := func(in io.Reader, out io.Writer) string {
|
|
||||||
reader := bufio.NewReader(in)
|
|
||||||
line, _, err := reader.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(out, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return string(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
authconfig, ok := cli.configFile.AuthConfigs[serverAddress]
|
|
||||||
if !ok {
|
|
||||||
authconfig = cliconfig.AuthConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if username == "" {
|
|
||||||
promptDefault("Username", authconfig.Username)
|
|
||||||
username = readInput(cli.in, cli.out)
|
|
||||||
username = strings.Trim(username, " ")
|
|
||||||
if username == "" {
|
|
||||||
username = authconfig.Username
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Assume that a different username means they may not want to use
|
|
||||||
// the password or email from the config file, so prompt them
|
|
||||||
if username != authconfig.Username {
|
|
||||||
if password == "" {
|
|
||||||
oldState, err := term.SaveState(cli.inFd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "Password: ")
|
|
||||||
term.DisableEcho(cli.inFd, oldState)
|
|
||||||
|
|
||||||
password = readInput(cli.in, cli.out)
|
|
||||||
fmt.Fprint(cli.out, "\n")
|
|
||||||
|
|
||||||
term.RestoreTerminal(cli.inFd, oldState)
|
|
||||||
if password == "" {
|
|
||||||
return fmt.Errorf("Error : Password Required")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if email == "" {
|
|
||||||
promptDefault("Email", authconfig.Email)
|
|
||||||
email = readInput(cli.in, cli.out)
|
|
||||||
if email == "" {
|
|
||||||
email = authconfig.Email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// However, if they don't override the username use the
|
|
||||||
// password or email from the cmd line if specified. IOW, allow
|
|
||||||
// then to change/override them. And if not specified, just
|
|
||||||
// use what's in the config file
|
|
||||||
if password == "" {
|
|
||||||
password = authconfig.Password
|
|
||||||
}
|
|
||||||
if email == "" {
|
|
||||||
email = authconfig.Email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authconfig.Username = username
|
|
||||||
authconfig.Password = password
|
|
||||||
authconfig.Email = email
|
|
||||||
authconfig.ServerAddress = serverAddress
|
|
||||||
cli.configFile.AuthConfigs[serverAddress] = authconfig
|
|
||||||
|
|
||||||
serverResp, err := cli.call("POST", "/auth", cli.configFile.AuthConfigs[serverAddress], nil)
|
|
||||||
if serverResp.statusCode == 401 {
|
|
||||||
delete(cli.configFile.AuthConfigs, serverAddress)
|
|
||||||
if err2 := cli.configFile.Save(); err2 != nil {
|
|
||||||
fmt.Fprintf(cli.out, "WARNING: could not save config file: %v\n", err2)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var response types.AuthResponse
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
|
|
||||||
// Upon error, remove entry
|
|
||||||
delete(cli.configFile.AuthConfigs, serverAddress)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cli.configFile.Save(); err != nil {
|
|
||||||
return fmt.Errorf("Error saving config file: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "WARNING: login credentials saved in %s\n", cli.configFile.Filename())
|
|
||||||
|
|
||||||
if response.Status != "" {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", response.Status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdLogout logs a user out from a Docker registry.
|
|
||||||
//
|
|
||||||
// If no server is specified, the user will be logged out from the registry's index server.
|
|
||||||
//
|
|
||||||
// Usage: docker logout [SERVER]
|
|
||||||
func (cli *DockerCli) CmdLogout(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("logout", []string{"[SERVER]"}, "Log out from a Docker registry, if no server is\nspecified \""+registry.IndexServer+"\" is the default.", true)
|
|
||||||
cmd.Require(flag.Max, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverAddress := registry.IndexServer
|
|
||||||
if len(cmd.Args()) > 0 {
|
|
||||||
serverAddress = cmd.Arg(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cli.configFile.AuthConfigs[serverAddress]; !ok {
|
|
||||||
fmt.Fprintf(cli.out, "Not logged in to %s\n", serverAddress)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "Remove login credentials for %s\n", serverAddress)
|
|
||||||
delete(cli.configFile.AuthConfigs, serverAddress)
|
|
||||||
|
|
||||||
if err := cli.configFile.Save(); err != nil {
|
|
||||||
return fmt.Errorf("Failed to save docker config: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/timeutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdLogs fetches the logs of a given container.
|
|
||||||
//
|
|
||||||
// docker logs [OPTIONS] CONTAINER
|
|
||||||
func (cli *DockerCli) CmdLogs(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("logs", []string{"CONTAINER"}, "Fetch the logs of a container", true)
|
|
||||||
follow := cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
|
|
||||||
since := cmd.String([]string{"-since"}, "", "Show logs since timestamp")
|
|
||||||
times := cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
|
|
||||||
tail := cmd.String([]string{"-tail"}, "all", "Number of lines to show from the end of the logs")
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
name := cmd.Arg(0)
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+name+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var c types.ContainerJSON
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if logType := c.HostConfig.LogConfig.Type; logType != "json-file" {
|
|
||||||
return fmt.Errorf("\"logs\" command is supported only for \"json-file\" logging driver (got: %s)", logType)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("stdout", "1")
|
|
||||||
v.Set("stderr", "1")
|
|
||||||
|
|
||||||
if *since != "" {
|
|
||||||
v.Set("since", timeutils.GetTimestamp(*since, time.Now()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *times {
|
|
||||||
v.Set("timestamps", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *follow {
|
|
||||||
v.Set("follow", "1")
|
|
||||||
}
|
|
||||||
v.Set("tail", *tail)
|
|
||||||
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: c.Config.Tty,
|
|
||||||
out: cli.out,
|
|
||||||
err: cli.err,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = cli.stream("GET", "/containers/"+name+"/logs?"+v.Encode(), sopts)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// +build experimental
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
nwclient "github.com/docker/libnetwork/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *DockerCli) CmdNetwork(args ...string) error {
|
|
||||||
nCli := nwclient.NewNetworkCli(cli.out, cli.err, nwclient.CallFunc(cli.callWrapper))
|
|
||||||
args = append([]string{"network"}, args...)
|
|
||||||
return nCli.Cmd(os.Args[0], args...)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdPause pauses all processes within one or more containers.
|
|
||||||
//
|
|
||||||
// Usage: docker pause CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdPause(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("pause", []string{"CONTAINER [CONTAINER...]"}, "Pause all processes within a container", true)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/pause", name), nil, nil)); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to pause containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/nat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdPort lists port mappings for a container.
|
|
||||||
// If a private port is specified, it also shows the public-facing port that is NATed to the private port.
|
|
||||||
//
|
|
||||||
// Usage: docker port CONTAINER [PRIVATE_PORT[/PROTO]]
|
|
||||||
func (cli *DockerCli) CmdPort(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("port", []string{"CONTAINER [PRIVATE_PORT[/PROTO]]"}, "List port mappings for the CONTAINER, or lookup the public-facing port that\nis NAT-ed to the PRIVATE_PORT", true)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var c struct {
|
|
||||||
NetworkSettings struct {
|
|
||||||
Ports nat.PortMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.NArg() == 2 {
|
|
||||||
var (
|
|
||||||
port = cmd.Arg(1)
|
|
||||||
proto = "tcp"
|
|
||||||
parts = strings.SplitN(port, "/", 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(parts) == 2 && len(parts[1]) != 0 {
|
|
||||||
port = parts[0]
|
|
||||||
proto = parts[1]
|
|
||||||
}
|
|
||||||
natPort := port + "/" + proto
|
|
||||||
newP, err := nat.NewPort(proto, port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil {
|
|
||||||
for _, frontend := range frontends {
|
|
||||||
fmt.Fprintf(cli.out, "%s:%s\n", frontend.HostIP, frontend.HostPort)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Error: No public port '%s' published for %s", natPort, cmd.Arg(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
for from, frontends := range c.NetworkSettings.Ports {
|
|
||||||
for _, frontend := range frontends {
|
|
||||||
fmt.Fprintf(cli.out, "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/client/ps"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers/filters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdPs outputs a list of Docker containers.
|
|
||||||
//
|
|
||||||
// Usage: docker ps [OPTIONS]
|
|
||||||
func (cli *DockerCli) CmdPs(args ...string) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
|
|
||||||
psFilterArgs = filters.Args{}
|
|
||||||
v = url.Values{}
|
|
||||||
|
|
||||||
cmd = Cli.Subcmd("ps", nil, "List containers", true)
|
|
||||||
quiet = cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs")
|
|
||||||
size = cmd.Bool([]string{"s", "-size"}, false, "Display total file sizes")
|
|
||||||
all = cmd.Bool([]string{"a", "-all"}, false, "Show all containers (default shows just running)")
|
|
||||||
noTrunc = cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
|
|
||||||
nLatest = cmd.Bool([]string{"l", "-latest"}, false, "Show the latest created container, include non-running")
|
|
||||||
since = cmd.String([]string{"#sinceId", "#-since-id", "-since"}, "", "Show created since Id or Name, include non-running")
|
|
||||||
before = cmd.String([]string{"#beforeId", "#-before-id", "-before"}, "", "Show only container created before Id or Name")
|
|
||||||
last = cmd.Int([]string{"n"}, -1, "Show n last created containers, include non-running")
|
|
||||||
format = cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template")
|
|
||||||
flFilter = opts.NewListOpts(nil)
|
|
||||||
)
|
|
||||||
cmd.Require(flag.Exact, 0)
|
|
||||||
|
|
||||||
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
if *last == -1 && *nLatest {
|
|
||||||
*last = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if *all {
|
|
||||||
v.Set("all", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *last != -1 {
|
|
||||||
v.Set("limit", strconv.Itoa(*last))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *since != "" {
|
|
||||||
v.Set("since", *since)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *before != "" {
|
|
||||||
v.Set("before", *before)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *size {
|
|
||||||
v.Set("size", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consolidate all filter flags, and sanity check them.
|
|
||||||
// They'll get processed in the daemon/server.
|
|
||||||
for _, f := range flFilter.GetAll() {
|
|
||||||
if psFilterArgs, err = filters.ParseFlag(f, psFilterArgs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(psFilterArgs) > 0 {
|
|
||||||
filterJSON, err := filters.ToParam(psFilterArgs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("filters", filterJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/json?"+v.Encode(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
containers := []types.Container{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&containers); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f := *format
|
|
||||||
if len(f) == 0 {
|
|
||||||
if len(cli.PsFormat()) > 0 {
|
|
||||||
f = cli.PsFormat()
|
|
||||||
} else {
|
|
||||||
f = "table"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
psCtx := ps.Context{
|
|
||||||
Output: cli.out,
|
|
||||||
Format: f,
|
|
||||||
Quiet: *quiet,
|
|
||||||
Size: *size,
|
|
||||||
Trunc: !*noTrunc,
|
|
||||||
}
|
|
||||||
|
|
||||||
ps.Format(psCtx, containers)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
package ps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/stringutils"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tableKey = "table"
|
|
||||||
|
|
||||||
idHeader = "CONTAINER ID"
|
|
||||||
imageHeader = "IMAGE"
|
|
||||||
namesHeader = "NAMES"
|
|
||||||
commandHeader = "COMMAND"
|
|
||||||
createdAtHeader = "CREATED AT"
|
|
||||||
runningForHeader = "CREATED"
|
|
||||||
statusHeader = "STATUS"
|
|
||||||
portsHeader = "PORTS"
|
|
||||||
sizeHeader = "SIZE"
|
|
||||||
labelsHeader = "LABELS"
|
|
||||||
)
|
|
||||||
|
|
||||||
type containerContext struct {
|
|
||||||
trunc bool
|
|
||||||
header []string
|
|
||||||
c types.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) ID() string {
|
|
||||||
c.addHeader(idHeader)
|
|
||||||
if c.trunc {
|
|
||||||
return stringid.TruncateID(c.c.ID)
|
|
||||||
}
|
|
||||||
return c.c.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Names() string {
|
|
||||||
c.addHeader(namesHeader)
|
|
||||||
names := stripNamePrefix(c.c.Names)
|
|
||||||
if c.trunc {
|
|
||||||
for _, name := range names {
|
|
||||||
if len(strings.Split(name, "/")) == 1 {
|
|
||||||
names = []string{name}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(names, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Image() string {
|
|
||||||
c.addHeader(imageHeader)
|
|
||||||
if c.c.Image == "" {
|
|
||||||
return "<no image>"
|
|
||||||
}
|
|
||||||
return c.c.Image
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Command() string {
|
|
||||||
c.addHeader(commandHeader)
|
|
||||||
command := c.c.Command
|
|
||||||
if c.trunc {
|
|
||||||
command = stringutils.Truncate(command, 20)
|
|
||||||
}
|
|
||||||
return strconv.Quote(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) CreatedAt() string {
|
|
||||||
c.addHeader(createdAtHeader)
|
|
||||||
return time.Unix(int64(c.c.Created), 0).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) RunningFor() string {
|
|
||||||
c.addHeader(runningForHeader)
|
|
||||||
createdAt := time.Unix(int64(c.c.Created), 0)
|
|
||||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Ports() string {
|
|
||||||
c.addHeader(portsHeader)
|
|
||||||
return api.DisplayablePorts(c.c.Ports)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Status() string {
|
|
||||||
c.addHeader(statusHeader)
|
|
||||||
return c.c.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Size() string {
|
|
||||||
c.addHeader(sizeHeader)
|
|
||||||
srw := units.HumanSize(float64(c.c.SizeRw))
|
|
||||||
sv := units.HumanSize(float64(c.c.SizeRootFs))
|
|
||||||
|
|
||||||
sf := srw
|
|
||||||
if c.c.SizeRootFs > 0 {
|
|
||||||
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
|
||||||
}
|
|
||||||
return sf
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Labels() string {
|
|
||||||
c.addHeader(labelsHeader)
|
|
||||||
if c.c.Labels == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var joinLabels []string
|
|
||||||
for k, v := range c.c.Labels {
|
|
||||||
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
|
||||||
}
|
|
||||||
return strings.Join(joinLabels, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) Label(name string) string {
|
|
||||||
n := strings.Split(name, ".")
|
|
||||||
r := strings.NewReplacer("-", " ", "_", " ")
|
|
||||||
h := r.Replace(n[len(n)-1])
|
|
||||||
|
|
||||||
c.addHeader(h)
|
|
||||||
|
|
||||||
if c.c.Labels == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return c.c.Labels[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) fullHeader() string {
|
|
||||||
if c.header == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join(c.header, "\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerContext) addHeader(header string) {
|
|
||||||
if c.header == nil {
|
|
||||||
c.header = []string{}
|
|
||||||
}
|
|
||||||
c.header = append(c.header, strings.ToUpper(header))
|
|
||||||
}
|
|
||||||
|
|
||||||
func customFormat(ctx Context, containers []types.Container) {
|
|
||||||
var (
|
|
||||||
table bool
|
|
||||||
header string
|
|
||||||
format = ctx.Format
|
|
||||||
buffer = bytes.NewBufferString("")
|
|
||||||
)
|
|
||||||
|
|
||||||
if strings.HasPrefix(ctx.Format, tableKey) {
|
|
||||||
table = true
|
|
||||||
format = format[len(tableKey):]
|
|
||||||
}
|
|
||||||
|
|
||||||
format = strings.Trim(format, " ")
|
|
||||||
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
|
|
||||||
format = r.Replace(format)
|
|
||||||
|
|
||||||
if table && ctx.Size {
|
|
||||||
format += "\t{{.Size}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("").Parse(format)
|
|
||||||
if err != nil {
|
|
||||||
buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
||||||
buffer.WriteTo(ctx.Output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, container := range containers {
|
|
||||||
containerCtx := &containerContext{
|
|
||||||
trunc: ctx.Trunc,
|
|
||||||
c: container,
|
|
||||||
}
|
|
||||||
if err := tmpl.Execute(buffer, containerCtx); err != nil {
|
|
||||||
buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
|
|
||||||
buffer.WriteTo(ctx.Output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if table && len(header) == 0 {
|
|
||||||
header = containerCtx.fullHeader()
|
|
||||||
}
|
|
||||||
buffer.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if table {
|
|
||||||
if len(header) == 0 {
|
|
||||||
// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
|
|
||||||
containerCtx := &containerContext{}
|
|
||||||
tmpl.Execute(bytes.NewBufferString(""), containerCtx)
|
|
||||||
header = containerCtx.fullHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
t := tabwriter.NewWriter(ctx.Output, 20, 1, 3, ' ', 0)
|
|
||||||
t.Write([]byte(header))
|
|
||||||
t.Write([]byte("\n"))
|
|
||||||
buffer.WriteTo(t)
|
|
||||||
t.Flush()
|
|
||||||
} else {
|
|
||||||
buffer.WriteTo(ctx.Output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripNamePrefix(ss []string) []string {
|
|
||||||
for i, s := range ss {
|
|
||||||
ss[i] = s[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package ps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContainerPsContext(t *testing.T) {
|
|
||||||
containerId := stringid.GenerateRandomID()
|
|
||||||
unix := time.Now().Unix()
|
|
||||||
|
|
||||||
var ctx containerContext
|
|
||||||
cases := []struct {
|
|
||||||
container types.Container
|
|
||||||
trunc bool
|
|
||||||
expValue string
|
|
||||||
expHeader string
|
|
||||||
call func() string
|
|
||||||
}{
|
|
||||||
{types.Container{ID: containerId}, true, stringid.TruncateID(containerId), idHeader, ctx.ID},
|
|
||||||
{types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names},
|
|
||||||
{types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image},
|
|
||||||
{types.Container{Image: ""}, true, "<no image>", imageHeader, ctx.Image},
|
|
||||||
{types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command},
|
|
||||||
{types.Container{Created: int(unix)}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt},
|
|
||||||
{types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports},
|
|
||||||
{types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status},
|
|
||||||
{types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size},
|
|
||||||
{types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size},
|
|
||||||
{types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cases {
|
|
||||||
ctx = containerContext{c: c.container, trunc: c.trunc}
|
|
||||||
v := c.call()
|
|
||||||
if strings.Contains(v, ",") {
|
|
||||||
// comma-separated values means probably a map input, which won't
|
|
||||||
// be guaranteed to have the same order as our expected value
|
|
||||||
// We'll create maps and use reflect.DeepEquals to check instead:
|
|
||||||
entriesMap := make(map[string]string)
|
|
||||||
expMap := make(map[string]string)
|
|
||||||
entries := strings.Split(v, ",")
|
|
||||||
expectedEntries := strings.Split(c.expValue, ",")
|
|
||||||
for _, entry := range entries {
|
|
||||||
keyval := strings.Split(entry, "=")
|
|
||||||
entriesMap[keyval[0]] = keyval[1]
|
|
||||||
}
|
|
||||||
for _, expected := range expectedEntries {
|
|
||||||
keyval := strings.Split(expected, "=")
|
|
||||||
expMap[keyval[0]] = keyval[1]
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(expMap, entriesMap) {
|
|
||||||
t.Fatalf("Expected entries: %v, got: %v", c.expValue, v)
|
|
||||||
}
|
|
||||||
} else if v != c.expValue {
|
|
||||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := ctx.fullHeader()
|
|
||||||
if h != c.expHeader {
|
|
||||||
t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}}
|
|
||||||
ctx = containerContext{c: c, trunc: true}
|
|
||||||
|
|
||||||
sid := ctx.Label("com.docker.swarm.swarm-id")
|
|
||||||
node := ctx.Label("com.docker.swarm.node_name")
|
|
||||||
if sid != "33" {
|
|
||||||
t.Fatalf("Expected 33, was %s\n", sid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if node != "ubuntu" {
|
|
||||||
t.Fatalf("Expected ubuntu, was %s\n", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := ctx.fullHeader()
|
|
||||||
if h != "SWARM ID\tNODE NAME" {
|
|
||||||
t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainerPsFormatError(t *testing.T) {
|
|
||||||
out := bytes.NewBufferString("")
|
|
||||||
ctx := Context{
|
|
||||||
Format: "{{InvalidFunction}}",
|
|
||||||
Output: out,
|
|
||||||
}
|
|
||||||
|
|
||||||
customFormat(ctx, make([]types.Container, 0))
|
|
||||||
if out.String() != "Template parsing error: template: :1: function \"InvalidFunction\" not defined\n" {
|
|
||||||
t.Fatalf("Expected format error, got `%v`\n", out.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package ps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tableFormatKey = "table"
|
|
||||||
rawFormatKey = "raw"
|
|
||||||
|
|
||||||
defaultTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
||||||
defaultQuietFormat = "{{.ID}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Output io.Writer
|
|
||||||
Format string
|
|
||||||
Size bool
|
|
||||||
Quiet bool
|
|
||||||
Trunc bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func Format(ctx Context, containers []types.Container) {
|
|
||||||
switch ctx.Format {
|
|
||||||
case tableFormatKey:
|
|
||||||
tableFormat(ctx, containers)
|
|
||||||
case rawFormatKey:
|
|
||||||
rawFormat(ctx, containers)
|
|
||||||
default:
|
|
||||||
customFormat(ctx, containers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func rawFormat(ctx Context, containers []types.Container) {
|
|
||||||
if ctx.Quiet {
|
|
||||||
ctx.Format = `container_id: {{.ID}}`
|
|
||||||
} else {
|
|
||||||
ctx.Format = `container_id: {{.ID}}
|
|
||||||
image: {{.Image}}
|
|
||||||
command: {{.Command}}
|
|
||||||
created_at: {{.CreatedAt}}
|
|
||||||
status: {{.Status}}
|
|
||||||
names: {{.Names}}
|
|
||||||
labels: {{.Labels}}
|
|
||||||
ports: {{.Ports}}
|
|
||||||
`
|
|
||||||
if ctx.Size {
|
|
||||||
ctx.Format += `size: {{.Size}}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customFormat(ctx, containers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableFormat(ctx Context, containers []types.Container) {
|
|
||||||
ctx.Format = defaultTableFormat
|
|
||||||
if ctx.Quiet {
|
|
||||||
ctx.Format = defaultQuietFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
customFormat(ctx, containers)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/graph/tags"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdPull pulls an image or a repository from the registry.
|
|
||||||
//
|
|
||||||
// Usage: docker pull [OPTIONS] IMAGENAME[:TAG|@DIGEST]
|
|
||||||
func (cli *DockerCli) CmdPull(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("pull", []string{"NAME[:TAG|@DIGEST]"}, "Pull an image or a repository from a registry", true)
|
|
||||||
allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository")
|
|
||||||
addTrustedFlags(cmd, true)
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
remote := cmd.Arg(0)
|
|
||||||
|
|
||||||
taglessRemote, tag := parsers.ParseRepositoryTag(remote)
|
|
||||||
if tag == "" && !*allTags {
|
|
||||||
tag = tags.DEFAULTTAG
|
|
||||||
fmt.Fprintf(cli.out, "Using default tag: %s\n", tag)
|
|
||||||
} else if tag != "" && *allTags {
|
|
||||||
return fmt.Errorf("tag can't be used with --all-tags/-a")
|
|
||||||
}
|
|
||||||
|
|
||||||
ref := registry.ParseReference(tag)
|
|
||||||
|
|
||||||
// Resolve the Repository name from fqn to RepositoryInfo
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTrusted() && !ref.HasDigest() {
|
|
||||||
// Check if tag is digest
|
|
||||||
authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
|
|
||||||
return cli.trustedPull(repoInfo, ref, authConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("fromImage", ref.ImageName(taglessRemote))
|
|
||||||
|
|
||||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdPush pushes an image or repository to the registry.
|
|
||||||
//
|
|
||||||
// Usage: docker push NAME[:TAG]
|
|
||||||
func (cli *DockerCli) CmdPush(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("push", []string{"NAME[:TAG]"}, "Push an image or a repository to a registry", true)
|
|
||||||
addTrustedFlags(cmd, false)
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
remote, tag := parsers.ParseRepositoryTag(cmd.Arg(0))
|
|
||||||
|
|
||||||
// Resolve the Repository name from fqn to RepositoryInfo
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(remote)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Resolve the Auth config relevant for this server
|
|
||||||
authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
|
|
||||||
// If we're not using a custom registry, we know the restrictions
|
|
||||||
// applied to repository names and can warn the user in advance.
|
|
||||||
// Custom repositories can have different rules, and we must also
|
|
||||||
// allow pushing by image ID.
|
|
||||||
if repoInfo.Official {
|
|
||||||
username := authConfig.Username
|
|
||||||
if username == "" {
|
|
||||||
username = "<user>"
|
|
||||||
}
|
|
||||||
return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to <user>/<repo> (ex: %s/%s)", username, repoInfo.LocalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTrusted() {
|
|
||||||
return cli.trustedPush(repoInfo, tag, authConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("tag", tag)
|
|
||||||
|
|
||||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdRename renames a container.
|
|
||||||
//
|
|
||||||
// Usage: docker rename OLD_NAME NEW_NAME
|
|
||||||
func (cli *DockerCli) CmdRename(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("rename", []string{"OLD_NAME NEW_NAME"}, "Rename a container", true)
|
|
||||||
cmd.Require(flag.Exact, 2)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
oldName := cmd.Arg(0)
|
|
||||||
newName := cmd.Arg(1)
|
|
||||||
|
|
||||||
if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/rename?name=%s", oldName, newName), nil, nil)); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
return fmt.Errorf("Error: failed to rename container named %s", oldName)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdRestart restarts one or more running containers.
|
|
||||||
//
|
|
||||||
// Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdRestart(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("restart", []string{"CONTAINER [CONTAINER...]"}, "Restart a running container", true)
|
|
||||||
nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing the container")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("t", strconv.Itoa(*nSeconds))
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
_, _, err := readBody(cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil, nil))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to restart containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdRm removes one or more containers.
|
|
||||||
//
|
|
||||||
// Usage: docker rm [OPTIONS] CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdRm(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("rm", []string{"CONTAINER [CONTAINER...]"}, "Remove one or more containers", true)
|
|
||||||
v := cmd.Bool([]string{"v", "-volumes"}, false, "Remove the volumes associated with the container")
|
|
||||||
link := cmd.Bool([]string{"l", "#link", "-link"}, false, "Remove the specified link")
|
|
||||||
force := cmd.Bool([]string{"f", "-force"}, false, "Force the removal of a running container (uses SIGKILL)")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
val := url.Values{}
|
|
||||||
if *v {
|
|
||||||
val.Set("v", "1")
|
|
||||||
}
|
|
||||||
if *link {
|
|
||||||
val.Set("link", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *force {
|
|
||||||
val.Set("force", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("Container name cannot be empty")
|
|
||||||
}
|
|
||||||
name = strings.Trim(name, "/")
|
|
||||||
|
|
||||||
_, _, err := readBody(cli.call("DELETE", "/containers/"+name+"?"+val.Encode(), nil, nil))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to remove containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdRmi removes all images with the specified name(s).
|
|
||||||
//
|
|
||||||
// Usage: docker rmi [OPTIONS] IMAGE [IMAGE...]
|
|
||||||
func (cli *DockerCli) CmdRmi(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("rmi", []string{"IMAGE [IMAGE...]"}, "Remove one or more images", true)
|
|
||||||
force := cmd.Bool([]string{"f", "-force"}, false, "Force removal of the image")
|
|
||||||
noprune := cmd.Bool([]string{"-no-prune"}, false, "Do not delete untagged parents")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
if *force {
|
|
||||||
v.Set("force", "1")
|
|
||||||
}
|
|
||||||
if *noprune {
|
|
||||||
v.Set("noprune", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
serverResp, err := cli.call("DELETE", "/images/"+name+"?"+v.Encode(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
dels := []types.ImageDelete{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&dels); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, del := range dels {
|
|
||||||
if del.Deleted != "" {
|
|
||||||
fmt.Fprintf(cli.out, "Deleted: %s\n", del.Deleted)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "Untagged: %s\n", del.Untagged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to remove images: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
"github.com/docker/docker/pkg/promise"
|
|
||||||
"github.com/docker/docker/pkg/signal"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
"github.com/docker/libnetwork/resolvconf/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cid *cidFile) Close() error {
|
|
||||||
cid.file.Close()
|
|
||||||
|
|
||||||
if !cid.written {
|
|
||||||
if err := os.Remove(cid.path); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cid *cidFile) Write(id string) error {
|
|
||||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
|
||||||
return fmt.Errorf("Failed to write the container ID to the file: %s", err)
|
|
||||||
}
|
|
||||||
cid.written = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdRun runs a command in a new container.
|
|
||||||
//
|
|
||||||
// Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
|
|
||||||
func (cli *DockerCli) CmdRun(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("run", []string{"IMAGE [COMMAND] [ARG...]"}, "Run a command in a new container", true)
|
|
||||||
addTrustedFlags(cmd, true)
|
|
||||||
|
|
||||||
// These are flags not stored in Config/HostConfig
|
|
||||||
var (
|
|
||||||
flAutoRemove = cmd.Bool([]string{"-rm"}, false, "Automatically remove the container when it exits")
|
|
||||||
flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Run container in background and print container ID")
|
|
||||||
flSigProxy = cmd.Bool([]string{"-sig-proxy"}, true, "Proxy received signals to the process")
|
|
||||||
flName = cmd.String([]string{"-name"}, "", "Assign a name to the container")
|
|
||||||
flAttach *opts.ListOpts
|
|
||||||
|
|
||||||
ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d")
|
|
||||||
ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm")
|
|
||||||
ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d")
|
|
||||||
)
|
|
||||||
|
|
||||||
config, hostConfig, cmd, err := runconfig.Parse(cmd, args)
|
|
||||||
// just in case the Parse does not exit
|
|
||||||
if err != nil {
|
|
||||||
cmd.ReportError(err.Error(), true)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hostConfig.Dns) > 0 {
|
|
||||||
// check the DNS settings passed via --dns against
|
|
||||||
// localhost regexp to warn if they are trying to
|
|
||||||
// set a DNS to a localhost address
|
|
||||||
for _, dnsIP := range hostConfig.Dns {
|
|
||||||
if dns.IsLocalhost(dnsIP) {
|
|
||||||
fmt.Fprintf(cli.err, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.Image == "" {
|
|
||||||
cmd.Usage()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*flDetach {
|
|
||||||
if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fl := cmd.Lookup("-attach"); fl != nil {
|
|
||||||
flAttach = fl.Value.(*opts.ListOpts)
|
|
||||||
if flAttach.Len() != 0 {
|
|
||||||
return ErrConflictAttachDetach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if *flAutoRemove {
|
|
||||||
return ErrConflictDetachAutoRemove
|
|
||||||
}
|
|
||||||
|
|
||||||
config.AttachStdin = false
|
|
||||||
config.AttachStdout = false
|
|
||||||
config.AttachStderr = false
|
|
||||||
config.StdinOnce = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable flSigProxy when in TTY mode
|
|
||||||
sigProxy := *flSigProxy
|
|
||||||
if config.Tty {
|
|
||||||
sigProxy = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telling the Windows daemon the initial size of the tty during start makes
|
|
||||||
// a far better user experience rather than relying on subsequent resizes
|
|
||||||
// to cause things to catch up.
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = cli.getTtySize()
|
|
||||||
}
|
|
||||||
|
|
||||||
createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if sigProxy {
|
|
||||||
sigc := cli.forwardAllSignals(createResponse.ID)
|
|
||||||
defer signal.StopCatch(sigc)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
waitDisplayID chan struct{}
|
|
||||||
errCh chan error
|
|
||||||
)
|
|
||||||
if !config.AttachStdout && !config.AttachStderr {
|
|
||||||
// Make this asynchronous to allow the client to write to stdin before having to read the ID
|
|
||||||
waitDisplayID = make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(waitDisplayID)
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", createResponse.ID)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
if *flAutoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) {
|
|
||||||
return ErrConflictRestartPolicyAndAutoRemove
|
|
||||||
}
|
|
||||||
// We need to instantiate the chan because the select needs it. It can
|
|
||||||
// be closed but can't be uninitialized.
|
|
||||||
hijacked := make(chan io.Closer)
|
|
||||||
// Block the return until the chan gets closed
|
|
||||||
defer func() {
|
|
||||||
logrus.Debugf("End of CmdRun(), Waiting for hijack to finish.")
|
|
||||||
if _, ok := <-hijacked; ok {
|
|
||||||
fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if config.AttachStdin || config.AttachStdout || config.AttachStderr {
|
|
||||||
var (
|
|
||||||
out, stderr io.Writer
|
|
||||||
in io.ReadCloser
|
|
||||||
v = url.Values{}
|
|
||||||
)
|
|
||||||
v.Set("stream", "1")
|
|
||||||
if config.AttachStdin {
|
|
||||||
v.Set("stdin", "1")
|
|
||||||
in = cli.in
|
|
||||||
}
|
|
||||||
if config.AttachStdout {
|
|
||||||
v.Set("stdout", "1")
|
|
||||||
out = cli.out
|
|
||||||
}
|
|
||||||
if config.AttachStderr {
|
|
||||||
v.Set("stderr", "1")
|
|
||||||
if config.Tty {
|
|
||||||
stderr = cli.out
|
|
||||||
} else {
|
|
||||||
stderr = cli.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errCh = promise.Go(func() error {
|
|
||||||
return cli.hijack("POST", "/containers/"+createResponse.ID+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked, nil)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
close(hijacked)
|
|
||||||
}
|
|
||||||
// Acknowledge the hijack before starting
|
|
||||||
select {
|
|
||||||
case closer := <-hijacked:
|
|
||||||
// Make sure that the hijack gets closed when returning (results
|
|
||||||
// in closing the hijack chan and freeing server's goroutines)
|
|
||||||
if closer != nil {
|
|
||||||
defer closer.Close()
|
|
||||||
}
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error hijack: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if *flAutoRemove {
|
|
||||||
if _, _, err = readBody(cli.call("DELETE", "/containers/"+createResponse.ID+"?v=1", nil, nil)); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "Error deleting container: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
//start the container
|
|
||||||
if _, _, err = readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut {
|
|
||||||
if err := cli.monitorTtySize(createResponse.ID, false); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errCh != nil {
|
|
||||||
if err := <-errCh; err != nil {
|
|
||||||
logrus.Debugf("Error hijack: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detached mode: wait for the id to be displayed and return.
|
|
||||||
if !config.AttachStdout && !config.AttachStderr {
|
|
||||||
// Detached mode
|
|
||||||
<-waitDisplayID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var status int
|
|
||||||
|
|
||||||
// Attached mode
|
|
||||||
if *flAutoRemove {
|
|
||||||
// Autoremove: wait for the container to finish, retrieve
|
|
||||||
// the exit code and remove the container
|
|
||||||
if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/wait", nil, nil)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No Autoremove: Simply retrieve the exit code
|
|
||||||
if !config.Tty {
|
|
||||||
// In non-TTY mode, we can't detach, so we must wait for container exit
|
|
||||||
if status, err = waitForExit(cli, createResponse.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// In TTY mode, there is a race: if the process dies too slowly, the state could
|
|
||||||
// be updated after the getExitCode call and result in the wrong exit code being reported
|
|
||||||
if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if status != 0 {
|
|
||||||
return Cli.StatusError{StatusCode: status}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdSave saves one or more images to a tar archive.
|
|
||||||
//
|
|
||||||
// The tar archive is written to STDOUT by default, or written to a file.
|
|
||||||
//
|
|
||||||
// Usage: docker save [OPTIONS] IMAGE [IMAGE...]
|
|
||||||
func (cli *DockerCli) CmdSave(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("save", []string{"IMAGE [IMAGE...]"}, "Save an image(s) to a tar archive (streamed to STDOUT by default)", true)
|
|
||||||
outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
output io.Writer = cli.out
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if *outfile != "" {
|
|
||||||
output, err = os.Create(*outfile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if cli.isTerminalOut {
|
|
||||||
return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
|
|
||||||
}
|
|
||||||
|
|
||||||
sopts := &streamOpts{
|
|
||||||
rawTerminal: true,
|
|
||||||
out: output,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmd.Args()) == 1 {
|
|
||||||
image := cmd.Arg(0)
|
|
||||||
if _, err := cli.stream("GET", "/images/"+image+"/get", sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
v := url.Values{}
|
|
||||||
for _, arg := range cmd.Args() {
|
|
||||||
v.Add("names", arg)
|
|
||||||
}
|
|
||||||
if _, err := cli.stream("GET", "/images/get?"+v.Encode(), sopts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/stringutils"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ByStars sorts search results in ascending order by number of stars.
|
|
||||||
type ByStars []registry.SearchResult
|
|
||||||
|
|
||||||
func (r ByStars) Len() int { return len(r) }
|
|
||||||
func (r ByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
||||||
func (r ByStars) Less(i, j int) bool { return r[i].StarCount < r[j].StarCount }
|
|
||||||
|
|
||||||
// CmdSearch searches the Docker Hub for images.
|
|
||||||
//
|
|
||||||
// Usage: docker search [OPTIONS] TERM
|
|
||||||
func (cli *DockerCli) CmdSearch(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("search", []string{"TERM"}, "Search the Docker Hub for images", true)
|
|
||||||
noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
|
|
||||||
trusted := cmd.Bool([]string{"#t", "#trusted", "#-trusted"}, false, "Only show trusted builds")
|
|
||||||
automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds")
|
|
||||||
stars := cmd.Uint([]string{"s", "#stars", "-stars"}, 0, "Only displays with at least x stars")
|
|
||||||
cmd.Require(flag.Exact, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
name := cmd.Arg(0)
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("term", name)
|
|
||||||
|
|
||||||
// Resolve the Repository name from fqn to hostname + name
|
|
||||||
taglessRemote, _ := parsers.ParseRepositoryTag(name)
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rdr, _, err := cli.clientRequestAttemptLogin("GET", "/images/search?"+v.Encode(), nil, nil, repoInfo.Index, "search")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer rdr.Close()
|
|
||||||
|
|
||||||
results := ByStars{}
|
|
||||||
if err := json.NewDecoder(rdr).Decode(&results); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(sort.Reverse(results))
|
|
||||||
|
|
||||||
w := tabwriter.NewWriter(cli.out, 10, 1, 3, ' ', 0)
|
|
||||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n")
|
|
||||||
for _, res := range results {
|
|
||||||
if ((*automated || *trusted) && (!res.IsTrusted && !res.IsAutomated)) || (int(*stars) > res.StarCount) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desc := strings.Replace(res.Description, "\n", " ", -1)
|
|
||||||
desc = strings.Replace(desc, "\r", " ", -1)
|
|
||||||
if !*noTrunc && len(desc) > 45 {
|
|
||||||
desc = stringutils.Truncate(desc, 42) + "..."
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount)
|
|
||||||
if res.IsOfficial {
|
|
||||||
fmt.Fprint(w, "[OK]")
|
|
||||||
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\t")
|
|
||||||
if res.IsAutomated || res.IsTrusted {
|
|
||||||
fmt.Fprint(w, "[OK]")
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, "\n")
|
|
||||||
}
|
|
||||||
w.Flush()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// +build experimental
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
nwclient "github.com/docker/libnetwork/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *DockerCli) CmdService(args ...string) error {
|
|
||||||
nCli := nwclient.NewNetworkCli(cli.out, cli.err, nwclient.CallFunc(cli.callWrapper))
|
|
||||||
args = append([]string{"service"}, args...)
|
|
||||||
return nCli.Cmd(os.Args[0], args...)
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/promise"
|
|
||||||
"github.com/docker/docker/pkg/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cli *DockerCli) forwardAllSignals(cid string) chan os.Signal {
|
|
||||||
sigc := make(chan os.Signal, 128)
|
|
||||||
signal.CatchAll(sigc)
|
|
||||||
go func() {
|
|
||||||
for s := range sigc {
|
|
||||||
if s == signal.SIGCHLD {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var sig string
|
|
||||||
for sigStr, sigN := range signal.SignalMap {
|
|
||||||
if sigN == s {
|
|
||||||
sig = sigStr
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sig == "" {
|
|
||||||
fmt.Fprintf(cli.err, "Unsupported signal: %v. Discarding.\n", s)
|
|
||||||
}
|
|
||||||
if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/kill?signal=%s", cid, sig), nil, nil)); err != nil {
|
|
||||||
logrus.Debugf("Error sending signal: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return sigc
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdStart starts one or more stopped containers.
|
|
||||||
//
|
|
||||||
// Usage: docker start [OPTIONS] CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdStart(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("start", []string{"CONTAINER [CONTAINER...]"}, "Start one or more stopped containers", true)
|
|
||||||
attach := cmd.Bool([]string{"a", "-attach"}, false, "Attach STDOUT/STDERR and forward signals")
|
|
||||||
openStdin := cmd.Bool([]string{"i", "-interactive"}, false, "Attach container's STDIN")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cErr chan error
|
|
||||||
tty bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if *attach || *openStdin {
|
|
||||||
if cmd.NArg() > 1 {
|
|
||||||
return fmt.Errorf("You cannot start and attach multiple containers at once.")
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var c types.ContainerJSON
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tty = c.Config.Tty
|
|
||||||
|
|
||||||
if !tty {
|
|
||||||
sigc := cli.forwardAllSignals(cmd.Arg(0))
|
|
||||||
defer signal.StopCatch(sigc)
|
|
||||||
}
|
|
||||||
|
|
||||||
var in io.ReadCloser
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("stream", "1")
|
|
||||||
|
|
||||||
if *openStdin && c.Config.OpenStdin {
|
|
||||||
v.Set("stdin", "1")
|
|
||||||
in = cli.in
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("stdout", "1")
|
|
||||||
v.Set("stderr", "1")
|
|
||||||
|
|
||||||
hijacked := make(chan io.Closer)
|
|
||||||
// Block the return until the chan gets closed
|
|
||||||
defer func() {
|
|
||||||
logrus.Debugf("CmdStart() returned, defer waiting for hijack to finish.")
|
|
||||||
if _, ok := <-hijacked; ok {
|
|
||||||
fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
|
|
||||||
}
|
|
||||||
cli.in.Close()
|
|
||||||
}()
|
|
||||||
cErr = promise.Go(func() error {
|
|
||||||
return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, hijacked, nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Acknowledge the hijack before starting
|
|
||||||
select {
|
|
||||||
case closer := <-hijacked:
|
|
||||||
// Make sure that the hijack gets closed when returning (results
|
|
||||||
// in closing the hijack chan and freeing server's goroutines)
|
|
||||||
if closer != nil {
|
|
||||||
defer closer.Close()
|
|
||||||
}
|
|
||||||
case err := <-cErr:
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encounteredError error
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
_, _, err := readBody(cli.call("POST", "/containers/"+name+"/start", nil, nil))
|
|
||||||
if err != nil {
|
|
||||||
if !*attach && !*openStdin {
|
|
||||||
// attach and openStdin is false means it could be starting multiple containers
|
|
||||||
// when a container start failed, show the error message and start next
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
encounteredError = err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !*attach && !*openStdin {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
encounteredError = fmt.Errorf("Error: failed to start containers: %v", errNames)
|
|
||||||
}
|
|
||||||
if encounteredError != nil {
|
|
||||||
return encounteredError
|
|
||||||
}
|
|
||||||
|
|
||||||
if *openStdin || *attach {
|
|
||||||
if tty && cli.isTerminalOut {
|
|
||||||
if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if attchErr := <-cErr; attchErr != nil {
|
|
||||||
return attchErr
|
|
||||||
}
|
|
||||||
_, status, err := getExitCode(cli, cmd.Arg(0))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if status != 0 {
|
|
||||||
return Cli.StatusError{StatusCode: status}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"text/tabwriter"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/units"
|
|
||||||
)
|
|
||||||
|
|
||||||
type containerStats struct {
|
|
||||||
Name string
|
|
||||||
CPUPercentage float64
|
|
||||||
Memory float64
|
|
||||||
MemoryLimit float64
|
|
||||||
MemoryPercentage float64
|
|
||||||
NetworkRx float64
|
|
||||||
NetworkTx float64
|
|
||||||
mu sync.RWMutex
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *containerStats) Collect(cli *DockerCli, streamStats bool) {
|
|
||||||
v := url.Values{}
|
|
||||||
if streamStats {
|
|
||||||
v.Set("stream", "1")
|
|
||||||
} else {
|
|
||||||
v.Set("stream", "0")
|
|
||||||
}
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+s.Name+"/stats?"+v.Encode(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.err = err
|
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var (
|
|
||||||
previousCPU uint64
|
|
||||||
previousSystem uint64
|
|
||||||
dec = json.NewDecoder(serverResp.body)
|
|
||||||
u = make(chan error, 1)
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
var v *types.Stats
|
|
||||||
if err := dec.Decode(&v); err != nil {
|
|
||||||
u <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0
|
|
||||||
cpuPercent = 0.0
|
|
||||||
)
|
|
||||||
previousCPU = v.PreCpuStats.CpuUsage.TotalUsage
|
|
||||||
previousSystem = v.PreCpuStats.SystemUsage
|
|
||||||
cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v)
|
|
||||||
s.mu.Lock()
|
|
||||||
s.CPUPercentage = cpuPercent
|
|
||||||
s.Memory = float64(v.MemoryStats.Usage)
|
|
||||||
s.MemoryLimit = float64(v.MemoryStats.Limit)
|
|
||||||
s.MemoryPercentage = memPercent
|
|
||||||
s.NetworkRx = float64(v.Network.RxBytes)
|
|
||||||
s.NetworkTx = float64(v.Network.TxBytes)
|
|
||||||
s.mu.Unlock()
|
|
||||||
u <- nil
|
|
||||||
if !streamStats {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
// zero out the values if we have not received an update within
|
|
||||||
// the specified duration.
|
|
||||||
s.mu.Lock()
|
|
||||||
s.CPUPercentage = 0
|
|
||||||
s.Memory = 0
|
|
||||||
s.MemoryPercentage = 0
|
|
||||||
s.mu.Unlock()
|
|
||||||
case err := <-u:
|
|
||||||
if err != nil {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.err = err
|
|
||||||
s.mu.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !streamStats {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *containerStats) Display(w io.Writer) error {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
if s.err != nil {
|
|
||||||
return s.err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s\t%.2f%%\t%s/%s\t%.2f%%\t%s/%s\n",
|
|
||||||
s.Name,
|
|
||||||
s.CPUPercentage,
|
|
||||||
units.HumanSize(s.Memory), units.HumanSize(s.MemoryLimit),
|
|
||||||
s.MemoryPercentage,
|
|
||||||
units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdStats displays a live stream of resource usage statistics for one or more containers.
|
|
||||||
//
|
|
||||||
// This shows real-time information on CPU usage, memory usage, and network I/O.
|
|
||||||
//
|
|
||||||
// Usage: docker stats CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdStats(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("stats", []string{"CONTAINER [CONTAINER...]"}, "Display a live stream of one or more containers' resource usage statistics", true)
|
|
||||||
noStream := cmd.Bool([]string{"-no-stream"}, false, "Disable streaming stats and only pull the first result")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
names := cmd.Args()
|
|
||||||
sort.Strings(names)
|
|
||||||
var (
|
|
||||||
cStats []*containerStats
|
|
||||||
w = tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
||||||
)
|
|
||||||
printHeader := func() {
|
|
||||||
if !*noStream {
|
|
||||||
fmt.Fprint(cli.out, "\033[2J")
|
|
||||||
fmt.Fprint(cli.out, "\033[H")
|
|
||||||
}
|
|
||||||
io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE/LIMIT\tMEM %\tNET I/O\n")
|
|
||||||
}
|
|
||||||
for _, n := range names {
|
|
||||||
s := &containerStats{Name: n}
|
|
||||||
cStats = append(cStats, s)
|
|
||||||
go s.Collect(cli, !*noStream)
|
|
||||||
}
|
|
||||||
// do a quick pause so that any failed connections for containers that do not exist are able to be
|
|
||||||
// evicted before we display the initial or default values.
|
|
||||||
time.Sleep(1500 * time.Millisecond)
|
|
||||||
var errs []string
|
|
||||||
for _, c := range cStats {
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.err != nil {
|
|
||||||
errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
|
|
||||||
}
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("%s", strings.Join(errs, ", "))
|
|
||||||
}
|
|
||||||
for range time.Tick(500 * time.Millisecond) {
|
|
||||||
printHeader()
|
|
||||||
toRemove := []int{}
|
|
||||||
for i, s := range cStats {
|
|
||||||
if err := s.Display(w); err != nil && !*noStream {
|
|
||||||
toRemove = append(toRemove, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for j := len(toRemove) - 1; j >= 0; j-- {
|
|
||||||
i := toRemove[j]
|
|
||||||
cStats = append(cStats[:i], cStats[i+1:]...)
|
|
||||||
}
|
|
||||||
if len(cStats) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w.Flush()
|
|
||||||
if *noStream {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.Stats) float64 {
|
|
||||||
var (
|
|
||||||
cpuPercent = 0.0
|
|
||||||
// calculate the change for the cpu usage of the container in between readings
|
|
||||||
cpuDelta = float64(v.CpuStats.CpuUsage.TotalUsage - previousCPU)
|
|
||||||
// calculate the change for the entire system between readings
|
|
||||||
systemDelta = float64(v.CpuStats.SystemUsage - previousSystem)
|
|
||||||
)
|
|
||||||
|
|
||||||
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
|
||||||
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CpuStats.CpuUsage.PercpuUsage)) * 100.0
|
|
||||||
}
|
|
||||||
return cpuPercent
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDisplay(t *testing.T) {
|
|
||||||
c := &containerStats{
|
|
||||||
Name: "app",
|
|
||||||
CPUPercentage: 30.0,
|
|
||||||
Memory: 100 * 1024 * 1024.0,
|
|
||||||
MemoryLimit: 2048 * 1024 * 1024.0,
|
|
||||||
MemoryPercentage: 100.0 / 2048.0 * 100.0,
|
|
||||||
NetworkRx: 100 * 1024 * 1024,
|
|
||||||
NetworkTx: 800 * 1024 * 1024,
|
|
||||||
mu: sync.RWMutex{},
|
|
||||||
}
|
|
||||||
var b bytes.Buffer
|
|
||||||
if err := c.Display(&b); err != nil {
|
|
||||||
t.Fatalf("c.Display() gave error: %s", err)
|
|
||||||
}
|
|
||||||
got := b.String()
|
|
||||||
want := "app\t30.00%\t104.9 MB/2.147 GB\t4.88%\t104.9 MB/838.9 MB\n"
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("c.Display() = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdStop stops one or more running containers.
|
|
||||||
//
|
|
||||||
// A running container is stopped by first sending SIGTERM and then SIGKILL if the container fails to stop within a grace period (the default is 10 seconds).
|
|
||||||
//
|
|
||||||
// Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdStop(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("stop", []string{"CONTAINER [CONTAINER...]"}, "Stop a running container by sending SIGTERM and then SIGKILL after a\ngrace period", true)
|
|
||||||
nSeconds := cmd.Int([]string{"t", "-time"}, 10, "Seconds to wait for stop before killing it")
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("t", strconv.Itoa(*nSeconds))
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
_, _, err := readBody(cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil, nil))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to stop containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdTag tags an image into a repository.
|
|
||||||
//
|
|
||||||
// Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
|
|
||||||
func (cli *DockerCli) CmdTag(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("tag", []string{"IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]"}, "Tag an image into a repository", true)
|
|
||||||
force := cmd.Bool([]string{"f", "#force", "-force"}, false, "Force")
|
|
||||||
cmd.Require(flag.Exact, 2)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var (
|
|
||||||
repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1))
|
|
||||||
v = url.Values{}
|
|
||||||
)
|
|
||||||
|
|
||||||
//Check if the given image name can be resolved
|
|
||||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Set("repo", repository)
|
|
||||||
v.Set("tag", tag)
|
|
||||||
|
|
||||||
if *force {
|
|
||||||
v.Set("force", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, _, err := readBody(cli.call("POST", "/images/"+cmd.Arg(0)+"/tag?"+v.Encode(), nil, nil)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdTop displays the running processes of a container.
|
|
||||||
//
|
|
||||||
// Usage: docker top CONTAINER
|
|
||||||
func (cli *DockerCli) CmdTop(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("top", []string{"CONTAINER [ps OPTIONS]"}, "Display the running processes of a container", true)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
val := url.Values{}
|
|
||||||
if cmd.NArg() > 1 {
|
|
||||||
val.Set("ps_args", strings.Join(cmd.Args()[1:], " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top?"+val.Encode(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
procList := types.ContainerProcessList{}
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&procList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
||||||
fmt.Fprintln(w, strings.Join(procList.Titles, "\t"))
|
|
||||||
|
|
||||||
for _, proc := range procList.Processes {
|
|
||||||
fmt.Fprintln(w, strings.Join(proc, "\t"))
|
|
||||||
}
|
|
||||||
w.Flush()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/digest"
|
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/pkg/ansiescape"
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/tlsconfig"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/notary/client"
|
|
||||||
"github.com/docker/notary/pkg/passphrase"
|
|
||||||
"github.com/docker/notary/trustmanager"
|
|
||||||
"github.com/endophage/gotuf/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
var untrusted bool
|
|
||||||
|
|
||||||
func addTrustedFlags(fs *flag.FlagSet, verify bool) {
|
|
||||||
var trusted bool
|
|
||||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
|
||||||
if t, err := strconv.ParseBool(e); t || err != nil {
|
|
||||||
// treat any other value as true
|
|
||||||
trusted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message := "Skip image signing"
|
|
||||||
if verify {
|
|
||||||
message = "Skip image verification"
|
|
||||||
}
|
|
||||||
fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTrusted() bool {
|
|
||||||
return !untrusted
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetRegexp = regexp.MustCompile(`([\S]+): digest: ([\S]+) size: ([\d]+)`)
|
|
||||||
|
|
||||||
type target struct {
|
|
||||||
reference registry.Reference
|
|
||||||
digest digest.Digest
|
|
||||||
size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) trustDirectory() string {
|
|
||||||
return filepath.Join(cliconfig.ConfigDir(), "trust")
|
|
||||||
}
|
|
||||||
|
|
||||||
// certificateDirectory returns the directory containing
|
|
||||||
// TLS certificates for the given server. An error is
|
|
||||||
// returned if there was an error parsing the server string.
|
|
||||||
func (cli *DockerCli) certificateDirectory(server string) (string, error) {
|
|
||||||
u, err := url.Parse(server)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func trustServer(index *registry.IndexInfo) string {
|
|
||||||
if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
|
|
||||||
if !strings.HasPrefix(s, "https://") {
|
|
||||||
return "https://" + s
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
if index.Official {
|
|
||||||
return registry.NotaryServer
|
|
||||||
}
|
|
||||||
return "https://" + index.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
type simpleCredentialStore struct {
|
|
||||||
auth cliconfig.AuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
|
|
||||||
return scs.auth.Username, scs.auth.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig cliconfig.AuthConfig) (*client.NotaryRepository, error) {
|
|
||||||
server := trustServer(repoInfo.Index)
|
|
||||||
if !strings.HasPrefix(server, "https://") {
|
|
||||||
return nil, errors.New("unsupported scheme: https required for trust server")
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg = tlsconfig.ClientDefault
|
|
||||||
cfg.InsecureSkipVerify = !repoInfo.Index.Secure
|
|
||||||
|
|
||||||
// Get certificate base directory
|
|
||||||
certDir, err := cli.certificateDirectory(server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logrus.Debugf("reading certificate directory: %s", certDir)
|
|
||||||
|
|
||||||
if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
base := &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
DualStack: true,
|
|
||||||
}).Dial,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
TLSClientConfig: &cfg,
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip configuration headers since request is not going to Docker daemon
|
|
||||||
modifiers := registry.DockerHeaders(http.Header{})
|
|
||||||
authTransport := transport.NewTransport(base, modifiers...)
|
|
||||||
pingClient := &http.Client{
|
|
||||||
Transport: authTransport,
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
endpointStr := server + "/v2/"
|
|
||||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := pingClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
challengeManager := auth.NewSimpleChallengeManager()
|
|
||||||
if err := challengeManager.AddResponse(resp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
creds := simpleCredentialStore{auth: authConfig}
|
|
||||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.CanonicalName, "push", "pull")
|
|
||||||
basicHandler := auth.NewBasicHandler(creds)
|
|
||||||
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
|
|
||||||
tr := transport.NewTransport(base, modifiers...)
|
|
||||||
|
|
||||||
return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.CanonicalName, server, tr, cli.getPassphraseRetriever())
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertTarget(t client.Target) (target, error) {
|
|
||||||
h, ok := t.Hashes["sha256"]
|
|
||||||
if !ok {
|
|
||||||
return target{}, errors.New("no valid hash, expecting sha256")
|
|
||||||
}
|
|
||||||
return target{
|
|
||||||
reference: registry.ParseReference(t.Name),
|
|
||||||
digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
|
|
||||||
size: t.Length,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
|
|
||||||
aliasMap := map[string]string{
|
|
||||||
"root": "offline",
|
|
||||||
"snapshot": "tagging",
|
|
||||||
"targets": "tagging",
|
|
||||||
}
|
|
||||||
baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap)
|
|
||||||
env := map[string]string{
|
|
||||||
"root": os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"),
|
|
||||||
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"),
|
|
||||||
"targets": os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"),
|
|
||||||
}
|
|
||||||
return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
|
|
||||||
if v := env[alias]; v != "" {
|
|
||||||
return v, numAttempts > 1, nil
|
|
||||||
}
|
|
||||||
return baseRetriever(keyName, alias, createNew, numAttempts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (registry.Reference, error) {
|
|
||||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the Auth config relevant for this server
|
|
||||||
authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)
|
|
||||||
|
|
||||||
notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := notaryRepo.GetTargetByName(ref.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
r, err := convertTarget(*t)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return registry.DigestReference(r.digest), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) tagTrusted(repoInfo *registry.RepositoryInfo, trustedRef, ref registry.Reference) error {
|
|
||||||
fullName := trustedRef.ImageName(repoInfo.LocalName)
|
|
||||||
fmt.Fprintf(cli.out, "Tagging %s as %s\n", fullName, ref.ImageName(repoInfo.LocalName))
|
|
||||||
tv := url.Values{}
|
|
||||||
tv.Set("repo", repoInfo.LocalName)
|
|
||||||
tv.Set("tag", ref.String())
|
|
||||||
tv.Set("force", "1")
|
|
||||||
|
|
||||||
if _, _, err := readBody(cli.call("POST", "/images/"+fullName+"/tag?"+tv.Encode(), nil, nil)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func notaryError(err error) error {
|
|
||||||
switch err.(type) {
|
|
||||||
case *json.SyntaxError:
|
|
||||||
logrus.Debugf("Notary syntax error: %s", err)
|
|
||||||
return errors.New("no trust data available for remote repository")
|
|
||||||
case client.ErrExpired:
|
|
||||||
return fmt.Errorf("remote repository out-of-date: %v", err)
|
|
||||||
case trustmanager.ErrKeyNotFound:
|
|
||||||
return fmt.Errorf("signing keys not found: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig cliconfig.AuthConfig) error {
|
|
||||||
var (
|
|
||||||
v = url.Values{}
|
|
||||||
refs = []target{}
|
|
||||||
)
|
|
||||||
|
|
||||||
notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ref.String() == "" {
|
|
||||||
// List all targets
|
|
||||||
targets, err := notaryRepo.ListTargets()
|
|
||||||
if err != nil {
|
|
||||||
return notaryError(err)
|
|
||||||
}
|
|
||||||
for _, tgt := range targets {
|
|
||||||
t, err := convertTarget(*tgt)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.LocalName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
refs = append(refs, t)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t, err := notaryRepo.GetTargetByName(ref.String())
|
|
||||||
if err != nil {
|
|
||||||
return notaryError(err)
|
|
||||||
}
|
|
||||||
r, err := convertTarget(*t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
|
|
||||||
}
|
|
||||||
refs = append(refs, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("fromImage", repoInfo.LocalName)
|
|
||||||
for i, r := range refs {
|
|
||||||
displayTag := r.reference.String()
|
|
||||||
if displayTag != "" {
|
|
||||||
displayTag = ":" + displayTag
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.LocalName, displayTag, r.digest)
|
|
||||||
v.Set("tag", r.digest.String())
|
|
||||||
|
|
||||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If reference is not trusted, tag by trusted reference
|
|
||||||
if !r.reference.HasDigest() {
|
|
||||||
if err := cli.tagTrusted(repoInfo, registry.DigestReference(r.digest), r.reference); err != nil {
|
|
||||||
return err
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectKey(keys map[string]string) string {
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
keyIDs := []string{}
|
|
||||||
for k := range keys {
|
|
||||||
keyIDs = append(keyIDs, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(dmcgowan): let user choose if multiple keys, now pick consistently
|
|
||||||
sort.Strings(keyIDs)
|
|
||||||
|
|
||||||
return keyIDs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) {
|
|
||||||
r, w := io.Pipe()
|
|
||||||
out := io.MultiWriter(in, w)
|
|
||||||
targetChan := make(chan []target)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
targets := []target{}
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
scanner.Split(ansiescape.ScanANSILines)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Bytes()
|
|
||||||
if matches := targetRegexp.FindSubmatch(line); len(matches) == 4 {
|
|
||||||
dgst, err := digest.ParseDigest(string(matches[2]))
|
|
||||||
if err != nil {
|
|
||||||
// Line does match what is expected, continue looking for valid lines
|
|
||||||
logrus.Debugf("Bad digest value %q in matched line, ignoring\n", string(matches[2]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s, err := strconv.ParseInt(string(matches[3]), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
// Line does match what is expected, continue looking for valid lines
|
|
||||||
logrus.Debugf("Bad size value %q in matched line, ignoring\n", string(matches[3]))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
targets = append(targets, target{
|
|
||||||
reference: registry.ParseReference(string(matches[1])),
|
|
||||||
digest: dgst,
|
|
||||||
size: s,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetChan <- targets
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig cliconfig.AuthConfig) error {
|
|
||||||
streamOut, targetChan := targetStream(cli.out)
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("tag", tag)
|
|
||||||
|
|
||||||
_, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push")
|
|
||||||
// Close stream channel to finish target parsing
|
|
||||||
if err := streamOut.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Check error from request
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get target results
|
|
||||||
targets := <-targetChan
|
|
||||||
|
|
||||||
if tag == "" {
|
|
||||||
fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(targets) == 0 {
|
|
||||||
fmt.Fprintf(cli.out, "No targets found, skipping trust metadata push\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(cli.out, "Signing and pushing trust metadata\n")
|
|
||||||
|
|
||||||
repo, err := cli.getNotaryRepository(repoInfo, authConfig)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, target := range targets {
|
|
||||||
h, err := hex.DecodeString(target.digest.Hex())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t := &client.Target{
|
|
||||||
Name: target.reference.String(),
|
|
||||||
Hashes: data.Hashes{
|
|
||||||
string(target.digest.Algorithm()): h,
|
|
||||||
},
|
|
||||||
Length: int64(target.size),
|
|
||||||
}
|
|
||||||
if err := repo.AddTarget(t); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = repo.Publish()
|
|
||||||
if _, ok := err.(*client.ErrRepoNotInitialized); !ok {
|
|
||||||
return notaryError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ks := repo.KeyStoreManager
|
|
||||||
keys := ks.RootKeyStore().ListKeys()
|
|
||||||
|
|
||||||
rootKey := selectKey(keys)
|
|
||||||
if rootKey == "" {
|
|
||||||
rootKey, err = ks.GenRootKey("ecdsa")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoService, err := ks.GetRootCryptoService(rootKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := repo.Initialize(cryptoService); err != nil {
|
|
||||||
return notaryError(err)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.CanonicalName)
|
|
||||||
|
|
||||||
return notaryError(repo.Publish())
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdUnpause unpauses all processes within a container, for one or more containers.
|
|
||||||
//
|
|
||||||
// Usage: docker unpause CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdUnpause(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("unpause", []string{"CONTAINER [CONTAINER...]"}, "Unpause all processes within a container", true)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
if _, _, err := readBody(cli.call("POST", fmt.Sprintf("/containers/%s/unpause", name), nil, nil)); err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to unpause containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
gosignal "os/signal"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/autogen/dockerversion"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/docker/pkg/signal"
|
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
|
||||||
"github.com/docker/docker/pkg/term"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errConnectionRefused = errors.New("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
|
|
||||||
)
|
|
||||||
|
|
||||||
type serverResponse struct {
|
|
||||||
body io.ReadCloser
|
|
||||||
header http.Header
|
|
||||||
statusCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPClient creates a new HTTP client with the cli's client transport instance.
|
|
||||||
func (cli *DockerCli) HTTPClient() *http.Client {
|
|
||||||
return &http.Client{Transport: cli.transport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) encodeData(data interface{}) (*bytes.Buffer, error) {
|
|
||||||
params := bytes.NewBuffer(nil)
|
|
||||||
if data != nil {
|
|
||||||
if err := json.NewEncoder(params).Encode(data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return params, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers map[string][]string) (*serverResponse, error) {
|
|
||||||
|
|
||||||
serverResp := &serverResponse{
|
|
||||||
body: nil,
|
|
||||||
statusCode: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedPayload := (method == "POST" || method == "PUT")
|
|
||||||
if expectedPayload && in == nil {
|
|
||||||
in = bytes.NewReader([]byte{})
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(method, fmt.Sprintf("%s/v%s%s", cli.basePath, api.Version, path), in)
|
|
||||||
if err != nil {
|
|
||||||
return serverResp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
|
|
||||||
// then the user can't change OUR headers
|
|
||||||
for k, v := range cli.configFile.HTTPHeaders {
|
|
||||||
req.Header.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")")
|
|
||||||
req.URL.Host = cli.addr
|
|
||||||
req.URL.Scheme = cli.scheme
|
|
||||||
|
|
||||||
if headers != nil {
|
|
||||||
for k, v := range headers {
|
|
||||||
req.Header[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedPayload && req.Header.Get("Content-Type") == "" {
|
|
||||||
req.Header.Set("Content-Type", "text/plain")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := cli.HTTPClient().Do(req)
|
|
||||||
if resp != nil {
|
|
||||||
serverResp.statusCode = resp.StatusCode
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "connection refused") {
|
|
||||||
return serverResp, errConnectionRefused
|
|
||||||
}
|
|
||||||
|
|
||||||
if cli.tlsConfig == nil {
|
|
||||||
return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?\n* Is your docker daemon up and running?", err)
|
|
||||||
}
|
|
||||||
if cli.tlsConfig != nil && strings.Contains(err.Error(), "remote error: bad certificate") {
|
|
||||||
return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return serverResp, err
|
|
||||||
}
|
|
||||||
if len(body) == 0 {
|
|
||||||
return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
|
|
||||||
}
|
|
||||||
return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp.body = resp.Body
|
|
||||||
serverResp.header = resp.Header
|
|
||||||
return serverResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) clientRequestAttemptLogin(method, path string, in io.Reader, out io.Writer, index *registry.IndexInfo, cmdName string) (io.ReadCloser, int, error) {
|
|
||||||
cmdAttempt := func(authConfig cliconfig.AuthConfig) (io.ReadCloser, int, error) {
|
|
||||||
buf, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, err
|
|
||||||
}
|
|
||||||
registryAuthHeader := []string{
|
|
||||||
base64.URLEncoding.EncodeToString(buf),
|
|
||||||
}
|
|
||||||
|
|
||||||
// begin the request
|
|
||||||
serverResp, err := cli.clientRequest(method, path, in, map[string][]string{
|
|
||||||
"X-Registry-Auth": registryAuthHeader,
|
|
||||||
})
|
|
||||||
if err == nil && out != nil {
|
|
||||||
// If we are streaming output, complete the stream since
|
|
||||||
// errors may not appear until later.
|
|
||||||
err = cli.streamBody(serverResp.body, serverResp.header.Get("Content-Type"), true, out, nil)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// Since errors in a stream appear after status 200 has been written,
|
|
||||||
// we may need to change the status code.
|
|
||||||
if strings.Contains(err.Error(), "Authentication is required") ||
|
|
||||||
strings.Contains(err.Error(), "Status 401") ||
|
|
||||||
strings.Contains(err.Error(), "401 Unauthorized") ||
|
|
||||||
strings.Contains(err.Error(), "status code 401") {
|
|
||||||
serverResp.statusCode = http.StatusUnauthorized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return serverResp.body, serverResp.statusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the Auth config relevant for this server
|
|
||||||
authConfig := registry.ResolveAuthConfig(cli.configFile, index)
|
|
||||||
body, statusCode, err := cmdAttempt(authConfig)
|
|
||||||
if statusCode == http.StatusUnauthorized {
|
|
||||||
fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName)
|
|
||||||
if err = cli.CmdLogin(index.GetAuthConfigKey()); err != nil {
|
|
||||||
return nil, -1, err
|
|
||||||
}
|
|
||||||
authConfig = registry.ResolveAuthConfig(cli.configFile, index)
|
|
||||||
return cmdAttempt(authConfig)
|
|
||||||
}
|
|
||||||
return body, statusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) callWrapper(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
|
|
||||||
sr, err := cli.call(method, path, data, headers)
|
|
||||||
return sr.body, sr.header, sr.statusCode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) call(method, path string, data interface{}, headers map[string][]string) (*serverResponse, error) {
|
|
||||||
params, err := cli.encodeData(data)
|
|
||||||
if err != nil {
|
|
||||||
sr := &serverResponse{
|
|
||||||
body: nil,
|
|
||||||
header: nil,
|
|
||||||
statusCode: -1,
|
|
||||||
}
|
|
||||||
return sr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
if headers == nil {
|
|
||||||
headers = make(map[string][]string)
|
|
||||||
}
|
|
||||||
headers["Content-Type"] = []string{"application/json"}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverResp, err := cli.clientRequest(method, path, params, headers)
|
|
||||||
return serverResp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamOpts struct {
|
|
||||||
rawTerminal bool
|
|
||||||
in io.Reader
|
|
||||||
out io.Writer
|
|
||||||
err io.Writer
|
|
||||||
headers map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) stream(method, path string, opts *streamOpts) (*serverResponse, error) {
|
|
||||||
serverResp, err := cli.clientRequest(method, path, opts.in, opts.headers)
|
|
||||||
if err != nil {
|
|
||||||
return serverResp, err
|
|
||||||
}
|
|
||||||
return serverResp, cli.streamBody(serverResp.body, serverResp.header.Get("Content-Type"), opts.rawTerminal, opts.out, opts.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) streamBody(body io.ReadCloser, contentType string, rawTerminal bool, stdout, stderr io.Writer) error {
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
if api.MatchesContentType(contentType, "application/json") {
|
|
||||||
return jsonmessage.DisplayJSONMessagesStream(body, stdout, cli.outFd, cli.isTerminalOut)
|
|
||||||
}
|
|
||||||
if stdout != nil || stderr != nil {
|
|
||||||
// When TTY is ON, use regular copy
|
|
||||||
var err error
|
|
||||||
if rawTerminal {
|
|
||||||
_, err = io.Copy(stdout, body)
|
|
||||||
} else {
|
|
||||||
_, err = stdcopy.StdCopy(stdout, stderr, body)
|
|
||||||
}
|
|
||||||
logrus.Debugf("[stream] End of stdout")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) resizeTty(id string, isExec bool) {
|
|
||||||
height, width := cli.getTtySize()
|
|
||||||
if height == 0 && width == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("h", strconv.Itoa(height))
|
|
||||||
v.Set("w", strconv.Itoa(width))
|
|
||||||
|
|
||||||
path := ""
|
|
||||||
if !isExec {
|
|
||||||
path = "/containers/" + id + "/resize?"
|
|
||||||
} else {
|
|
||||||
path = "/exec/" + id + "/resize?"
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, nil)); err != nil {
|
|
||||||
logrus.Debugf("Error resize: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForExit(cli *DockerCli, containerID string) (int, error) {
|
|
||||||
serverResp, err := cli.call("POST", "/containers/"+containerID+"/wait", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var res types.ContainerWaitResponse
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&res); err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.StatusCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getExitCode perform an inspect on the container. It returns
|
|
||||||
// the running state and the exit code.
|
|
||||||
func getExitCode(cli *DockerCli, containerID string) (bool, int, error) {
|
|
||||||
serverResp, err := cli.call("GET", "/containers/"+containerID+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
// If we can't connect, then the daemon probably died.
|
|
||||||
if err != errConnectionRefused {
|
|
||||||
return false, -1, err
|
|
||||||
}
|
|
||||||
return false, -1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
var c types.ContainerJSON
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return false, -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.State.Running, c.State.ExitCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getExecExitCode perform an inspect on the exec command. It returns
|
|
||||||
// the running state and the exit code.
|
|
||||||
func getExecExitCode(cli *DockerCli, execID string) (bool, int, error) {
|
|
||||||
serverResp, err := cli.call("GET", "/exec/"+execID+"/json", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
// If we can't connect, then the daemon probably died.
|
|
||||||
if err != errConnectionRefused {
|
|
||||||
return false, -1, err
|
|
||||||
}
|
|
||||||
return false, -1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
//TODO: Should we reconsider having a type in api/types?
|
|
||||||
//this is a response to exex/id/json not container
|
|
||||||
var c struct {
|
|
||||||
Running bool
|
|
||||||
ExitCode int
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil {
|
|
||||||
return false, -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Running, c.ExitCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
|
|
||||||
cli.resizeTty(id, isExec)
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
go func() {
|
|
||||||
prevH, prevW := cli.getTtySize()
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Millisecond * 250)
|
|
||||||
h, w := cli.getTtySize()
|
|
||||||
|
|
||||||
if prevW != w || prevH != h {
|
|
||||||
cli.resizeTty(id, isExec)
|
|
||||||
}
|
|
||||||
prevH = h
|
|
||||||
prevW = w
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
sigchan := make(chan os.Signal, 1)
|
|
||||||
gosignal.Notify(sigchan, signal.SIGWINCH)
|
|
||||||
go func() {
|
|
||||||
for range sigchan {
|
|
||||||
cli.resizeTty(id, isExec)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cli *DockerCli) getTtySize() (int, int) {
|
|
||||||
if !cli.isTerminalOut {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
ws, err := term.GetWinsize(cli.outFd)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error getting size: %s", err)
|
|
||||||
if ws == nil {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return int(ws.Height), int(ws.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBody(serverResp *serverResponse, err error) ([]byte, int, error) {
|
|
||||||
if serverResp.body != nil {
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverResp.statusCode, err
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(serverResp.body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, err
|
|
||||||
}
|
|
||||||
return body, serverResp.statusCode, nil
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"runtime"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/autogen/dockerversion"
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
var VersionTemplate = `Client:
|
|
||||||
Version: {{.Client.Version}}
|
|
||||||
API version: {{.Client.ApiVersion}}
|
|
||||||
Go version: {{.Client.GoVersion}}
|
|
||||||
Git commit: {{.Client.GitCommit}}
|
|
||||||
Built: {{.Client.BuildTime}}
|
|
||||||
OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .Client.Experimental}}
|
|
||||||
Experimental: {{.Client.Experimental}}{{end}}{{if .ServerOK}}
|
|
||||||
|
|
||||||
Server:
|
|
||||||
Version: {{.Server.Version}}
|
|
||||||
API version: {{.Server.ApiVersion}}
|
|
||||||
Go version: {{.Server.GoVersion}}
|
|
||||||
Git commit: {{.Server.GitCommit}}
|
|
||||||
Built: {{.Server.BuildTime}}
|
|
||||||
OS/Arch: {{.Server.Os}}/{{.Server.Arch}}{{if .Server.Experimental}}
|
|
||||||
Experimental: {{.Server.Experimental}}{{end}}{{end}}`
|
|
||||||
|
|
||||||
type VersionData struct {
|
|
||||||
Client types.Version
|
|
||||||
ServerOK bool
|
|
||||||
Server types.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdVersion shows Docker version information.
|
|
||||||
//
|
|
||||||
// Available version information is shown for: client Docker version, client API version, client Go version, client Git commit, client OS/Arch, server Docker version, server API version, server Go version, server Git commit, and server OS/Arch.
|
|
||||||
//
|
|
||||||
// Usage: docker version
|
|
||||||
func (cli *DockerCli) CmdVersion(args ...string) (err error) {
|
|
||||||
cmd := Cli.Subcmd("version", nil, "Show the Docker version information.", true)
|
|
||||||
tmplStr := cmd.String([]string{"f", "#format", "-format"}, "", "Format the output using the given go template")
|
|
||||||
cmd.Require(flag.Exact, 0)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
if *tmplStr == "" {
|
|
||||||
*tmplStr = VersionTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmpl *template.Template
|
|
||||||
if tmpl, err = template.New("").Funcs(funcMap).Parse(*tmplStr); err != nil {
|
|
||||||
return Cli.StatusError{StatusCode: 64,
|
|
||||||
Status: "Template parsing error: " + err.Error()}
|
|
||||||
}
|
|
||||||
|
|
||||||
vd := VersionData{
|
|
||||||
Client: types.Version{
|
|
||||||
Version: dockerversion.VERSION,
|
|
||||||
ApiVersion: api.Version,
|
|
||||||
GoVersion: runtime.Version(),
|
|
||||||
GitCommit: dockerversion.GITCOMMIT,
|
|
||||||
BuildTime: dockerversion.BUILDTIME,
|
|
||||||
Os: runtime.GOOS,
|
|
||||||
Arch: runtime.GOARCH,
|
|
||||||
Experimental: utils.ExperimentalBuild(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err2 := tmpl.Execute(cli.out, vd); err2 != nil && err == nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
cli.out.Write([]byte{'\n'})
|
|
||||||
}()
|
|
||||||
|
|
||||||
serverResp, err := cli.call("GET", "/version", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer serverResp.body.Close()
|
|
||||||
|
|
||||||
if err = json.NewDecoder(serverResp.body).Decode(&vd.Server); err != nil {
|
|
||||||
return Cli.StatusError{StatusCode: 1,
|
|
||||||
Status: "Error reading remote version: " + err.Error()}
|
|
||||||
}
|
|
||||||
|
|
||||||
vd.ServerOK = true
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
Cli "github.com/docker/docker/cli"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdWait blocks until a container stops, then prints its exit code.
|
|
||||||
//
|
|
||||||
// If more than one container is specified, this will wait synchronously on each container.
|
|
||||||
//
|
|
||||||
// Usage: docker wait CONTAINER [CONTAINER...]
|
|
||||||
func (cli *DockerCli) CmdWait(args ...string) error {
|
|
||||||
cmd := Cli.Subcmd("wait", []string{"CONTAINER [CONTAINER...]"}, "Block until a container stops, then print its exit code.", true)
|
|
||||||
cmd.Require(flag.Min, 1)
|
|
||||||
|
|
||||||
cmd.ParseFlags(args, true)
|
|
||||||
|
|
||||||
var errNames []string
|
|
||||||
for _, name := range cmd.Args() {
|
|
||||||
status, err := waitForExit(cli, name)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(cli.err, "%s\n", err)
|
|
||||||
errNames = append(errNames, name)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(cli.out, "%d\n", status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errNames) > 0 {
|
|
||||||
return fmt.Errorf("Error: failed to wait containers: %v", errNames)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"mime"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/pkg/system"
|
|
||||||
"github.com/docker/docker/pkg/version"
|
|
||||||
"github.com/docker/libtrust"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Common constants for daemon and client.
|
|
||||||
const (
|
|
||||||
// Current REST API version
|
|
||||||
Version version.Version = "1.20"
|
|
||||||
|
|
||||||
// Minimun REST API version supported
|
|
||||||
MinVersion version.Version = "1.12"
|
|
||||||
|
|
||||||
// Default filename with Docker commands, read by docker build
|
|
||||||
DefaultDockerfileName string = "Dockerfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ByPrivatePort []types.Port
|
|
||||||
|
|
||||||
func (r ByPrivatePort) Len() int { return len(r) }
|
|
||||||
func (r ByPrivatePort) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
||||||
func (r ByPrivatePort) Less(i, j int) bool { return r[i].PrivatePort < r[j].PrivatePort }
|
|
||||||
|
|
||||||
func DisplayablePorts(ports []types.Port) string {
|
|
||||||
var (
|
|
||||||
result = []string{}
|
|
||||||
hostMappings = []string{}
|
|
||||||
firstInGroupMap map[string]int
|
|
||||||
lastInGroupMap map[string]int
|
|
||||||
)
|
|
||||||
firstInGroupMap = make(map[string]int)
|
|
||||||
lastInGroupMap = make(map[string]int)
|
|
||||||
sort.Sort(ByPrivatePort(ports))
|
|
||||||
for _, port := range ports {
|
|
||||||
var (
|
|
||||||
current = port.PrivatePort
|
|
||||||
portKey = port.Type
|
|
||||||
firstInGroup int
|
|
||||||
lastInGroup int
|
|
||||||
)
|
|
||||||
if port.IP != "" {
|
|
||||||
if port.PublicPort != current {
|
|
||||||
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
|
|
||||||
}
|
|
||||||
firstInGroup = firstInGroupMap[portKey]
|
|
||||||
lastInGroup = lastInGroupMap[portKey]
|
|
||||||
|
|
||||||
if firstInGroup == 0 {
|
|
||||||
firstInGroupMap[portKey] = current
|
|
||||||
lastInGroupMap[portKey] = current
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if current == (lastInGroup + 1) {
|
|
||||||
lastInGroupMap[portKey] = current
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, FormGroup(portKey, firstInGroup, lastInGroup))
|
|
||||||
firstInGroupMap[portKey] = current
|
|
||||||
lastInGroupMap[portKey] = current
|
|
||||||
}
|
|
||||||
for portKey, firstInGroup := range firstInGroupMap {
|
|
||||||
result = append(result, FormGroup(portKey, firstInGroup, lastInGroupMap[portKey]))
|
|
||||||
}
|
|
||||||
result = append(result, hostMappings...)
|
|
||||||
return strings.Join(result, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormGroup(key string, start, last int) string {
|
|
||||||
var (
|
|
||||||
group string
|
|
||||||
parts = strings.Split(key, "/")
|
|
||||||
groupType = parts[0]
|
|
||||||
ip = ""
|
|
||||||
)
|
|
||||||
if len(parts) > 1 {
|
|
||||||
ip = parts[0]
|
|
||||||
groupType = parts[1]
|
|
||||||
}
|
|
||||||
if start == last {
|
|
||||||
group = fmt.Sprintf("%d", start)
|
|
||||||
} else {
|
|
||||||
group = fmt.Sprintf("%d-%d", start, last)
|
|
||||||
}
|
|
||||||
if ip != "" {
|
|
||||||
group = fmt.Sprintf("%s:%s->%s", ip, group, group)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s", group, groupType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MatchesContentType(contentType, expectedType string) bool {
|
|
||||||
mimetype, _, err := mime.ParseMediaType(contentType)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("Error parsing media type: %s error: %v", contentType, err)
|
|
||||||
}
|
|
||||||
return err == nil && mimetype == expectedType
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadOrCreateTrustKey attempts to load the libtrust key at the given path,
|
|
||||||
// otherwise generates a new one
|
|
||||||
func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) {
|
|
||||||
err := system.MkdirAll(filepath.Dir(trustKeyPath), 0700)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
trustKey, err := libtrust.LoadKeyFile(trustKeyPath)
|
|
||||||
if err == libtrust.ErrKeyFileDoesNotExist {
|
|
||||||
trustKey, err = libtrust.GenerateECP256PrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error generating key: %s", err)
|
|
||||||
}
|
|
||||||
if err := libtrust.SaveKey(trustKeyPath, trustKey); err != nil {
|
|
||||||
return nil, fmt.Errorf("Error saving key file: %s", err)
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error loading key file %s: %s", trustKeyPath, err)
|
|
||||||
}
|
|
||||||
return trustKey, nil
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func boolValue(r *http.Request, k string) bool {
|
|
||||||
s := strings.ToLower(strings.TrimSpace(r.FormValue(k)))
|
|
||||||
return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none")
|
|
||||||
}
|
|
||||||
|
|
||||||
// boolValueOrDefault returns the default bool passed if the query param is
|
|
||||||
// missing, otherwise it's just a proxy to boolValue above
|
|
||||||
func boolValueOrDefault(r *http.Request, k string, d bool) bool {
|
|
||||||
if _, ok := r.Form[k]; !ok {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return boolValue(r, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
func int64ValueOrZero(r *http.Request, k string) int64 {
|
|
||||||
val, err := strconv.ParseInt(r.FormValue(k), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
type archiveOptions struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func archiveFormValues(r *http.Request, vars map[string]string) (archiveOptions, error) {
|
|
||||||
if vars == nil {
|
|
||||||
return archiveOptions{}, fmt.Errorf("Missing parameter")
|
|
||||||
}
|
|
||||||
if err := parseForm(r); err != nil {
|
|
||||||
return archiveOptions{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := vars["name"]
|
|
||||||
path := r.Form.Get("path")
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case name == "":
|
|
||||||
return archiveOptions{}, fmt.Errorf("bad parameter: 'name' cannot be empty")
|
|
||||||
case path == "":
|
|
||||||
return archiveOptions{}, fmt.Errorf("bad parameter: 'path' cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return archiveOptions{name, path}, nil
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBoolValue(t *testing.T) {
|
|
||||||
cases := map[string]bool{
|
|
||||||
"": false,
|
|
||||||
"0": false,
|
|
||||||
"no": false,
|
|
||||||
"false": false,
|
|
||||||
"none": false,
|
|
||||||
"1": true,
|
|
||||||
"yes": true,
|
|
||||||
"true": true,
|
|
||||||
"one": true,
|
|
||||||
"100": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for c, e := range cases {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("test", c)
|
|
||||||
r, _ := http.NewRequest("POST", "", nil)
|
|
||||||
r.Form = v
|
|
||||||
|
|
||||||
a := boolValue(r, "test")
|
|
||||||
if a != e {
|
|
||||||
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBoolValueOrDefault(t *testing.T) {
|
|
||||||
r, _ := http.NewRequest("GET", "", nil)
|
|
||||||
if !boolValueOrDefault(r, "queryparam", true) {
|
|
||||||
t.Fatal("Expected to get true default value, got false")
|
|
||||||
}
|
|
||||||
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("param", "")
|
|
||||||
r, _ = http.NewRequest("GET", "", nil)
|
|
||||||
r.Form = v
|
|
||||||
if boolValueOrDefault(r, "param", true) {
|
|
||||||
t.Fatal("Expected not to get true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInt64ValueOrZero(t *testing.T) {
|
|
||||||
cases := map[string]int64{
|
|
||||||
"": 0,
|
|
||||||
"asdf": 0,
|
|
||||||
"0": 0,
|
|
||||||
"1": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
for c, e := range cases {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("test", c)
|
|
||||||
r, _ := http.NewRequest("POST", "", nil)
|
|
||||||
r.Form = v
|
|
||||||
|
|
||||||
a := int64ValueOrZero(r, "test")
|
|
||||||
if a != e {
|
|
||||||
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"expvar"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/pprof"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProfilerSetup(mainRouter *mux.Router, path string) {
|
|
||||||
var r = mainRouter.PathPrefix(path).Subrouter()
|
|
||||||
r.HandleFunc("/vars", expVars)
|
|
||||||
r.HandleFunc("/pprof/", pprof.Index)
|
|
||||||
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
|
|
||||||
r.HandleFunc("/pprof/profile", pprof.Profile)
|
|
||||||
r.HandleFunc("/pprof/symbol", pprof.Symbol)
|
|
||||||
r.HandleFunc("/pprof/block", pprof.Handler("block").ServeHTTP)
|
|
||||||
r.HandleFunc("/pprof/heap", pprof.Handler("heap").ServeHTTP)
|
|
||||||
r.HandleFunc("/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP)
|
|
||||||
r.HandleFunc("/pprof/threadcreate", pprof.Handler("threadcreate").ServeHTTP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replicated from expvar.go as not public.
|
|
||||||
func expVars(w http.ResponseWriter, r *http.Request) {
|
|
||||||
first := true
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
fmt.Fprintf(w, "{\n")
|
|
||||||
expvar.Do(func(kv expvar.KeyValue) {
|
|
||||||
if !first {
|
|
||||||
fmt.Fprintf(w, ",\n")
|
|
||||||
}
|
|
||||||
first = false
|
|
||||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
|
||||||
})
|
|
||||||
fmt.Fprintf(w, "\n}\n")
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
// +build experimental
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
func (s *Server) registerSubRouter() {
|
|
||||||
httpHandler := s.daemon.NetworkApiRouter()
|
|
||||||
|
|
||||||
subrouter := s.router.PathPrefix("/v{version:[0-9.]+}/networks").Subrouter()
|
|
||||||
subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler)
|
|
||||||
subrouter = s.router.PathPrefix("/networks").Subrouter()
|
|
||||||
subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler)
|
|
||||||
|
|
||||||
subrouter = s.router.PathPrefix("/v{version:[0-9.]+}/services").Subrouter()
|
|
||||||
subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler)
|
|
||||||
subrouter = s.router.PathPrefix("/services").Subrouter()
|
|
||||||
subrouter.Methods("GET", "POST", "PUT", "DELETE").HandlerFunc(httpHandler)
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// +build linux
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/daemon"
|
|
||||||
"github.com/docker/docker/pkg/sockets"
|
|
||||||
"github.com/docker/docker/pkg/systemd"
|
|
||||||
"github.com/docker/docker/pkg/version"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
"github.com/docker/libnetwork/portallocator"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// See http://git.kernel.org/cgit/linux/kernel/git/tip/tip.git/tree/kernel/sched/sched.h?id=8cd9234c64c584432f6992fe944ca9e46ca8ea76#n269
|
|
||||||
linuxMinCpuShares = 2
|
|
||||||
linuxMaxCpuShares = 262144
|
|
||||||
)
|
|
||||||
|
|
||||||
// newServer sets up the required serverClosers and does protocol specific checking.
|
|
||||||
func (s *Server) newServer(proto, addr string) ([]serverCloser, error) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
ls []net.Listener
|
|
||||||
)
|
|
||||||
switch proto {
|
|
||||||
case "fd":
|
|
||||||
ls, err = systemd.ListenFD(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// We don't want to start serving on these sockets until the
|
|
||||||
// daemon is initialized and installed. Otherwise required handlers
|
|
||||||
// won't be ready.
|
|
||||||
<-s.start
|
|
||||||
case "tcp":
|
|
||||||
l, err := s.initTcpSocket(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ls = append(ls, l)
|
|
||||||
case "unix":
|
|
||||||
l, err := sockets.NewUnixSocket(addr, s.cfg.SocketGroup, s.start)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ls = append(ls, l)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Invalid protocol format: %q", proto)
|
|
||||||
}
|
|
||||||
var res []serverCloser
|
|
||||||
for _, l := range ls {
|
|
||||||
res = append(res, &HttpServer{
|
|
||||||
&http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: s.router,
|
|
||||||
},
|
|
||||||
l,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) AcceptConnections(d *daemon.Daemon) {
|
|
||||||
// Tell the init daemon we are accepting requests
|
|
||||||
s.daemon = d
|
|
||||||
s.registerSubRouter()
|
|
||||||
go systemd.SdNotify("READY=1")
|
|
||||||
// close the lock so the listeners start accepting connections
|
|
||||||
select {
|
|
||||||
case <-s.start:
|
|
||||||
default:
|
|
||||||
close(s.start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func allocateDaemonPort(addr string) error {
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
intPort, err := strconv.Atoi(port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostIPs []net.IP
|
|
||||||
if parsedIP := net.ParseIP(host); parsedIP != nil {
|
|
||||||
hostIPs = append(hostIPs, parsedIP)
|
|
||||||
} else if hostIPs, err = net.LookupIP(host); err != nil {
|
|
||||||
return fmt.Errorf("failed to lookup %s address in host specification", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
pa := portallocator.Get()
|
|
||||||
for _, hostIP := range hostIPs {
|
|
||||||
if _, err := pa.RequestPort(hostIP, "tcp", intPort); err != nil {
|
|
||||||
return fmt.Errorf("failed to allocate daemon listening port %d (err: %v)", intPort, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func adjustCpuShares(version version.Version, hostConfig *runconfig.HostConfig) {
|
|
||||||
if version.LessThan("1.19") {
|
|
||||||
if hostConfig != nil && hostConfig.CpuShares > 0 {
|
|
||||||
// Handle unsupported CpuShares
|
|
||||||
if hostConfig.CpuShares < linuxMinCpuShares {
|
|
||||||
logrus.Warnf("Changing requested CpuShares of %d to minimum allowed of %d", hostConfig.CpuShares, linuxMinCpuShares)
|
|
||||||
hostConfig.CpuShares = linuxMinCpuShares
|
|
||||||
} else if hostConfig.CpuShares > linuxMaxCpuShares {
|
|
||||||
logrus.Warnf("Changing requested CpuShares of %d to maximum allowed of %d", hostConfig.CpuShares, linuxMaxCpuShares)
|
|
||||||
hostConfig.CpuShares = linuxMaxCpuShares
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// +build linux
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/version"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAdjustCpuSharesOldApi(t *testing.T) {
|
|
||||||
apiVersion := version.Version("1.18")
|
|
||||||
hostConfig := &runconfig.HostConfig{
|
|
||||||
CpuShares: linuxMinCpuShares - 1,
|
|
||||||
}
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != linuxMinCpuShares {
|
|
||||||
t.Errorf("Expected CpuShares to be %d", linuxMinCpuShares)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = linuxMaxCpuShares + 1
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != linuxMaxCpuShares {
|
|
||||||
t.Errorf("Expected CpuShares to be %d", linuxMaxCpuShares)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = 0
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != 0 {
|
|
||||||
t.Error("Expected CpuShares to be unchanged")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = 1024
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != 1024 {
|
|
||||||
t.Error("Expected CpuShares to be unchanged")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdjustCpuSharesNoAdjustment(t *testing.T) {
|
|
||||||
apiVersion := version.Version("1.19")
|
|
||||||
hostConfig := &runconfig.HostConfig{
|
|
||||||
CpuShares: linuxMinCpuShares - 1,
|
|
||||||
}
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != linuxMinCpuShares-1 {
|
|
||||||
t.Errorf("Expected CpuShares to be %d", linuxMinCpuShares-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = linuxMaxCpuShares + 1
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != linuxMaxCpuShares+1 {
|
|
||||||
t.Errorf("Expected CpuShares to be %d", linuxMaxCpuShares+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = 0
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != 0 {
|
|
||||||
t.Error("Expected CpuShares to be unchanged")
|
|
||||||
}
|
|
||||||
|
|
||||||
hostConfig.CpuShares = 1024
|
|
||||||
adjustCpuShares(apiVersion, hostConfig)
|
|
||||||
if hostConfig.CpuShares != 1024 {
|
|
||||||
t.Error("Expected CpuShares to be unchanged")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// +build !experimental
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
func (s *Server) registerSubRouter() {
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// +build windows
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/docker/docker/daemon"
|
|
||||||
"github.com/docker/docker/pkg/version"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewServer sets up the required Server and does protocol specific checking.
|
|
||||||
func (s *Server) newServer(proto, addr string) ([]serverCloser, error) {
|
|
||||||
var (
|
|
||||||
ls []net.Listener
|
|
||||||
)
|
|
||||||
switch proto {
|
|
||||||
case "tcp":
|
|
||||||
l, err := s.initTcpSocket(addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ls = append(ls, l)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errors.New("Invalid protocol format. Windows only supports tcp.")
|
|
||||||
}
|
|
||||||
|
|
||||||
var res []serverCloser
|
|
||||||
for _, l := range ls {
|
|
||||||
res = append(res, &HttpServer{
|
|
||||||
&http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: s.router,
|
|
||||||
},
|
|
||||||
l,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) AcceptConnections(d *daemon.Daemon) {
|
|
||||||
s.daemon = d
|
|
||||||
s.registerSubRouter()
|
|
||||||
// close the lock so the listeners start accepting connections
|
|
||||||
select {
|
|
||||||
case <-s.start:
|
|
||||||
default:
|
|
||||||
close(s.start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func allocateDaemonPort(addr string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func adjustCpuShares(version version.Version, hostConfig *runconfig.HostConfig) {
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// This package is used for API stability in the types and response to the
|
|
||||||
// consumers of the API stats endpoint.
|
|
||||||
package types
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type ThrottlingData struct {
|
|
||||||
// Number of periods with throttling active
|
|
||||||
Periods uint64 `json:"periods"`
|
|
||||||
// Number of periods when the container hit its throttling limit.
|
|
||||||
ThrottledPeriods uint64 `json:"throttled_periods"`
|
|
||||||
// Aggregate time the container was throttled for in nanoseconds.
|
|
||||||
ThrottledTime uint64 `json:"throttled_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// All CPU stats are aggregated since container inception.
|
|
||||||
type CpuUsage struct {
|
|
||||||
// Total CPU time consumed.
|
|
||||||
// Units: nanoseconds.
|
|
||||||
TotalUsage uint64 `json:"total_usage"`
|
|
||||||
// Total CPU time consumed per core.
|
|
||||||
// Units: nanoseconds.
|
|
||||||
PercpuUsage []uint64 `json:"percpu_usage"`
|
|
||||||
// Time spent by tasks of the cgroup in kernel mode.
|
|
||||||
// Units: nanoseconds.
|
|
||||||
UsageInKernelmode uint64 `json:"usage_in_kernelmode"`
|
|
||||||
// Time spent by tasks of the cgroup in user mode.
|
|
||||||
// Units: nanoseconds.
|
|
||||||
UsageInUsermode uint64 `json:"usage_in_usermode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CpuStats struct {
|
|
||||||
CpuUsage CpuUsage `json:"cpu_usage"`
|
|
||||||
SystemUsage uint64 `json:"system_cpu_usage"`
|
|
||||||
ThrottlingData ThrottlingData `json:"throttling_data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemoryStats struct {
|
|
||||||
// current res_counter usage for memory
|
|
||||||
Usage uint64 `json:"usage"`
|
|
||||||
// maximum usage ever recorded.
|
|
||||||
MaxUsage uint64 `json:"max_usage"`
|
|
||||||
// TODO(vishh): Export these as stronger types.
|
|
||||||
// all the stats exported via memory.stat.
|
|
||||||
Stats map[string]uint64 `json:"stats"`
|
|
||||||
// number of times memory usage hits limits.
|
|
||||||
Failcnt uint64 `json:"failcnt"`
|
|
||||||
Limit uint64 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Windows: This can be factored out
|
|
||||||
type BlkioStatEntry struct {
|
|
||||||
Major uint64 `json:"major"`
|
|
||||||
Minor uint64 `json:"minor"`
|
|
||||||
Op string `json:"op"`
|
|
||||||
Value uint64 `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Windows: This can be factored out
|
|
||||||
type BlkioStats struct {
|
|
||||||
// number of bytes tranferred to and from the block device
|
|
||||||
IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive"`
|
|
||||||
IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive"`
|
|
||||||
IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive"`
|
|
||||||
IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive"`
|
|
||||||
IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive"`
|
|
||||||
IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive"`
|
|
||||||
IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive"`
|
|
||||||
SectorsRecursive []BlkioStatEntry `json:"sectors_recursive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Windows: This will require refactoring
|
|
||||||
type Network struct {
|
|
||||||
RxBytes uint64 `json:"rx_bytes"`
|
|
||||||
RxPackets uint64 `json:"rx_packets"`
|
|
||||||
RxErrors uint64 `json:"rx_errors"`
|
|
||||||
RxDropped uint64 `json:"rx_dropped"`
|
|
||||||
TxBytes uint64 `json:"tx_bytes"`
|
|
||||||
TxPackets uint64 `json:"tx_packets"`
|
|
||||||
TxErrors uint64 `json:"tx_errors"`
|
|
||||||
TxDropped uint64 `json:"tx_dropped"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stats struct {
|
|
||||||
Read time.Time `json:"read"`
|
|
||||||
Network Network `json:"network,omitempty"`
|
|
||||||
PreCpuStats CpuStats `json:"precpu_stats,omitempty"`
|
|
||||||
CpuStats CpuStats `json:"cpu_stats,omitempty"`
|
|
||||||
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
|
|
||||||
BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/daemon/network"
|
|
||||||
"github.com/docker/docker/pkg/version"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContainerCreateResponse contains the information returned to a client on the
|
|
||||||
// creation of a new container.
|
|
||||||
type ContainerCreateResponse struct {
|
|
||||||
// ID is the ID of the created container.
|
|
||||||
ID string `json:"Id"`
|
|
||||||
|
|
||||||
// Warnings are any warnings encountered during the creation of the container.
|
|
||||||
Warnings []string `json:"Warnings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /containers/{name:.*}/exec
|
|
||||||
type ContainerExecCreateResponse struct {
|
|
||||||
// ID is the exec ID.
|
|
||||||
ID string `json:"Id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /auth
|
|
||||||
type AuthResponse struct {
|
|
||||||
// Status is the authentication status
|
|
||||||
Status string `json:"Status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST "/containers/"+containerID+"/wait"
|
|
||||||
type ContainerWaitResponse struct {
|
|
||||||
// StatusCode is the status code of the wait job
|
|
||||||
StatusCode int `json:"StatusCode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST "/commit?container="+containerID
|
|
||||||
type ContainerCommitResponse struct {
|
|
||||||
ID string `json:"Id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/containers/{name:.*}/changes"
|
|
||||||
type ContainerChange struct {
|
|
||||||
Kind int
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/images/{name:.*}/history"
|
|
||||||
type ImageHistory struct {
|
|
||||||
ID string `json:"Id"`
|
|
||||||
Created int64
|
|
||||||
CreatedBy string
|
|
||||||
Tags []string
|
|
||||||
Size int64
|
|
||||||
Comment string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE "/images/{name:.*}"
|
|
||||||
type ImageDelete struct {
|
|
||||||
Untagged string `json:",omitempty"`
|
|
||||||
Deleted string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/images/json"
|
|
||||||
type Image struct {
|
|
||||||
ID string `json:"Id"`
|
|
||||||
ParentId string
|
|
||||||
RepoTags []string
|
|
||||||
RepoDigests []string
|
|
||||||
Created int
|
|
||||||
Size int
|
|
||||||
VirtualSize int
|
|
||||||
Labels map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GraphDriverData struct {
|
|
||||||
Name string
|
|
||||||
Data map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/images/{name:.*}/json"
|
|
||||||
type ImageInspect struct {
|
|
||||||
Id string
|
|
||||||
Parent string
|
|
||||||
Comment string
|
|
||||||
Created string
|
|
||||||
Container string
|
|
||||||
ContainerConfig *runconfig.Config
|
|
||||||
DockerVersion string
|
|
||||||
Author string
|
|
||||||
Config *runconfig.Config
|
|
||||||
Architecture string
|
|
||||||
Os string
|
|
||||||
Size int64
|
|
||||||
VirtualSize int64
|
|
||||||
GraphDriver GraphDriverData
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/containers/json"
|
|
||||||
type Port struct {
|
|
||||||
IP string `json:",omitempty"`
|
|
||||||
PrivatePort int
|
|
||||||
PublicPort int `json:",omitempty"`
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Container struct {
|
|
||||||
ID string `json:"Id"`
|
|
||||||
Names []string
|
|
||||||
Image string
|
|
||||||
Command string
|
|
||||||
Created int
|
|
||||||
Ports []Port
|
|
||||||
SizeRw int `json:",omitempty"`
|
|
||||||
SizeRootFs int `json:",omitempty"`
|
|
||||||
Labels map[string]string
|
|
||||||
Status string
|
|
||||||
HostConfig struct {
|
|
||||||
NetworkMode string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST "/containers/"+containerID+"/copy"
|
|
||||||
type CopyConfig struct {
|
|
||||||
Resource string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerPathStat is used to encode the header from
|
|
||||||
// GET /containers/{name:.*}/archive
|
|
||||||
// "name" is basename of the resource.
|
|
||||||
type ContainerPathStat struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Mode os.FileMode `json:"mode"`
|
|
||||||
Mtime time.Time `json:"mtime"`
|
|
||||||
LinkTarget string `json:"linkTarget"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/containers/{name:.*}/top"
|
|
||||||
type ContainerProcessList struct {
|
|
||||||
Processes [][]string
|
|
||||||
Titles []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Version struct {
|
|
||||||
Version string
|
|
||||||
ApiVersion version.Version
|
|
||||||
GitCommit string
|
|
||||||
GoVersion string
|
|
||||||
Os string
|
|
||||||
Arch string
|
|
||||||
KernelVersion string `json:",omitempty"`
|
|
||||||
Experimental bool `json:",omitempty"`
|
|
||||||
BuildTime string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/info"
|
|
||||||
type Info struct {
|
|
||||||
ID string
|
|
||||||
Containers int
|
|
||||||
Images int
|
|
||||||
Driver string
|
|
||||||
DriverStatus [][2]string
|
|
||||||
MemoryLimit bool
|
|
||||||
SwapLimit bool
|
|
||||||
CpuCfsPeriod bool
|
|
||||||
CpuCfsQuota bool
|
|
||||||
IPv4Forwarding bool
|
|
||||||
BridgeNfIptables bool
|
|
||||||
BridgeNfIp6tables bool
|
|
||||||
Debug bool
|
|
||||||
NFd int
|
|
||||||
OomKillDisable bool
|
|
||||||
NGoroutines int
|
|
||||||
SystemTime string
|
|
||||||
ExecutionDriver string
|
|
||||||
LoggingDriver string
|
|
||||||
NEventsListener int
|
|
||||||
KernelVersion string
|
|
||||||
OperatingSystem string
|
|
||||||
IndexServerAddress string
|
|
||||||
RegistryConfig interface{}
|
|
||||||
InitSha1 string
|
|
||||||
InitPath string
|
|
||||||
NCPU int
|
|
||||||
MemTotal int64
|
|
||||||
DockerRootDir string
|
|
||||||
HttpProxy string
|
|
||||||
HttpsProxy string
|
|
||||||
NoProxy string
|
|
||||||
Name string
|
|
||||||
Labels []string
|
|
||||||
ExperimentalBuild bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// This struct is a temp struct used by execStart
|
|
||||||
// Config fields is part of ExecConfig in runconfig package
|
|
||||||
type ExecStartCheck struct {
|
|
||||||
// ExecStart will first check if it's detached
|
|
||||||
Detach bool
|
|
||||||
// Check if there's a tty
|
|
||||||
Tty bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerState struct {
|
|
||||||
Running bool
|
|
||||||
Paused bool
|
|
||||||
Restarting bool
|
|
||||||
OOMKilled bool
|
|
||||||
Dead bool
|
|
||||||
Pid int
|
|
||||||
ExitCode int
|
|
||||||
Error string
|
|
||||||
StartedAt string
|
|
||||||
FinishedAt string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET "/containers/{name:.*}/json"
|
|
||||||
type ContainerJSONBase struct {
|
|
||||||
Id string
|
|
||||||
Created string
|
|
||||||
Path string
|
|
||||||
Args []string
|
|
||||||
State *ContainerState
|
|
||||||
Image string
|
|
||||||
NetworkSettings *network.Settings
|
|
||||||
ResolvConfPath string
|
|
||||||
HostnamePath string
|
|
||||||
HostsPath string
|
|
||||||
LogPath string
|
|
||||||
Name string
|
|
||||||
RestartCount int
|
|
||||||
Driver string
|
|
||||||
ExecDriver string
|
|
||||||
MountLabel string
|
|
||||||
ProcessLabel string
|
|
||||||
AppArmorProfile string
|
|
||||||
ExecIDs []string
|
|
||||||
HostConfig *runconfig.HostConfig
|
|
||||||
GraphDriver GraphDriverData
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerJSON struct {
|
|
||||||
*ContainerJSONBase
|
|
||||||
Mounts []MountPoint
|
|
||||||
Config *runconfig.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// backcompatibility struct along with ContainerConfig
|
|
||||||
type ContainerJSONPre120 struct {
|
|
||||||
*ContainerJSONBase
|
|
||||||
Volumes map[string]string
|
|
||||||
VolumesRW map[string]bool
|
|
||||||
Config *ContainerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContainerConfig struct {
|
|
||||||
*runconfig.Config
|
|
||||||
|
|
||||||
// backward compatibility, they now live in HostConfig
|
|
||||||
Memory int64
|
|
||||||
MemorySwap int64
|
|
||||||
CpuShares int64
|
|
||||||
Cpuset string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MountPoint represents a mount point configuration inside the container.
|
|
||||||
type MountPoint struct {
|
|
||||||
Name string `json:",omitempty"`
|
|
||||||
Source string
|
|
||||||
Destination string
|
|
||||||
Driver string `json:",omitempty"`
|
|
||||||
Mode string // this is internally named `Relabel`
|
|
||||||
RW bool
|
|
||||||
}
|
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
type APIHistory struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Tags []string `json:",omitempty"`
|
||||||
|
Created int64
|
||||||
|
CreatedBy string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIImages struct {
|
||||||
|
Repository string `json:",omitempty"`
|
||||||
|
Tag string `json:",omitempty"`
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Created int64
|
||||||
|
Size int64
|
||||||
|
VirtualSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIInfo struct {
|
||||||
|
Debug bool
|
||||||
|
Containers int
|
||||||
|
Images int
|
||||||
|
NFd int `json:",omitempty"`
|
||||||
|
NGoroutines int `json:",omitempty"`
|
||||||
|
MemoryLimit bool `json:",omitempty"`
|
||||||
|
SwapLimit bool `json:",omitempty"`
|
||||||
|
IPv4Forwarding bool `json:",omitempty"`
|
||||||
|
LXCVersion string `json:",omitempty"`
|
||||||
|
NEventsListener int `json:",omitempty"`
|
||||||
|
KernelVersion string `json:",omitempty"`
|
||||||
|
IndexServerAddress string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APITop struct {
|
||||||
|
Titles []string
|
||||||
|
Processes [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIRmi struct {
|
||||||
|
Deleted string `json:",omitempty"`
|
||||||
|
Untagged string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIContainers struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Image string
|
||||||
|
Command string
|
||||||
|
Created int64
|
||||||
|
Status string
|
||||||
|
Ports []APIPort
|
||||||
|
SizeRw int64
|
||||||
|
SizeRootFs int64
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *APIContainers) ToLegacy() APIContainersOld {
|
||||||
|
return APIContainersOld{
|
||||||
|
ID: self.ID,
|
||||||
|
Image: self.Image,
|
||||||
|
Command: self.Command,
|
||||||
|
Created: self.Created,
|
||||||
|
Status: self.Status,
|
||||||
|
Ports: displayablePorts(self.Ports),
|
||||||
|
SizeRw: self.SizeRw,
|
||||||
|
SizeRootFs: self.SizeRootFs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIContainersOld struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Image string
|
||||||
|
Command string
|
||||||
|
Created int64
|
||||||
|
Status string
|
||||||
|
Ports string
|
||||||
|
SizeRw int64
|
||||||
|
SizeRootFs int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type APISearch struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIID struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIRun struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Warnings []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIPort struct {
|
||||||
|
PrivatePort int64
|
||||||
|
PublicPort int64
|
||||||
|
Type string
|
||||||
|
IP string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIVersion struct {
|
||||||
|
Version string
|
||||||
|
GitCommit string `json:",omitempty"`
|
||||||
|
GoVersion string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIWait struct {
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIAuth struct {
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIImageConfig struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
*Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type APICopy struct {
|
||||||
|
Resource string
|
||||||
|
HostPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APILink struct {
|
||||||
|
Path string
|
||||||
|
ContainerID string
|
||||||
|
Image string
|
||||||
|
}
|
||||||
+1350
File diff suppressed because it is too large
Load Diff
+302
@@ -0,0 +1,302 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dotcloud/docker/utils"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Archive io.Reader
|
||||||
|
|
||||||
|
type Compression uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
Uncompressed Compression = iota
|
||||||
|
Bzip2
|
||||||
|
Gzip
|
||||||
|
Xz
|
||||||
|
)
|
||||||
|
|
||||||
|
func DetectCompression(source []byte) Compression {
|
||||||
|
sourceLen := len(source)
|
||||||
|
for compression, m := range map[Compression][]byte{
|
||||||
|
Bzip2: {0x42, 0x5A, 0x68},
|
||||||
|
Gzip: {0x1F, 0x8B, 0x08},
|
||||||
|
Xz: {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00},
|
||||||
|
} {
|
||||||
|
fail := false
|
||||||
|
if len(m) > sourceLen {
|
||||||
|
utils.Debugf("Len too short")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for _, b := range m {
|
||||||
|
if b != source[i] {
|
||||||
|
fail = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if !fail {
|
||||||
|
return compression
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Uncompressed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (compression *Compression) Flag() string {
|
||||||
|
switch *compression {
|
||||||
|
case Bzip2:
|
||||||
|
return "j"
|
||||||
|
case Gzip:
|
||||||
|
return "z"
|
||||||
|
case Xz:
|
||||||
|
return "J"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (compression *Compression) Extension() string {
|
||||||
|
switch *compression {
|
||||||
|
case Uncompressed:
|
||||||
|
return "tar"
|
||||||
|
case Bzip2:
|
||||||
|
return "tar.bz2"
|
||||||
|
case Gzip:
|
||||||
|
return "tar.gz"
|
||||||
|
case Xz:
|
||||||
|
return "tar.xz"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tar creates an archive from the directory at `path`, and returns it as a
|
||||||
|
// stream of bytes.
|
||||||
|
func Tar(path string, compression Compression) (io.Reader, error) {
|
||||||
|
return TarFilter(path, compression, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tar creates an archive from the directory at `path`, only including files whose relative
|
||||||
|
// paths are included in `filter`. If `filter` is nil, then all files are included.
|
||||||
|
func TarFilter(path string, compression Compression, filter []string) (io.Reader, error) {
|
||||||
|
args := []string{"tar", "--numeric-owner", "-f", "-", "-C", path}
|
||||||
|
if filter == nil {
|
||||||
|
filter = []string{"."}
|
||||||
|
}
|
||||||
|
for _, f := range filter {
|
||||||
|
args = append(args, "-c"+compression.Flag(), f)
|
||||||
|
}
|
||||||
|
return CmdStream(exec.Command(args[0], args[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untar reads a stream of bytes from `archive`, parses it as a tar archive,
|
||||||
|
// and unpacks it into the directory at `path`.
|
||||||
|
// The archive may be compressed with one of the following algorithms:
|
||||||
|
// identity (uncompressed), gzip, bzip2, xz.
|
||||||
|
// FIXME: specify behavior when target path exists vs. doesn't exist.
|
||||||
|
func Untar(archive io.Reader, path string) error {
|
||||||
|
if archive == nil {
|
||||||
|
return fmt.Errorf("Empty archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 10)
|
||||||
|
totalN := 0
|
||||||
|
for totalN < 10 {
|
||||||
|
if n, err := archive.Read(buf[totalN:]); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return fmt.Errorf("Tarball too short")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
totalN += n
|
||||||
|
utils.Debugf("[tar autodetect] n: %d", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compression := DetectCompression(buf)
|
||||||
|
|
||||||
|
utils.Debugf("Archive compression detected: %s", compression.Extension())
|
||||||
|
|
||||||
|
cmd := exec.Command("tar", "--numeric-owner", "-f", "-", "-C", path, "-x"+compression.Flag())
|
||||||
|
cmd.Stdin = io.MultiReader(bytes.NewReader(buf), archive)
|
||||||
|
// Hardcode locale environment for predictable outcome regardless of host configuration.
|
||||||
|
// (see https://github.com/dotcloud/docker/issues/355)
|
||||||
|
cmd.Env = []string{"LANG=en_US.utf-8", "LC_ALL=en_US.utf-8"}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %s", err, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TarUntar is a convenience function which calls Tar and Untar, with
|
||||||
|
// the output of one piped into the other. If either Tar or Untar fails,
|
||||||
|
// TarUntar aborts and returns the error.
|
||||||
|
func TarUntar(src string, filter []string, dst string) error {
|
||||||
|
utils.Debugf("TarUntar(%s %s %s)", src, filter, dst)
|
||||||
|
archive, err := TarFilter(src, Uncompressed, filter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Untar(archive, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UntarPath is a convenience function which looks for an archive
|
||||||
|
// at filesystem path `src`, and unpacks it at `dst`.
|
||||||
|
func UntarPath(src, dst string) error {
|
||||||
|
if archive, err := os.Open(src); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := Untar(archive, dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyWithTar creates a tar archive of filesystem path `src`, and
|
||||||
|
// unpacks it at filesystem path `dst`.
|
||||||
|
// The archive is streamed directly with fixed buffering and no
|
||||||
|
// intermediary disk IO.
|
||||||
|
//
|
||||||
|
func CopyWithTar(src, dst string) error {
|
||||||
|
srcSt, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !srcSt.IsDir() {
|
||||||
|
return CopyFileWithTar(src, dst)
|
||||||
|
}
|
||||||
|
// Create dst, copy src's content into it
|
||||||
|
utils.Debugf("Creating dest directory: %s", dst)
|
||||||
|
if err := os.MkdirAll(dst, 0755); err != nil && !os.IsExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
utils.Debugf("Calling TarUntar(%s, %s)", src, dst)
|
||||||
|
return TarUntar(src, nil, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFileWithTar emulates the behavior of the 'cp' command-line
|
||||||
|
// for a single file. It copies a regular file from path `src` to
|
||||||
|
// path `dst`, and preserves all its metadata.
|
||||||
|
//
|
||||||
|
// If `dst` ends with a trailing slash '/', the final destination path
|
||||||
|
// will be `dst/base(src)`.
|
||||||
|
func CopyFileWithTar(src, dst string) error {
|
||||||
|
utils.Debugf("CopyFileWithTar(%s, %s)", src, dst)
|
||||||
|
srcSt, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if srcSt.IsDir() {
|
||||||
|
return fmt.Errorf("Can't copy a directory")
|
||||||
|
}
|
||||||
|
// Clean up the trailing /
|
||||||
|
if dst[len(dst)-1] == '/' {
|
||||||
|
dst = path.Join(dst, filepath.Base(src))
|
||||||
|
}
|
||||||
|
// Create the holding directory if necessary
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil && !os.IsExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
tw := tar.NewWriter(buf)
|
||||||
|
hdr, err := tar.FileInfoHeader(srcSt, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hdr.Name = filepath.Base(dst)
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srcF, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(tw, srcF); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tw.Close()
|
||||||
|
return Untar(buf, filepath.Dir(dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdStream executes a command, and returns its stdout as a stream.
|
||||||
|
// If the command fails to run or doesn't complete successfully, an error
|
||||||
|
// will be returned, including anything written on stderr.
|
||||||
|
func CmdStream(cmd *exec.Cmd) (io.Reader, error) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pipeR, pipeW := io.Pipe()
|
||||||
|
errChan := make(chan []byte)
|
||||||
|
// Collect stderr, we will use it in case of an error
|
||||||
|
go func() {
|
||||||
|
errText, e := ioutil.ReadAll(stderr)
|
||||||
|
if e != nil {
|
||||||
|
errText = []byte("(...couldn't fetch stderr: " + e.Error() + ")")
|
||||||
|
}
|
||||||
|
errChan <- errText
|
||||||
|
}()
|
||||||
|
// Copy stdout to the returned pipe
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(pipeW, stdout)
|
||||||
|
if err != nil {
|
||||||
|
pipeW.CloseWithError(err)
|
||||||
|
}
|
||||||
|
errText := <-errChan
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
pipeW.CloseWithError(fmt.Errorf("%s: %s", err, errText))
|
||||||
|
} else {
|
||||||
|
pipeW.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Run the command and return the pipe
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pipeR, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTempArchive reads the content of src into a temporary file, and returns the contents
|
||||||
|
// of that file as an archive. The archive can only be read once - as soon as reading completes,
|
||||||
|
// the file will be deleted.
|
||||||
|
func NewTempArchive(src Archive, dir string) (*TempArchive, error) {
|
||||||
|
f, err := ioutil.TempFile(dir, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(f, src); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
st, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
size := st.Size()
|
||||||
|
return &TempArchive{f, size}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TempArchive struct {
|
||||||
|
*os.File
|
||||||
|
Size int64 // Pre-computed from Stat().Size() as a convenience
|
||||||
|
}
|
||||||
|
|
||||||
|
func (archive *TempArchive) Read(data []byte) (int, error) {
|
||||||
|
n, err := archive.File.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(archive.File.Name())
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmdStreamLargeStderr(t *testing.T) {
|
||||||
|
cmd := exec.Command("/bin/sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello")
|
||||||
|
out, err := CmdStream(cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start command: %s", err)
|
||||||
|
}
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(ioutil.Discard, out)
|
||||||
|
errCh <- err
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Command should not have failed (err=%.100s...)", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("Command did not complete in 5 seconds; probable deadlock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdStreamBad(t *testing.T) {
|
||||||
|
badCmd := exec.Command("/bin/sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1")
|
||||||
|
out, err := CmdStream(badCmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start command: %s", err)
|
||||||
|
}
|
||||||
|
if output, err := ioutil.ReadAll(out); err == nil {
|
||||||
|
t.Fatalf("Command should have failed")
|
||||||
|
} else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" {
|
||||||
|
t.Fatalf("Wrong error value (%s)", err)
|
||||||
|
} else if s := string(output); s != "hello\n" {
|
||||||
|
t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdStreamGood(t *testing.T) {
|
||||||
|
cmd := exec.Command("/bin/sh", "-c", "echo hello; exit 0")
|
||||||
|
out, err := CmdStream(cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if output, err := ioutil.ReadAll(out); err != nil {
|
||||||
|
t.Fatalf("Command should not have failed (err=%s)", err)
|
||||||
|
} else if s := string(output); s != "hello\n" {
|
||||||
|
t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarUntar(t *testing.T, origin string, compression Compression) error {
|
||||||
|
archive, err := Tar(origin, compression)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 10)
|
||||||
|
if _, err := archive.Read(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
archive = io.MultiReader(bytes.NewReader(buf), archive)
|
||||||
|
|
||||||
|
detectedCompression := DetectCompression(buf)
|
||||||
|
if detectedCompression.Extension() != compression.Extension() {
|
||||||
|
return fmt.Errorf("Wrong compression detected. Actual compression: %s, found %s", compression.Extension(), detectedCompression.Extension())
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ioutil.TempDir("", "docker-test-untar")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
if err := Untar(archive, tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTarUntar(t *testing.T) {
|
||||||
|
origin, err := ioutil.TempDir("", "docker-test-untar-origin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(origin)
|
||||||
|
if err := ioutil.WriteFile(path.Join(origin, "1"), []byte("hello world"), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(path.Join(origin, "2"), []byte("welcome!"), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range []Compression{
|
||||||
|
Uncompressed,
|
||||||
|
Gzip,
|
||||||
|
Bzip2,
|
||||||
|
Xz,
|
||||||
|
} {
|
||||||
|
if err := tarUntar(t, origin, c); err != nil {
|
||||||
|
t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Sam Alba <sam@dotcloud.com> (@samalba)
|
||||||
|
Joffrey Fuhrer <joffrey@dotcloud.com> (@shin-)
|
||||||
|
Ken Cochrane <ken@dotcloud.com> (@kencochrane)
|
||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/dotcloud/docker/utils"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Where we store the config file
|
||||||
|
const CONFIGFILE = ".dockercfg"
|
||||||
|
|
||||||
|
// Only used for user auth + account creation
|
||||||
|
const INDEXSERVER = "https://index.docker.io/v1/"
|
||||||
|
|
||||||
|
//const INDEXSERVER = "https://indexstaging-docker.dotcloud.com/v1/"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrConfigFileMissing = errors.New("The Auth config file is missing")
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthConfig struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Auth string `json:"auth"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
ServerAddress string `json:"serveraddress,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigFile struct {
|
||||||
|
Configs map[string]AuthConfig `json:"configs,omitempty"`
|
||||||
|
rootPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexServerAddress() string {
|
||||||
|
return INDEXSERVER
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a base64 encoded auth string to store in config
|
||||||
|
func encodeAuth(authConfig *AuthConfig) string {
|
||||||
|
authStr := authConfig.Username + ":" + authConfig.Password
|
||||||
|
msg := []byte(authStr)
|
||||||
|
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
|
||||||
|
base64.StdEncoding.Encode(encoded, msg)
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode the auth string
|
||||||
|
func decodeAuth(authStr string) (string, string, error) {
|
||||||
|
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
||||||
|
decoded := make([]byte, decLen)
|
||||||
|
authByte := []byte(authStr)
|
||||||
|
n, err := base64.StdEncoding.Decode(decoded, authByte)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if n > decLen {
|
||||||
|
return "", "", fmt.Errorf("Something went wrong decoding auth config")
|
||||||
|
}
|
||||||
|
arr := strings.Split(string(decoded), ":")
|
||||||
|
if len(arr) != 2 {
|
||||||
|
return "", "", fmt.Errorf("Invalid auth configuration file")
|
||||||
|
}
|
||||||
|
password := strings.Trim(arr[1], "\x00")
|
||||||
|
return arr[0], password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load up the auth config information and return values
|
||||||
|
// FIXME: use the internal golang config parser
|
||||||
|
func LoadConfig(rootPath string) (*ConfigFile, error) {
|
||||||
|
configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
|
||||||
|
confFile := path.Join(rootPath, CONFIGFILE)
|
||||||
|
if _, err := os.Stat(confFile); err != nil {
|
||||||
|
return &configFile, nil //missing file is not an error
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadFile(confFile)
|
||||||
|
if err != nil {
|
||||||
|
return &configFile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &configFile.Configs); err != nil {
|
||||||
|
arr := strings.Split(string(b), "\n")
|
||||||
|
if len(arr) < 2 {
|
||||||
|
return &configFile, fmt.Errorf("The Auth config file is empty")
|
||||||
|
}
|
||||||
|
authConfig := AuthConfig{}
|
||||||
|
origAuth := strings.Split(arr[0], " = ")
|
||||||
|
if len(origAuth) != 2 {
|
||||||
|
return &configFile, fmt.Errorf("Invalid Auth config file")
|
||||||
|
}
|
||||||
|
authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
|
||||||
|
if err != nil {
|
||||||
|
return &configFile, err
|
||||||
|
}
|
||||||
|
origEmail := strings.Split(arr[1], " = ")
|
||||||
|
if len(origEmail) != 2 {
|
||||||
|
return &configFile, fmt.Errorf("Invalid Auth config file")
|
||||||
|
}
|
||||||
|
authConfig.Email = origEmail[1]
|
||||||
|
authConfig.ServerAddress = IndexServerAddress()
|
||||||
|
configFile.Configs[IndexServerAddress()] = authConfig
|
||||||
|
} else {
|
||||||
|
for k, authConfig := range configFile.Configs {
|
||||||
|
authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return &configFile, err
|
||||||
|
}
|
||||||
|
authConfig.Auth = ""
|
||||||
|
configFile.Configs[k] = authConfig
|
||||||
|
authConfig.ServerAddress = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &configFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the auth config
|
||||||
|
func SaveConfig(configFile *ConfigFile) error {
|
||||||
|
confFile := path.Join(configFile.rootPath, CONFIGFILE)
|
||||||
|
if len(configFile.Configs) == 0 {
|
||||||
|
os.Remove(confFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := make(map[string]AuthConfig, len(configFile.Configs))
|
||||||
|
for k, authConfig := range configFile.Configs {
|
||||||
|
authCopy := authConfig
|
||||||
|
|
||||||
|
authCopy.Auth = encodeAuth(&authCopy)
|
||||||
|
authCopy.Username = ""
|
||||||
|
authCopy.Password = ""
|
||||||
|
authCopy.ServerAddress = ""
|
||||||
|
configs[k] = authCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(configs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(confFile, b, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to register/login to the registry server
|
||||||
|
func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
reqStatusCode := 0
|
||||||
|
var status string
|
||||||
|
var reqBody []byte
|
||||||
|
|
||||||
|
serverAddress := authConfig.ServerAddress
|
||||||
|
if serverAddress == "" {
|
||||||
|
serverAddress = IndexServerAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
|
||||||
|
|
||||||
|
// to avoid sending the server address to the server it should be removed before marshalled
|
||||||
|
authCopy := *authConfig
|
||||||
|
authCopy.ServerAddress = ""
|
||||||
|
|
||||||
|
jsonBody, err := json.Marshal(authCopy)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Config Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
|
||||||
|
b := strings.NewReader(string(jsonBody))
|
||||||
|
req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Server Error: %s", err)
|
||||||
|
}
|
||||||
|
reqStatusCode = req1.StatusCode
|
||||||
|
defer req1.Body.Close()
|
||||||
|
reqBody, err = ioutil.ReadAll(req1.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqStatusCode == 201 {
|
||||||
|
if loginAgainstOfficialIndex {
|
||||||
|
status = "Account created. Please use the confirmation link we sent" +
|
||||||
|
" to your e-mail to activate it."
|
||||||
|
} else {
|
||||||
|
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
|
||||||
|
}
|
||||||
|
} else if reqStatusCode == 403 {
|
||||||
|
if loginAgainstOfficialIndex {
|
||||||
|
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
|
||||||
|
"Please check your e-mail for a confirmation link.")
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
|
||||||
|
"Please see the documentation of the registry " + serverAddress + " for instructions how to activate it.")
|
||||||
|
}
|
||||||
|
} else if reqStatusCode == 400 {
|
||||||
|
if string(reqBody) == "\"Username or email already exists\"" {
|
||||||
|
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
|
||||||
|
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
status = "Login Succeeded"
|
||||||
|
} else if resp.StatusCode == 401 {
|
||||||
|
return "", fmt.Errorf("Wrong login/password, please try again")
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
|
||||||
|
resp.StatusCode, resp.Header)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Registration: %s", reqBody)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// this method matches a auth configuration to a server address or a url
|
||||||
|
func (config *ConfigFile) ResolveAuthConfig(registry string) AuthConfig {
|
||||||
|
if registry == IndexServerAddress() || len(registry) == 0 {
|
||||||
|
// default to the index server
|
||||||
|
return config.Configs[IndexServerAddress()]
|
||||||
|
}
|
||||||
|
// if its not the index server there are three cases:
|
||||||
|
//
|
||||||
|
// 1. this is a full config url -> it should be used as is
|
||||||
|
// 2. it could be a full url, but with the wrong protocol
|
||||||
|
// 3. it can be the hostname optionally with a port
|
||||||
|
//
|
||||||
|
// as there is only one auth entry which is fully qualified we need to start
|
||||||
|
// parsing and matching
|
||||||
|
|
||||||
|
swapProtocol := func(url string) string {
|
||||||
|
if strings.HasPrefix(url, "http:") {
|
||||||
|
return strings.Replace(url, "http:", "https:", 1)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(url, "https:") {
|
||||||
|
return strings.Replace(url, "https:", "http:", 1)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveIgnoringProtocol := func(url string) AuthConfig {
|
||||||
|
if c, found := config.Configs[url]; found {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
registrySwappedProtocol := swapProtocol(url)
|
||||||
|
// now try to match with the different protocol
|
||||||
|
if c, found := config.Configs[registrySwappedProtocol]; found {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return AuthConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match both protocols as it could also be a server name like httpfoo
|
||||||
|
if strings.HasPrefix(registry, "http:") || strings.HasPrefix(registry, "https:") {
|
||||||
|
return resolveIgnoringProtocol(registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://" + registry
|
||||||
|
if !strings.Contains(registry, "/") {
|
||||||
|
url = url + "/v1/"
|
||||||
|
}
|
||||||
|
return resolveIgnoringProtocol(url)
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeAuth(t *testing.T) {
|
||||||
|
newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
|
||||||
|
authStr := encodeAuth(newAuthConfig)
|
||||||
|
decAuthConfig := &AuthConfig{}
|
||||||
|
var err error
|
||||||
|
decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if newAuthConfig.Username != decAuthConfig.Username {
|
||||||
|
t.Fatal("Encode Username doesn't match decoded Username")
|
||||||
|
}
|
||||||
|
if newAuthConfig.Password != decAuthConfig.Password {
|
||||||
|
t.Fatal("Encode Password doesn't match decoded Password")
|
||||||
|
}
|
||||||
|
if authStr != "a2VuOnRlc3Q=" {
|
||||||
|
t.Fatal("AuthString encoding isn't correct.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
|
||||||
|
defer os.Setenv("DOCKER_INDEX_URL", "")
|
||||||
|
authConfig := &AuthConfig{Username: "unittester", Password: "surlautrerivejetattendrai", Email: "noise+unittester@dotcloud.com"}
|
||||||
|
status, err := Login(authConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if status != "Login Succeeded" {
|
||||||
|
t.Fatalf("Expected status \"Login Succeeded\", found \"%s\" instead", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateAccount(t *testing.T) {
|
||||||
|
os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
|
||||||
|
defer os.Setenv("DOCKER_INDEX_URL", "")
|
||||||
|
tokenBuffer := make([]byte, 16)
|
||||||
|
_, err := rand.Read(tokenBuffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
token := hex.EncodeToString(tokenBuffer)[:12]
|
||||||
|
username := "ut" + token
|
||||||
|
authConfig := &AuthConfig{Username: username, Password: "test42", Email: "docker-ut+" + token + "@example.com"}
|
||||||
|
status, err := Login(authConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expectedStatus := "Account created. Please use the confirmation link we sent" +
|
||||||
|
" to your e-mail to activate it."
|
||||||
|
if status != expectedStatus {
|
||||||
|
t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err = Login(authConfig, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error but found nil instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "Login: Account is not Active"
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), expectedError) {
|
||||||
|
t.Fatalf("Expected message \"%s\" but found \"%s\" instead", expectedError, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSameAuthDataPostSave(t *testing.T) {
|
||||||
|
root, err := ioutil.TempDir("", "docker-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
configFile := &ConfigFile{
|
||||||
|
rootPath: root,
|
||||||
|
Configs: make(map[string]AuthConfig, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile.Configs["testIndex"] = AuthConfig{
|
||||||
|
Username: "docker-user",
|
||||||
|
Password: "docker-pass",
|
||||||
|
Email: "docker@docker.io",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SaveConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig := configFile.Configs["testIndex"]
|
||||||
|
if authConfig.Username != "docker-user" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if authConfig.Password != "docker-pass" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if authConfig.Email != "docker@docker.io" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if authConfig.Auth != "" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FlagType is the type of the build flag
|
|
||||||
type FlagType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
boolType FlagType = iota
|
|
||||||
stringType
|
|
||||||
)
|
|
||||||
|
|
||||||
// BFlags contains all flags information for the builder
|
|
||||||
type BFlags struct {
|
|
||||||
Args []string // actual flags/args from cmd line
|
|
||||||
flags map[string]*Flag
|
|
||||||
used map[string]*Flag
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flag contains all information for a flag
|
|
||||||
type Flag struct {
|
|
||||||
bf *BFlags
|
|
||||||
name string
|
|
||||||
flagType FlagType
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBFlags return the new BFlags struct
|
|
||||||
func NewBFlags() *BFlags {
|
|
||||||
return &BFlags{
|
|
||||||
flags: make(map[string]*Flag),
|
|
||||||
used: make(map[string]*Flag),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddBool adds a bool flag to BFlags
|
|
||||||
func (bf *BFlags) AddBool(name string, def bool) *Flag {
|
|
||||||
flag := bf.addFlag(name, boolType)
|
|
||||||
if flag == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if def {
|
|
||||||
flag.Value = "true"
|
|
||||||
} else {
|
|
||||||
flag.Value = "false"
|
|
||||||
}
|
|
||||||
return flag
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddString adds a string flag to BFlags
|
|
||||||
func (bf *BFlags) AddString(name string, def string) *Flag {
|
|
||||||
flag := bf.addFlag(name, stringType)
|
|
||||||
if flag == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
flag.Value = def
|
|
||||||
return flag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
|
|
||||||
if _, ok := bf.flags[name]; ok {
|
|
||||||
bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
newFlag := &Flag{
|
|
||||||
bf: bf,
|
|
||||||
name: name,
|
|
||||||
flagType: flagType,
|
|
||||||
}
|
|
||||||
bf.flags[name] = newFlag
|
|
||||||
|
|
||||||
return newFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUsed checks if the flag is used
|
|
||||||
func (fl *Flag) IsUsed() bool {
|
|
||||||
if _, ok := fl.bf.used[fl.name]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTrue checks if a bool flag is true
|
|
||||||
func (fl *Flag) IsTrue() bool {
|
|
||||||
if fl.flagType != boolType {
|
|
||||||
// Should never get here
|
|
||||||
panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
|
|
||||||
}
|
|
||||||
return fl.Value == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses and checks if the BFlags is valid
|
|
||||||
func (bf *BFlags) Parse() error {
|
|
||||||
// If there was an error while defining the possible flags
|
|
||||||
// go ahead and bubble it back up here since we didn't do it
|
|
||||||
// earlier in the processing
|
|
||||||
if bf.Err != nil {
|
|
||||||
return fmt.Errorf("Error setting up flags: %s", bf.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arg := range bf.Args {
|
|
||||||
if !strings.HasPrefix(arg, "--") {
|
|
||||||
return fmt.Errorf("Arg should start with -- : %s", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if arg == "--" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
arg = arg[2:]
|
|
||||||
value := ""
|
|
||||||
|
|
||||||
index := strings.Index(arg, "=")
|
|
||||||
if index >= 0 {
|
|
||||||
value = arg[index+1:]
|
|
||||||
arg = arg[:index]
|
|
||||||
}
|
|
||||||
|
|
||||||
flag, ok := bf.flags[arg]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Unknown flag: %s", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok = bf.used[arg]; ok {
|
|
||||||
return fmt.Errorf("Duplicate flag specified: %s", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
bf.used[arg] = flag
|
|
||||||
|
|
||||||
switch flag.flagType {
|
|
||||||
case boolType:
|
|
||||||
// value == "" is only ok if no "=" was specified
|
|
||||||
if index >= 0 && value == "" {
|
|
||||||
return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
lower := strings.ToLower(value)
|
|
||||||
if lower == "" {
|
|
||||||
flag.Value = "true"
|
|
||||||
} else if lower == "true" || lower == "false" {
|
|
||||||
flag.Value = lower
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
case stringType:
|
|
||||||
if index < 0 {
|
|
||||||
return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
||||||
}
|
|
||||||
flag.Value = value
|
|
||||||
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("No idea what kind of flag we have! Should never get here!"))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuilderFlags(t *testing.T) {
|
|
||||||
var expected string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf := NewBFlags()
|
|
||||||
bf.Args = []string{}
|
|
||||||
if err := bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
bf.Args = []string{"--"}
|
|
||||||
if err := bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 := bf.AddString("str1", "")
|
|
||||||
flBool1 := bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{}
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flStr1.IsUsed() == true {
|
|
||||||
t.Fatalf("Test3 - str1 was not used!")
|
|
||||||
}
|
|
||||||
if flBool1.IsUsed() == true {
|
|
||||||
t.Fatalf("Test3 - bool1 was not used!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 = bf.AddString("str1", "HI")
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flStr1.Value != "HI" {
|
|
||||||
t.Fatalf("Str1 was supposed to default to: HI")
|
|
||||||
}
|
|
||||||
if flBool1.IsTrue() {
|
|
||||||
t.Fatalf("Bool1 was supposed to default to: false")
|
|
||||||
}
|
|
||||||
if flStr1.IsUsed() == true {
|
|
||||||
t.Fatalf("Str1 was not used!")
|
|
||||||
}
|
|
||||||
if flBool1.IsUsed() == true {
|
|
||||||
t.Fatalf("Bool1 was not used!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 = bf.AddString("str1", "HI")
|
|
||||||
bf.Args = []string{"--str1"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err == nil {
|
|
||||||
t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 = bf.AddString("str1", "HI")
|
|
||||||
bf.Args = []string{"--str1="}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected = ""
|
|
||||||
if flStr1.Value != expected {
|
|
||||||
t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 = bf.AddString("str1", "HI")
|
|
||||||
bf.Args = []string{"--str1=BYE"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected = "BYE"
|
|
||||||
if flStr1.Value != expected {
|
|
||||||
t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool1"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !flBool1.IsTrue() {
|
|
||||||
t.Fatalf("Test-b1 Bool1 was supposed to be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool1=true"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !flBool1.IsTrue() {
|
|
||||||
t.Fatalf("Test-b2 Bool1 was supposed to be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool1=false"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flBool1.IsTrue() {
|
|
||||||
t.Fatalf("Test-b3 Bool1 was supposed to be false")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool1=false1"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err == nil {
|
|
||||||
t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool2"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err == nil {
|
|
||||||
t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
bf = NewBFlags()
|
|
||||||
flStr1 = bf.AddString("str1", "HI")
|
|
||||||
flBool1 = bf.AddBool("bool1", false)
|
|
||||||
bf.Args = []string{"--bool1", "--str1=BYE"}
|
|
||||||
|
|
||||||
if err = bf.Parse(); err != nil {
|
|
||||||
t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flStr1.Value != "BYE" {
|
|
||||||
t.Fatalf("Teset %s, str1 should be BYE", bf.Args)
|
|
||||||
}
|
|
||||||
if !flBool1.IsTrue() {
|
|
||||||
t.Fatalf("Teset %s, bool1 should be true", bf.Args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Package command contains the set of Dockerfile commands.
|
|
||||||
package command
|
|
||||||
|
|
||||||
// Define constants for the command strings
|
|
||||||
const (
|
|
||||||
Env = "env"
|
|
||||||
Label = "label"
|
|
||||||
Maintainer = "maintainer"
|
|
||||||
Add = "add"
|
|
||||||
Copy = "copy"
|
|
||||||
From = "from"
|
|
||||||
Onbuild = "onbuild"
|
|
||||||
Workdir = "workdir"
|
|
||||||
Run = "run"
|
|
||||||
Cmd = "cmd"
|
|
||||||
Entrypoint = "entrypoint"
|
|
||||||
Expose = "expose"
|
|
||||||
Volume = "volume"
|
|
||||||
User = "user"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Commands is list of all Dockerfile commands
|
|
||||||
var Commands = map[string]struct{}{
|
|
||||||
Env: {},
|
|
||||||
Label: {},
|
|
||||||
Maintainer: {},
|
|
||||||
Add: {},
|
|
||||||
Copy: {},
|
|
||||||
From: {},
|
|
||||||
Onbuild: {},
|
|
||||||
Workdir: {},
|
|
||||||
Run: {},
|
|
||||||
Cmd: {},
|
|
||||||
Entrypoint: {},
|
|
||||||
Expose: {},
|
|
||||||
Volume: {},
|
|
||||||
User: {},
|
|
||||||
}
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
// This file contains the dispatchers for each command. Note that
|
|
||||||
// `nullDispatch` is not actually a command, but support for commands we parse
|
|
||||||
// but do nothing with.
|
|
||||||
//
|
|
||||||
// See evaluator.go for a higher level discussion of the whole evaluator
|
|
||||||
// package.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/pkg/nat"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// NoBaseImageSpecifier is the symbol used by the FROM
|
|
||||||
// command to specify that no base image is to be used.
|
|
||||||
NoBaseImageSpecifier string = "scratch"
|
|
||||||
)
|
|
||||||
|
|
||||||
// dispatch with no layer / parsing. This is effectively not a command.
|
|
||||||
func nullDispatch(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENV foo bar
|
|
||||||
//
|
|
||||||
// Sets the environment variable foo to bar, also makes interpolation
|
|
||||||
// in the dockerfile available from the next statement on via ${foo}.
|
|
||||||
//
|
|
||||||
func env(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("ENV requires at least one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args)%2 != 0 {
|
|
||||||
// should never get here, but just in case
|
|
||||||
return fmt.Errorf("Bad input to ENV, too many args")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO/FIXME/NOT USED
|
|
||||||
// Just here to show how to use the builder flags stuff within the
|
|
||||||
// context of a builder command. Will remove once we actually add
|
|
||||||
// a builder command to something!
|
|
||||||
/*
|
|
||||||
flBool1 := b.BuilderFlags.AddBool("bool1", false)
|
|
||||||
flStr1 := b.BuilderFlags.AddString("str1", "HI")
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Bool1:%v\n", flBool1)
|
|
||||||
fmt.Printf("Str1:%v\n", flStr1)
|
|
||||||
*/
|
|
||||||
|
|
||||||
commitStr := "ENV"
|
|
||||||
|
|
||||||
for j := 0; j < len(args); j++ {
|
|
||||||
// name ==> args[j]
|
|
||||||
// value ==> args[j+1]
|
|
||||||
newVar := args[j] + "=" + args[j+1] + ""
|
|
||||||
commitStr += " " + newVar
|
|
||||||
|
|
||||||
gotOne := false
|
|
||||||
for i, envVar := range b.Config.Env {
|
|
||||||
envParts := strings.SplitN(envVar, "=", 2)
|
|
||||||
if envParts[0] == args[j] {
|
|
||||||
b.Config.Env[i] = newVar
|
|
||||||
gotOne = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !gotOne {
|
|
||||||
b.Config.Env = append(b.Config.Env, newVar)
|
|
||||||
}
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.commit("", b.Config.Cmd, commitStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAINTAINER some text <maybe@an.email.address>
|
|
||||||
//
|
|
||||||
// Sets the maintainer metadata.
|
|
||||||
func maintainer(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return fmt.Errorf("MAINTAINER requires exactly one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.maintainer = args[0]
|
|
||||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// LABEL some json data describing the image
|
|
||||||
//
|
|
||||||
// Sets the Label variable foo to bar,
|
|
||||||
//
|
|
||||||
func label(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("LABEL requires at least one argument")
|
|
||||||
}
|
|
||||||
if len(args)%2 != 0 {
|
|
||||||
// should never get here, but just in case
|
|
||||||
return fmt.Errorf("Bad input to LABEL, too many args")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
commitStr := "LABEL"
|
|
||||||
|
|
||||||
if b.Config.Labels == nil {
|
|
||||||
b.Config.Labels = map[string]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := 0; j < len(args); j++ {
|
|
||||||
// name ==> args[j]
|
|
||||||
// value ==> args[j+1]
|
|
||||||
newVar := args[j] + "=" + args[j+1] + ""
|
|
||||||
commitStr += " " + newVar
|
|
||||||
|
|
||||||
b.Config.Labels[args[j]] = args[j+1]
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
return b.commit("", b.Config.Cmd, commitStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ADD foo /path
|
|
||||||
//
|
|
||||||
// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
|
|
||||||
// exist here. If you do not wish to have this automatic handling, use COPY.
|
|
||||||
//
|
|
||||||
func add(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) < 2 {
|
|
||||||
return fmt.Errorf("ADD requires at least two arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.runContextCommand(args, true, true, "ADD")
|
|
||||||
}
|
|
||||||
|
|
||||||
// COPY foo /path
|
|
||||||
//
|
|
||||||
// Same as 'ADD' but without the tar and remote url handling.
|
|
||||||
//
|
|
||||||
func dispatchCopy(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) < 2 {
|
|
||||||
return fmt.Errorf("COPY requires at least two arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.runContextCommand(args, false, false, "COPY")
|
|
||||||
}
|
|
||||||
|
|
||||||
// FROM imagename
|
|
||||||
//
|
|
||||||
// This sets the image the dockerfile will build on top of.
|
|
||||||
//
|
|
||||||
func from(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return fmt.Errorf("FROM requires one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := args[0]
|
|
||||||
|
|
||||||
if name == NoBaseImageSpecifier {
|
|
||||||
b.image = ""
|
|
||||||
b.noBaseImage = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
image, err := b.Daemon.Repositories().LookupImage(name)
|
|
||||||
if b.Pull {
|
|
||||||
image, err = b.pullImage(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if b.Daemon.Graph().IsNotExist(err, name) {
|
|
||||||
image, err = b.pullImage(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// note that the top level err will still be !nil here if IsNotExist is
|
|
||||||
// not the error. This approach just simplifies the logic a bit.
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.processImageFrom(image)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ONBUILD RUN echo yo
|
|
||||||
//
|
|
||||||
// ONBUILD triggers run when the image is used in a FROM statement.
|
|
||||||
//
|
|
||||||
// ONBUILD handling has a lot of special-case functionality, the heading in
|
|
||||||
// evaluator.go and comments around dispatch() in the same file explain the
|
|
||||||
// special cases. search for 'OnBuild' in internals.go for additional special
|
|
||||||
// cases.
|
|
||||||
//
|
|
||||||
func onbuild(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("ONBUILD requires at least one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0]))
|
|
||||||
switch triggerInstruction {
|
|
||||||
case "ONBUILD":
|
|
||||||
return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
|
|
||||||
case "MAINTAINER", "FROM":
|
|
||||||
return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
|
|
||||||
}
|
|
||||||
|
|
||||||
original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "")
|
|
||||||
|
|
||||||
b.Config.OnBuild = append(b.Config.OnBuild, original)
|
|
||||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WORKDIR /tmp
|
|
||||||
//
|
|
||||||
// Set the working directory for future RUN/CMD/etc statements.
|
|
||||||
//
|
|
||||||
func workdir(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return fmt.Errorf("WORKDIR requires exactly one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that workdir passed comes from the Dockerfile. Hence it is in
|
|
||||||
// Linux format using forward-slashes, even on Windows. However,
|
|
||||||
// b.Config.WorkingDir is in platform-specific notation (in other words
|
|
||||||
// on Windows will use `\`
|
|
||||||
workdir := args[0]
|
|
||||||
|
|
||||||
isAbs := false
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// Alternate processing for Windows here is necessary as we can't call
|
|
||||||
// filepath.IsAbs(workDir) as that would verify Windows style paths,
|
|
||||||
// along with drive-letters (eg c:\pathto\file.txt). We (arguably
|
|
||||||
// correctly or not) check for both forward and back slashes as this
|
|
||||||
// is what the 1.4.2 GoLang implementation of IsAbs() does in the
|
|
||||||
// isSlash() function.
|
|
||||||
isAbs = workdir[0] == '\\' || workdir[0] == '/'
|
|
||||||
} else {
|
|
||||||
isAbs = filepath.IsAbs(workdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isAbs {
|
|
||||||
current := b.Config.WorkingDir
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// Convert to Linux format before join
|
|
||||||
current = strings.Replace(current, "\\", "/", -1)
|
|
||||||
}
|
|
||||||
// Must use path.Join so works correctly on Windows, not filepath
|
|
||||||
workdir = path.Join("/", current, workdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to platform specific format
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
workdir = strings.Replace(workdir, "/", "\\", -1)
|
|
||||||
}
|
|
||||||
b.Config.WorkingDir = workdir
|
|
||||||
|
|
||||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("WORKDIR %v", workdir))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RUN some command yo
|
|
||||||
//
|
|
||||||
// run a command and commit the image. Args are automatically prepended with
|
|
||||||
// 'sh -c' under linux or 'cmd /S /C' under Windows, in the event there is
|
|
||||||
// only one argument. The difference in processing:
|
|
||||||
//
|
|
||||||
// RUN echo hi # sh -c echo hi (Linux)
|
|
||||||
// RUN echo hi # cmd /S /C echo hi (Windows)
|
|
||||||
// RUN [ "echo", "hi" ] # echo hi
|
|
||||||
//
|
|
||||||
func run(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if b.image == "" && !b.noBaseImage {
|
|
||||||
return fmt.Errorf("Please provide a source image with `from` prior to run")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
args = handleJSONArgs(args, attributes)
|
|
||||||
|
|
||||||
if !attributes["json"] {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
args = append([]string{"/bin/sh", "-c"}, args...)
|
|
||||||
} else {
|
|
||||||
args = append([]string{"cmd", "/S /C"}, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runCmd := flag.NewFlagSet("run", flag.ContinueOnError)
|
|
||||||
runCmd.SetOutput(ioutil.Discard)
|
|
||||||
runCmd.Usage = nil
|
|
||||||
|
|
||||||
config, _, _, err := runconfig.Parse(runCmd, append([]string{b.image}, args...))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := b.Config.Cmd
|
|
||||||
// set Cmd manually, this is special case only for Dockerfiles
|
|
||||||
b.Config.Cmd = config.Cmd
|
|
||||||
runconfig.Merge(b.Config, config)
|
|
||||||
|
|
||||||
defer func(cmd *runconfig.Command) { b.Config.Cmd = cmd }(cmd)
|
|
||||||
|
|
||||||
logrus.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd)
|
|
||||||
|
|
||||||
hit, err := b.probeCache()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if hit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := b.create()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that we keep the container mounted until the commit
|
|
||||||
// to avoid unmounting and then mounting directly again
|
|
||||||
c.Mount()
|
|
||||||
defer c.Unmount()
|
|
||||||
|
|
||||||
err = b.run(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := b.commit(c.ID, cmd, "run"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CMD foo
|
|
||||||
//
|
|
||||||
// Set the default command to run in the container (which may be empty).
|
|
||||||
// Argument handling is the same as RUN.
|
|
||||||
//
|
|
||||||
func cmd(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdSlice := handleJSONArgs(args, attributes)
|
|
||||||
|
|
||||||
if !attributes["json"] {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)
|
|
||||||
} else {
|
|
||||||
cmdSlice = append([]string{"cmd", "/S /C"}, cmdSlice...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Config.Cmd = runconfig.NewCommand(cmdSlice...)
|
|
||||||
|
|
||||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != 0 {
|
|
||||||
b.cmdSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENTRYPOINT /usr/sbin/nginx
|
|
||||||
//
|
|
||||||
// Set the entrypoint (which defaults to sh -c on linux, or cmd /S /C on Windows) to
|
|
||||||
// /usr/sbin/nginx. Will accept the CMD as the arguments to /usr/sbin/nginx.
|
|
||||||
//
|
|
||||||
// Handles command processing similar to CMD and RUN, only b.Config.Entrypoint
|
|
||||||
// is initialized at NewBuilder time instead of through argument parsing.
|
|
||||||
//
|
|
||||||
func entrypoint(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed := handleJSONArgs(args, attributes)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case attributes["json"]:
|
|
||||||
// ENTRYPOINT ["echo", "hi"]
|
|
||||||
b.Config.Entrypoint = runconfig.NewEntrypoint(parsed...)
|
|
||||||
case len(parsed) == 0:
|
|
||||||
// ENTRYPOINT []
|
|
||||||
b.Config.Entrypoint = nil
|
|
||||||
default:
|
|
||||||
// ENTRYPOINT echo hi
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
b.Config.Entrypoint = runconfig.NewEntrypoint("/bin/sh", "-c", parsed[0])
|
|
||||||
} else {
|
|
||||||
b.Config.Entrypoint = runconfig.NewEntrypoint("cmd", "/S /C", parsed[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when setting the entrypoint if a CMD was not explicitly set then
|
|
||||||
// set the command to nil
|
|
||||||
if !b.cmdSet {
|
|
||||||
b.Config.Cmd = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("ENTRYPOINT %q", b.Config.Entrypoint)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPOSE 6667/tcp 7000/tcp
|
|
||||||
//
|
|
||||||
// Expose ports for links and port mappings. This all ends up in
|
|
||||||
// b.Config.ExposedPorts for runconfig.
|
|
||||||
//
|
|
||||||
func expose(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
portsTab := args
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("EXPOSE requires at least one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Config.ExposedPorts == nil {
|
|
||||||
b.Config.ExposedPorts = make(nat.PortSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
ports, _, err := nat.ParsePortSpecs(portsTab)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// instead of using ports directly, we build a list of ports and sort it so
|
|
||||||
// the order is consistent. This prevents cache burst where map ordering
|
|
||||||
// changes between builds
|
|
||||||
portList := make([]string, len(ports))
|
|
||||||
var i int
|
|
||||||
for port := range ports {
|
|
||||||
if _, exists := b.Config.ExposedPorts[port]; !exists {
|
|
||||||
b.Config.ExposedPorts[port] = struct{}{}
|
|
||||||
}
|
|
||||||
portList[i] = string(port)
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
sort.Strings(portList)
|
|
||||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("EXPOSE %s", strings.Join(portList, " ")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// USER foo
|
|
||||||
//
|
|
||||||
// Set the user to 'foo' for future commands and when running the
|
|
||||||
// ENTRYPOINT/CMD at container run time.
|
|
||||||
//
|
|
||||||
func user(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return fmt.Errorf("USER is not supported on Windows")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != 1 {
|
|
||||||
return fmt.Errorf("USER requires exactly one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Config.User = args[0]
|
|
||||||
return b.commit("", b.Config.Cmd, fmt.Sprintf("USER %v", args))
|
|
||||||
}
|
|
||||||
|
|
||||||
// VOLUME /foo
|
|
||||||
//
|
|
||||||
// Expose the volume /foo for use. Will also accept the JSON array form.
|
|
||||||
//
|
|
||||||
func volume(b *builder, args []string, attributes map[string]bool, original string) error {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return fmt.Errorf("VOLUME is not supported on Windows")
|
|
||||||
}
|
|
||||||
if len(args) == 0 {
|
|
||||||
return fmt.Errorf("VOLUME requires at least one argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.BuilderFlags.Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Config.Volumes == nil {
|
|
||||||
b.Config.Volumes = map[string]struct{}{}
|
|
||||||
}
|
|
||||||
for _, v := range args {
|
|
||||||
v = strings.TrimSpace(v)
|
|
||||||
if v == "" {
|
|
||||||
return fmt.Errorf("Volume specified can not be an empty string")
|
|
||||||
}
|
|
||||||
b.Config.Volumes[v] = struct{}{}
|
|
||||||
}
|
|
||||||
if err := b.commit("", b.Config.Cmd, fmt.Sprintf("VOLUME %v", args)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
// Package builder is the evaluation step in the Dockerfile parse/evaluate pipeline.
|
|
||||||
//
|
|
||||||
// It incorporates a dispatch table based on the parser.Node values (see the
|
|
||||||
// parser package for more information) that are yielded from the parser itself.
|
|
||||||
// Calling NewBuilder with the BuildOpts struct can be used to customize the
|
|
||||||
// experience for execution purposes only. Parsing is controlled in the parser
|
|
||||||
// package, and this division of resposibility should be respected.
|
|
||||||
//
|
|
||||||
// Please see the jump table targets for the actual invocations, most of which
|
|
||||||
// will call out to the functions in internals.go to deal with their tasks.
|
|
||||||
//
|
|
||||||
// ONBUILD is a special case, which is covered in the onbuild() func in
|
|
||||||
// dispatchers.go.
|
|
||||||
//
|
|
||||||
// The evaluator uses the concept of "steps", which are usually each processable
|
|
||||||
// line in the Dockerfile. Each step is numbered and certain actions are taken
|
|
||||||
// before and after each step, such as creating an image ID and removing temporary
|
|
||||||
// containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which
|
|
||||||
// includes its own set of steps (usually only one of them).
|
|
||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/builder/command"
|
|
||||||
"github.com/docker/docker/builder/parser"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/daemon"
|
|
||||||
"github.com/docker/docker/pkg/fileutils"
|
|
||||||
"github.com/docker/docker/pkg/streamformatter"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/symlink"
|
|
||||||
"github.com/docker/docker/pkg/tarsum"
|
|
||||||
"github.com/docker/docker/pkg/ulimit"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
"github.com/docker/docker/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Environment variable interpolation will happen on these statements only.
|
|
||||||
var replaceEnvAllowed = map[string]struct{}{
|
|
||||||
command.Env: {},
|
|
||||||
command.Label: {},
|
|
||||||
command.Add: {},
|
|
||||||
command.Copy: {},
|
|
||||||
command.Workdir: {},
|
|
||||||
command.Expose: {},
|
|
||||||
command.Volume: {},
|
|
||||||
command.User: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
var evaluateTable map[string]func(*builder, []string, map[string]bool, string) error
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
evaluateTable = map[string]func(*builder, []string, map[string]bool, string) error{
|
|
||||||
command.Env: env,
|
|
||||||
command.Label: label,
|
|
||||||
command.Maintainer: maintainer,
|
|
||||||
command.Add: add,
|
|
||||||
command.Copy: dispatchCopy, // copy() is a go builtin
|
|
||||||
command.From: from,
|
|
||||||
command.Onbuild: onbuild,
|
|
||||||
command.Workdir: workdir,
|
|
||||||
command.Run: run,
|
|
||||||
command.Cmd: cmd,
|
|
||||||
command.Entrypoint: entrypoint,
|
|
||||||
command.Expose: expose,
|
|
||||||
command.Volume: volume,
|
|
||||||
command.User: user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// builder is an internal struct, used to maintain configuration of the Dockerfile's
|
|
||||||
// processing as it evaluates the parsing result.
|
|
||||||
type builder struct {
|
|
||||||
Daemon *daemon.Daemon
|
|
||||||
|
|
||||||
// effectively stdio for the run. Because it is not stdio, I said
|
|
||||||
// "Effectively". Do not use stdio anywhere in this package for any reason.
|
|
||||||
OutStream io.Writer
|
|
||||||
ErrStream io.Writer
|
|
||||||
|
|
||||||
Verbose bool
|
|
||||||
UtilizeCache bool
|
|
||||||
cacheBusted bool
|
|
||||||
|
|
||||||
// controls how images and containers are handled between steps.
|
|
||||||
Remove bool
|
|
||||||
ForceRemove bool
|
|
||||||
Pull bool
|
|
||||||
|
|
||||||
// set this to true if we want the builder to not commit between steps.
|
|
||||||
// This is useful when we only want to use the evaluator table to generate
|
|
||||||
// the final configs of the Dockerfile but dont want the layers
|
|
||||||
disableCommit bool
|
|
||||||
|
|
||||||
// Registry server auth configs used to pull images when handling `FROM`.
|
|
||||||
AuthConfigs map[string]cliconfig.AuthConfig
|
|
||||||
|
|
||||||
// Deprecated, original writer used for ImagePull. To be removed.
|
|
||||||
OutOld io.Writer
|
|
||||||
StreamFormatter *streamformatter.StreamFormatter
|
|
||||||
|
|
||||||
Config *runconfig.Config // runconfig for cmd, run, entrypoint etc.
|
|
||||||
|
|
||||||
// both of these are controlled by the Remove and ForceRemove options in BuildOpts
|
|
||||||
TmpContainers map[string]struct{} // a map of containers used for removes
|
|
||||||
|
|
||||||
dockerfileName string // name of Dockerfile
|
|
||||||
dockerfile *parser.Node // the syntax tree of the dockerfile
|
|
||||||
image string // image name for commit processing
|
|
||||||
maintainer string // maintainer name. could probably be removed.
|
|
||||||
cmdSet bool // indicates is CMD was set in current Dockerfile
|
|
||||||
BuilderFlags *BFlags // current cmd's BuilderFlags - temporary
|
|
||||||
context tarsum.TarSum // the context is a tarball that is uploaded by the client
|
|
||||||
contextPath string // the path of the temporary directory the local context is unpacked to (server side)
|
|
||||||
noBaseImage bool // indicates that this build does not start from any base image, but is being built from an empty file system.
|
|
||||||
|
|
||||||
// Set resource restrictions for build containers
|
|
||||||
cpuSetCpus string
|
|
||||||
cpuSetMems string
|
|
||||||
cpuShares int64
|
|
||||||
cpuPeriod int64
|
|
||||||
cpuQuota int64
|
|
||||||
cgroupParent string
|
|
||||||
memory int64
|
|
||||||
memorySwap int64
|
|
||||||
ulimits []*ulimit.Ulimit
|
|
||||||
|
|
||||||
cancelled <-chan struct{} // When closed, job was cancelled.
|
|
||||||
|
|
||||||
activeImages []string
|
|
||||||
id string // Used to hold reference images
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the builder with the context. This is the lynchpin of this package. This
|
|
||||||
// will (barring errors):
|
|
||||||
//
|
|
||||||
// * call readContext() which will set up the temporary directory and unpack
|
|
||||||
// the context into it.
|
|
||||||
// * read the dockerfile
|
|
||||||
// * parse the dockerfile
|
|
||||||
// * walk the parse tree and execute it by dispatching to handlers. If Remove
|
|
||||||
// or ForceRemove is set, additional cleanup around containers happens after
|
|
||||||
// processing.
|
|
||||||
// * Print a happy message and return the image ID.
|
|
||||||
//
|
|
||||||
func (b *builder) Run(context io.Reader) (string, error) {
|
|
||||||
if err := b.readContext(context); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := os.RemoveAll(b.contextPath); err != nil {
|
|
||||||
logrus.Debugf("[BUILDER] failed to remove temporary context: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := b.readDockerfile(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// some initializations that would not have been supplied by the caller.
|
|
||||||
b.Config = &runconfig.Config{}
|
|
||||||
|
|
||||||
b.TmpContainers = map[string]struct{}{}
|
|
||||||
|
|
||||||
for i, n := range b.dockerfile.Children {
|
|
||||||
select {
|
|
||||||
case <-b.cancelled:
|
|
||||||
logrus.Debug("Builder: build cancelled!")
|
|
||||||
fmt.Fprintf(b.OutStream, "Build cancelled")
|
|
||||||
return "", fmt.Errorf("Build cancelled")
|
|
||||||
default:
|
|
||||||
// Not cancelled yet, keep going...
|
|
||||||
}
|
|
||||||
if err := b.dispatch(i, n); err != nil {
|
|
||||||
if b.ForceRemove {
|
|
||||||
b.clearTmp()
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(b.OutStream, " ---> %s\n", stringid.TruncateID(b.image))
|
|
||||||
if b.Remove {
|
|
||||||
b.clearTmp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.image == "" {
|
|
||||||
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(b.OutStream, "Successfully built %s\n", stringid.TruncateID(b.image))
|
|
||||||
return b.image, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads a Dockerfile from the current context. It assumes that the
|
|
||||||
// 'filename' is a relative path from the root of the context
|
|
||||||
func (b *builder) readDockerfile() error {
|
|
||||||
// If no -f was specified then look for 'Dockerfile'. If we can't find
|
|
||||||
// that then look for 'dockerfile'. If neither are found then default
|
|
||||||
// back to 'Dockerfile' and use that in the error message.
|
|
||||||
if b.dockerfileName == "" {
|
|
||||||
b.dockerfileName = api.DefaultDockerfileName
|
|
||||||
tmpFN := filepath.Join(b.contextPath, api.DefaultDockerfileName)
|
|
||||||
if _, err := os.Lstat(tmpFN); err != nil {
|
|
||||||
tmpFN = filepath.Join(b.contextPath, strings.ToLower(api.DefaultDockerfileName))
|
|
||||||
if _, err := os.Lstat(tmpFN); err == nil {
|
|
||||||
b.dockerfileName = strings.ToLower(api.DefaultDockerfileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
origFile := b.dockerfileName
|
|
||||||
|
|
||||||
filename, err := symlink.FollowSymlinkInScope(filepath.Join(b.contextPath, origFile), b.contextPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("The Dockerfile (%s) must be within the build context", origFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.Lstat(filename)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("Cannot locate specified Dockerfile: %s", origFile)
|
|
||||||
}
|
|
||||||
if fi.Size() == 0 {
|
|
||||||
return fmt.Errorf("The Dockerfile (%s) cannot be empty", origFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.dockerfile, err = parser.Parse(f)
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// After the Dockerfile has been parsed, we need to check the .dockerignore
|
|
||||||
// file for either "Dockerfile" or ".dockerignore", and if either are
|
|
||||||
// present then erase them from the build context. These files should never
|
|
||||||
// have been sent from the client but we did send them to make sure that
|
|
||||||
// we had the Dockerfile to actually parse, and then we also need the
|
|
||||||
// .dockerignore file to know whether either file should be removed.
|
|
||||||
// Note that this assumes the Dockerfile has been read into memory and
|
|
||||||
// is now safe to be removed.
|
|
||||||
|
|
||||||
excludes, _ := utils.ReadDockerIgnore(filepath.Join(b.contextPath, ".dockerignore"))
|
|
||||||
if rm, _ := fileutils.Matches(".dockerignore", excludes); rm == true {
|
|
||||||
os.Remove(filepath.Join(b.contextPath, ".dockerignore"))
|
|
||||||
b.context.(tarsum.BuilderContext).Remove(".dockerignore")
|
|
||||||
}
|
|
||||||
if rm, _ := fileutils.Matches(b.dockerfileName, excludes); rm == true {
|
|
||||||
os.Remove(filepath.Join(b.contextPath, b.dockerfileName))
|
|
||||||
b.context.(tarsum.BuilderContext).Remove(b.dockerfileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is the entrypoint to all statement handling routines.
|
|
||||||
//
|
|
||||||
// Almost all nodes will have this structure:
|
|
||||||
// Child[Node, Node, Node] where Child is from parser.Node.Children and each
|
|
||||||
// node comes from parser.Node.Next. This forms a "line" with a statement and
|
|
||||||
// arguments and we process them in this normalized form by hitting
|
|
||||||
// evaluateTable with the leaf nodes of the command and the Builder object.
|
|
||||||
//
|
|
||||||
// ONBUILD is a special case; in this case the parser will emit:
|
|
||||||
// Child[Node, Child[Node, Node...]] where the first node is the literal
|
|
||||||
// "onbuild" and the child entrypoint is the command of the ONBUILD statmeent,
|
|
||||||
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
|
|
||||||
// deal with that, at least until it becomes more of a general concern with new
|
|
||||||
// features.
|
|
||||||
func (b *builder) dispatch(stepN int, ast *parser.Node) error {
|
|
||||||
cmd := ast.Value
|
|
||||||
attrs := ast.Attributes
|
|
||||||
original := ast.Original
|
|
||||||
flags := ast.Flags
|
|
||||||
strs := []string{}
|
|
||||||
msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd))
|
|
||||||
|
|
||||||
if len(ast.Flags) > 0 {
|
|
||||||
msg += " " + strings.Join(ast.Flags, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd == "onbuild" {
|
|
||||||
if ast.Next == nil {
|
|
||||||
return fmt.Errorf("ONBUILD requires at least one argument")
|
|
||||||
}
|
|
||||||
ast = ast.Next.Children[0]
|
|
||||||
strs = append(strs, ast.Value)
|
|
||||||
msg += " " + ast.Value
|
|
||||||
|
|
||||||
if len(ast.Flags) > 0 {
|
|
||||||
msg += " " + strings.Join(ast.Flags, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// count the number of nodes that we are going to traverse first
|
|
||||||
// so we can pre-create the argument and message array. This speeds up the
|
|
||||||
// allocation of those list a lot when they have a lot of arguments
|
|
||||||
cursor := ast
|
|
||||||
var n int
|
|
||||||
for cursor.Next != nil {
|
|
||||||
cursor = cursor.Next
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
l := len(strs)
|
|
||||||
strList := make([]string, n+l)
|
|
||||||
copy(strList, strs)
|
|
||||||
msgList := make([]string, n)
|
|
||||||
|
|
||||||
var i int
|
|
||||||
for ast.Next != nil {
|
|
||||||
ast = ast.Next
|
|
||||||
var str string
|
|
||||||
str = ast.Value
|
|
||||||
if _, ok := replaceEnvAllowed[cmd]; ok {
|
|
||||||
var err error
|
|
||||||
str, err = ProcessWord(ast.Value, b.Config.Env)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strList[i+l] = str
|
|
||||||
msgList[i] = ast.Value
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
msg += " " + strings.Join(msgList, " ")
|
|
||||||
fmt.Fprintln(b.OutStream, msg)
|
|
||||||
|
|
||||||
// XXX yes, we skip any cmds that are not valid; the parser should have
|
|
||||||
// picked these out already.
|
|
||||||
if f, ok := evaluateTable[cmd]; ok {
|
|
||||||
b.BuilderFlags = NewBFlags()
|
|
||||||
b.BuilderFlags.Args = flags
|
|
||||||
return f(b, strList, attrs, original)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Unknown instruction: %s", strings.ToUpper(cmd))
|
|
||||||
}
|
|
||||||
@@ -1,808 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
// internals for handling commands. Covers many areas and a lot of
|
|
||||||
// non-contiguous functionality. Please read the comments.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/builder/parser"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/daemon"
|
|
||||||
"github.com/docker/docker/graph"
|
|
||||||
"github.com/docker/docker/image"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
|
||||||
"github.com/docker/docker/pkg/chrootarchive"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/progressreader"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/system"
|
|
||||||
"github.com/docker/docker/pkg/tarsum"
|
|
||||||
"github.com/docker/docker/pkg/urlutil"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b *builder) readContext(context io.Reader) (err error) {
|
|
||||||
tmpdirPath, err := ioutil.TempDir("", "docker-build")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we clean-up upon error. In the happy case the caller
|
|
||||||
// is expected to manage the clean-up
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
if e := os.RemoveAll(tmpdirPath); e != nil {
|
|
||||||
logrus.Debugf("[BUILDER] failed to remove temporary context: %s", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
decompressedStream, err := archive.DecompressStream(context)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.context, err = tarsum.NewTarSum(decompressedStream, true, tarsum.Version1); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = chrootarchive.Untar(b.context, tmpdirPath, nil); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b.contextPath = tmpdirPath
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) commit(id string, autoCmd *runconfig.Command, comment string) error {
|
|
||||||
if b.disableCommit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if b.image == "" && !b.noBaseImage {
|
|
||||||
return fmt.Errorf("Please provide a source image with `from` prior to commit")
|
|
||||||
}
|
|
||||||
b.Config.Image = b.image
|
|
||||||
if id == "" {
|
|
||||||
cmd := b.Config.Cmd
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
b.Config.Cmd = runconfig.NewCommand("/bin/sh", "-c", "#(nop) "+comment)
|
|
||||||
} else {
|
|
||||||
b.Config.Cmd = runconfig.NewCommand("cmd", "/S /C", "REM (nop) "+comment)
|
|
||||||
}
|
|
||||||
defer func(cmd *runconfig.Command) { b.Config.Cmd = cmd }(cmd)
|
|
||||||
|
|
||||||
hit, err := b.probeCache()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if hit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
container, err := b.create()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
id = container.ID
|
|
||||||
|
|
||||||
if err := container.Mount(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer container.Unmount()
|
|
||||||
}
|
|
||||||
container, err := b.Daemon.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Actually copy the struct
|
|
||||||
autoConfig := *b.Config
|
|
||||||
autoConfig.Cmd = autoCmd
|
|
||||||
|
|
||||||
commitCfg := &daemon.ContainerCommitConfig{
|
|
||||||
Author: b.maintainer,
|
|
||||||
Pause: true,
|
|
||||||
Config: &autoConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the container
|
|
||||||
image, err := b.Daemon.Commit(container, commitCfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.Daemon.Graph().Retain(b.id, image.ID)
|
|
||||||
b.activeImages = append(b.activeImages, image.ID)
|
|
||||||
b.image = image.ID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type copyInfo struct {
|
|
||||||
origPath string
|
|
||||||
destPath string
|
|
||||||
hash string
|
|
||||||
decompress bool
|
|
||||||
tmpDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) runContextCommand(args []string, allowRemote bool, allowDecompression bool, cmdName string) error {
|
|
||||||
if b.context == nil {
|
|
||||||
return fmt.Errorf("No context given. Impossible to use %s", cmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) < 2 {
|
|
||||||
return fmt.Errorf("Invalid %s format - at least two arguments required", cmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work in daemon-specific filepath semantics
|
|
||||||
dest := filepath.FromSlash(args[len(args)-1]) // last one is always the dest
|
|
||||||
|
|
||||||
copyInfos := []*copyInfo{}
|
|
||||||
|
|
||||||
b.Config.Image = b.image
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, ci := range copyInfos {
|
|
||||||
if ci.tmpDir != "" {
|
|
||||||
os.RemoveAll(ci.tmpDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Loop through each src file and calculate the info we need to
|
|
||||||
// do the copy (e.g. hash value if cached). Don't actually do
|
|
||||||
// the copy until we've looked at all src files
|
|
||||||
for _, orig := range args[0 : len(args)-1] {
|
|
||||||
if err := calcCopyInfo(
|
|
||||||
b,
|
|
||||||
cmdName,
|
|
||||||
©Infos,
|
|
||||||
orig,
|
|
||||||
dest,
|
|
||||||
allowRemote,
|
|
||||||
allowDecompression,
|
|
||||||
true,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(copyInfos) == 0 {
|
|
||||||
return fmt.Errorf("No source files were specified")
|
|
||||||
}
|
|
||||||
if len(copyInfos) > 1 && !strings.HasSuffix(dest, string(os.PathSeparator)) {
|
|
||||||
return fmt.Errorf("When using %s with more than one source file, the destination must be a directory and end with a /", cmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For backwards compat, if there's just one CI then use it as the
|
|
||||||
// cache look-up string, otherwise hash 'em all into one
|
|
||||||
var srcHash string
|
|
||||||
var origPaths string
|
|
||||||
|
|
||||||
if len(copyInfos) == 1 {
|
|
||||||
srcHash = copyInfos[0].hash
|
|
||||||
origPaths = copyInfos[0].origPath
|
|
||||||
} else {
|
|
||||||
var hashs []string
|
|
||||||
var origs []string
|
|
||||||
for _, ci := range copyInfos {
|
|
||||||
hashs = append(hashs, ci.hash)
|
|
||||||
origs = append(origs, ci.origPath)
|
|
||||||
}
|
|
||||||
hasher := sha256.New()
|
|
||||||
hasher.Write([]byte(strings.Join(hashs, ",")))
|
|
||||||
srcHash = "multi:" + hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
origPaths = strings.Join(origs, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := b.Config.Cmd
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
b.Config.Cmd = runconfig.NewCommand("/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, srcHash, dest))
|
|
||||||
} else {
|
|
||||||
b.Config.Cmd = runconfig.NewCommand("cmd", "/S /C", fmt.Sprintf("REM (nop) %s %s in %s", cmdName, srcHash, dest))
|
|
||||||
}
|
|
||||||
defer func(cmd *runconfig.Command) { b.Config.Cmd = cmd }(cmd)
|
|
||||||
|
|
||||||
hit, err := b.probeCache()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if hit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
container, _, err := b.Daemon.Create(b.Config, nil, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.TmpContainers[container.ID] = struct{}{}
|
|
||||||
|
|
||||||
if err := container.Mount(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer container.Unmount()
|
|
||||||
|
|
||||||
if err := container.PrepareStorage(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ci := range copyInfos {
|
|
||||||
if err := b.addContext(container, ci.origPath, ci.destPath, ci.decompress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := container.CleanupStorage(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.commit(container.ID, cmd, fmt.Sprintf("%s %s in %s", cmdName, origPaths, dest)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcCopyInfo(b *builder, cmdName string, cInfos *[]*copyInfo, origPath string, destPath string, allowRemote bool, allowDecompression bool, allowWildcards bool) error {
|
|
||||||
|
|
||||||
// Work in daemon-specific OS filepath semantics. However, we save
|
|
||||||
// the the origPath passed in here, as it might also be a URL which
|
|
||||||
// we need to check for in this function.
|
|
||||||
passedInOrigPath := origPath
|
|
||||||
origPath = filepath.FromSlash(origPath)
|
|
||||||
destPath = filepath.FromSlash(destPath)
|
|
||||||
|
|
||||||
if origPath != "" && origPath[0] == os.PathSeparator && len(origPath) > 1 {
|
|
||||||
origPath = origPath[1:]
|
|
||||||
}
|
|
||||||
origPath = strings.TrimPrefix(origPath, "."+string(os.PathSeparator))
|
|
||||||
|
|
||||||
// Twiddle the destPath when its a relative path - meaning, make it
|
|
||||||
// relative to the WORKINGDIR
|
|
||||||
if !filepath.IsAbs(destPath) {
|
|
||||||
hasSlash := strings.HasSuffix(destPath, string(os.PathSeparator))
|
|
||||||
destPath = filepath.Join(string(os.PathSeparator), filepath.FromSlash(b.Config.WorkingDir), destPath)
|
|
||||||
|
|
||||||
// Make sure we preserve any trailing slash
|
|
||||||
if hasSlash {
|
|
||||||
destPath += string(os.PathSeparator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the remote/URL case, download it and gen its hashcode
|
|
||||||
if urlutil.IsURL(passedInOrigPath) {
|
|
||||||
|
|
||||||
// As it's a URL, we go back to processing on what was passed in
|
|
||||||
// to this function
|
|
||||||
origPath = passedInOrigPath
|
|
||||||
|
|
||||||
if !allowRemote {
|
|
||||||
return fmt.Errorf("Source can't be a URL for %s", cmdName)
|
|
||||||
}
|
|
||||||
|
|
||||||
ci := copyInfo{}
|
|
||||||
ci.origPath = origPath
|
|
||||||
ci.hash = origPath // default to this but can change
|
|
||||||
ci.destPath = destPath
|
|
||||||
ci.decompress = false
|
|
||||||
*cInfos = append(*cInfos, &ci)
|
|
||||||
|
|
||||||
// Initiate the download
|
|
||||||
resp, err := httputils.Download(ci.origPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a tmp dir
|
|
||||||
tmpDirName, err := ioutil.TempDir(b.contextPath, "docker-remote")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ci.tmpDir = tmpDirName
|
|
||||||
|
|
||||||
// Create a tmp file within our tmp dir
|
|
||||||
tmpFileName := filepath.Join(tmpDirName, "tmp")
|
|
||||||
tmpFile, err := os.OpenFile(tmpFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download and dump result to tmp file
|
|
||||||
if _, err := io.Copy(tmpFile, progressreader.New(progressreader.Config{
|
|
||||||
In: resp.Body,
|
|
||||||
Out: b.OutOld,
|
|
||||||
Formatter: b.StreamFormatter,
|
|
||||||
Size: int(resp.ContentLength),
|
|
||||||
NewLines: true,
|
|
||||||
ID: "",
|
|
||||||
Action: "Downloading",
|
|
||||||
})); err != nil {
|
|
||||||
tmpFile.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprintf(b.OutStream, "\n")
|
|
||||||
tmpFile.Close()
|
|
||||||
|
|
||||||
// Set the mtime to the Last-Modified header value if present
|
|
||||||
// Otherwise just remove atime and mtime
|
|
||||||
times := make([]syscall.Timespec, 2)
|
|
||||||
|
|
||||||
lastMod := resp.Header.Get("Last-Modified")
|
|
||||||
if lastMod != "" {
|
|
||||||
mTime, err := http.ParseTime(lastMod)
|
|
||||||
// If we can't parse it then just let it default to 'zero'
|
|
||||||
// otherwise use the parsed time value
|
|
||||||
if err == nil {
|
|
||||||
times[1] = syscall.NsecToTimespec(mTime.UnixNano())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := system.UtimesNano(tmpFileName, times); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ci.origPath = filepath.Join(filepath.Base(tmpDirName), filepath.Base(tmpFileName))
|
|
||||||
|
|
||||||
// If the destination is a directory, figure out the filename.
|
|
||||||
if strings.HasSuffix(ci.destPath, string(os.PathSeparator)) {
|
|
||||||
u, err := url.Parse(origPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
path := u.Path
|
|
||||||
if strings.HasSuffix(path, string(os.PathSeparator)) {
|
|
||||||
path = path[:len(path)-1]
|
|
||||||
}
|
|
||||||
parts := strings.Split(path, string(os.PathSeparator))
|
|
||||||
filename := parts[len(parts)-1]
|
|
||||||
if filename == "" {
|
|
||||||
return fmt.Errorf("cannot determine filename from url: %s", u)
|
|
||||||
}
|
|
||||||
ci.destPath = ci.destPath + filename
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calc the checksum, even if we're using the cache
|
|
||||||
r, err := archive.Tar(tmpFileName, archive.Uncompressed)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tarSum, err := tarsum.NewTarSum(r, true, tarsum.Version1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(ioutil.Discard, tarSum); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ci.hash = tarSum.Sum(nil)
|
|
||||||
r.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deal with wildcards
|
|
||||||
if allowWildcards && containsWildcards(origPath) {
|
|
||||||
for _, fileInfo := range b.context.GetSums() {
|
|
||||||
if fileInfo.Name() == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
match, _ := filepath.Match(origPath, fileInfo.Name())
|
|
||||||
if !match {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note we set allowWildcards to false in case the name has
|
|
||||||
// a * in it
|
|
||||||
calcCopyInfo(b, cmdName, cInfos, fileInfo.Name(), destPath, allowRemote, allowDecompression, false)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be a dir or a file
|
|
||||||
|
|
||||||
if err := b.checkPathForAddition(origPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fi, _ := os.Stat(filepath.Join(b.contextPath, origPath))
|
|
||||||
|
|
||||||
ci := copyInfo{}
|
|
||||||
ci.origPath = origPath
|
|
||||||
ci.hash = origPath
|
|
||||||
ci.destPath = destPath
|
|
||||||
ci.decompress = allowDecompression
|
|
||||||
*cInfos = append(*cInfos, &ci)
|
|
||||||
|
|
||||||
// Deal with the single file case
|
|
||||||
if !fi.IsDir() {
|
|
||||||
// This will match first file in sums of the archive
|
|
||||||
fis := b.context.GetSums().GetFile(ci.origPath)
|
|
||||||
if fis != nil {
|
|
||||||
ci.hash = "file:" + fis.Sum()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be a dir
|
|
||||||
var subfiles []string
|
|
||||||
absOrigPath := filepath.Join(b.contextPath, ci.origPath)
|
|
||||||
|
|
||||||
// Add a trailing / to make sure we only pick up nested files under
|
|
||||||
// the dir and not sibling files of the dir that just happen to
|
|
||||||
// start with the same chars
|
|
||||||
if !strings.HasSuffix(absOrigPath, string(os.PathSeparator)) {
|
|
||||||
absOrigPath += string(os.PathSeparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need path w/o slash too to find matching dir w/o trailing slash
|
|
||||||
absOrigPathNoSlash := absOrigPath[:len(absOrigPath)-1]
|
|
||||||
|
|
||||||
for _, fileInfo := range b.context.GetSums() {
|
|
||||||
absFile := filepath.Join(b.contextPath, fileInfo.Name())
|
|
||||||
// Any file in the context that starts with the given path will be
|
|
||||||
// picked up and its hashcode used. However, we'll exclude the
|
|
||||||
// root dir itself. We do this for a coupel of reasons:
|
|
||||||
// 1 - ADD/COPY will not copy the dir itself, just its children
|
|
||||||
// so there's no reason to include it in the hash calc
|
|
||||||
// 2 - the metadata on the dir will change when any child file
|
|
||||||
// changes. This will lead to a miss in the cache check if that
|
|
||||||
// child file is in the .dockerignore list.
|
|
||||||
if strings.HasPrefix(absFile, absOrigPath) && absFile != absOrigPathNoSlash {
|
|
||||||
subfiles = append(subfiles, fileInfo.Sum())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(subfiles)
|
|
||||||
hasher := sha256.New()
|
|
||||||
hasher.Write([]byte(strings.Join(subfiles, ",")))
|
|
||||||
ci.hash = "dir:" + hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsWildcards(name string) bool {
|
|
||||||
for i := 0; i < len(name); i++ {
|
|
||||||
ch := name[i]
|
|
||||||
if ch == '\\' {
|
|
||||||
i++
|
|
||||||
} else if ch == '*' || ch == '?' || ch == '[' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) pullImage(name string) (*image.Image, error) {
|
|
||||||
remote, tag := parsers.ParseRepositoryTag(name)
|
|
||||||
if tag == "" {
|
|
||||||
tag = "latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
pullRegistryAuth := &cliconfig.AuthConfig{}
|
|
||||||
if len(b.AuthConfigs) > 0 {
|
|
||||||
// The request came with a full auth config file, we prefer to use that
|
|
||||||
repoInfo, err := b.Daemon.RegistryService.ResolveRepository(remote)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedConfig := registry.ResolveAuthConfig(
|
|
||||||
&cliconfig.ConfigFile{AuthConfigs: b.AuthConfigs},
|
|
||||||
repoInfo.Index,
|
|
||||||
)
|
|
||||||
pullRegistryAuth = &resolvedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
imagePullConfig := &graph.ImagePullConfig{
|
|
||||||
AuthConfig: pullRegistryAuth,
|
|
||||||
OutStream: ioutils.NopWriteCloser(b.OutOld),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Daemon.Repositories().Pull(remote, tag, imagePullConfig); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
image, err := b.Daemon.Repositories().LookupImage(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return image, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) processImageFrom(img *image.Image) error {
|
|
||||||
b.image = img.ID
|
|
||||||
|
|
||||||
if img.Config != nil {
|
|
||||||
b.Config = img.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// The default path will be blank on Windows (set by HCS)
|
|
||||||
if len(b.Config.Env) == 0 && daemon.DefaultPathEnv != "" {
|
|
||||||
b.Config.Env = append(b.Config.Env, "PATH="+daemon.DefaultPathEnv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process ONBUILD triggers if they exist
|
|
||||||
if nTriggers := len(b.Config.OnBuild); nTriggers != 0 {
|
|
||||||
fmt.Fprintf(b.ErrStream, "# Executing %d build triggers\n", nTriggers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the ONBUILD triggers, and remove them from the config, since the config will be committed.
|
|
||||||
onBuildTriggers := b.Config.OnBuild
|
|
||||||
b.Config.OnBuild = []string{}
|
|
||||||
|
|
||||||
// parse the ONBUILD triggers by invoking the parser
|
|
||||||
for stepN, step := range onBuildTriggers {
|
|
||||||
ast, err := parser.Parse(strings.NewReader(step))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, n := range ast.Children {
|
|
||||||
switch strings.ToUpper(n.Value) {
|
|
||||||
case "ONBUILD":
|
|
||||||
return fmt.Errorf("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
|
|
||||||
case "MAINTAINER", "FROM":
|
|
||||||
return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", n.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(b.OutStream, "Trigger %d, %s\n", stepN, step)
|
|
||||||
|
|
||||||
if err := b.dispatch(i, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeCache checks to see if image-caching is enabled (`b.UtilizeCache`)
|
|
||||||
// and if so attempts to look up the current `b.image` and `b.Config` pair
|
|
||||||
// in the current server `b.Daemon`. If an image is found, probeCache returns
|
|
||||||
// `(true, nil)`. If no image is found, it returns `(false, nil)`. If there
|
|
||||||
// is any error, it returns `(false, err)`.
|
|
||||||
func (b *builder) probeCache() (bool, error) {
|
|
||||||
if !b.UtilizeCache || b.cacheBusted {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cache, err := b.Daemon.ImageGetCached(b.image, b.Config)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cache == nil {
|
|
||||||
logrus.Debugf("[BUILDER] Cache miss")
|
|
||||||
b.cacheBusted = true
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(b.OutStream, " ---> Using cache\n")
|
|
||||||
logrus.Debugf("[BUILDER] Use cached version")
|
|
||||||
b.image = cache.ID
|
|
||||||
b.Daemon.Graph().Retain(b.id, cache.ID)
|
|
||||||
b.activeImages = append(b.activeImages, cache.ID)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) create() (*daemon.Container, error) {
|
|
||||||
if b.image == "" && !b.noBaseImage {
|
|
||||||
return nil, fmt.Errorf("Please provide a source image with `from` prior to run")
|
|
||||||
}
|
|
||||||
b.Config.Image = b.image
|
|
||||||
|
|
||||||
hostConfig := &runconfig.HostConfig{
|
|
||||||
CpuShares: b.cpuShares,
|
|
||||||
CpuPeriod: b.cpuPeriod,
|
|
||||||
CpuQuota: b.cpuQuota,
|
|
||||||
CpusetCpus: b.cpuSetCpus,
|
|
||||||
CpusetMems: b.cpuSetMems,
|
|
||||||
CgroupParent: b.cgroupParent,
|
|
||||||
Memory: b.memory,
|
|
||||||
MemorySwap: b.memorySwap,
|
|
||||||
Ulimits: b.ulimits,
|
|
||||||
}
|
|
||||||
|
|
||||||
config := *b.Config
|
|
||||||
|
|
||||||
// Create the container
|
|
||||||
c, warnings, err := b.Daemon.Create(b.Config, hostConfig, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, warning := range warnings {
|
|
||||||
fmt.Fprintf(b.OutStream, " ---> [Warning] %s\n", warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.TmpContainers[c.ID] = struct{}{}
|
|
||||||
fmt.Fprintf(b.OutStream, " ---> Running in %s\n", stringid.TruncateID(c.ID))
|
|
||||||
|
|
||||||
if config.Cmd.Len() > 0 {
|
|
||||||
// override the entry point that may have been picked up from the base image
|
|
||||||
s := config.Cmd.Slice()
|
|
||||||
c.Path = s[0]
|
|
||||||
c.Args = s[1:]
|
|
||||||
} else {
|
|
||||||
config.Cmd = runconfig.NewCommand()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) run(c *daemon.Container) error {
|
|
||||||
var errCh chan error
|
|
||||||
if b.Verbose {
|
|
||||||
errCh = c.Attach(nil, b.OutStream, b.ErrStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
//start the container
|
|
||||||
if err := c.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
finished := make(chan struct{})
|
|
||||||
defer close(finished)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-b.cancelled:
|
|
||||||
logrus.Debugln("Build cancelled, killing container:", c.ID)
|
|
||||||
c.Kill()
|
|
||||||
case <-finished:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if b.Verbose {
|
|
||||||
// Block on reading output from container, stop on err or chan closed
|
|
||||||
if err := <-errCh; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for it to finish
|
|
||||||
if ret, _ := c.WaitStop(-1 * time.Second); ret != 0 {
|
|
||||||
return &jsonmessage.JSONError{
|
|
||||||
Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", b.Config.Cmd.ToString(), ret),
|
|
||||||
Code: ret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) checkPathForAddition(orig string) error {
|
|
||||||
origPath := filepath.Join(b.contextPath, orig)
|
|
||||||
origPath, err := filepath.EvalSymlinks(origPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("%s: no such file or directory", orig)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
contextPath, err := filepath.EvalSymlinks(b.contextPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(origPath, contextPath) {
|
|
||||||
return fmt.Errorf("Forbidden path outside the build context: %s (%s)", orig, origPath)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(origPath); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("%s: no such file or directory", orig)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) addContext(container *daemon.Container, orig, dest string, decompress bool) error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
destExists = true
|
|
||||||
origPath = filepath.Join(b.contextPath, orig)
|
|
||||||
destPath string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Work in daemon-local OS specific file paths
|
|
||||||
dest = filepath.FromSlash(dest)
|
|
||||||
|
|
||||||
destPath, err = container.GetResourcePath(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve the trailing slash
|
|
||||||
if strings.HasSuffix(dest, string(os.PathSeparator)) || dest == "." {
|
|
||||||
destPath = destPath + string(os.PathSeparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
destStat, err := os.Stat(destPath)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
destExists = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.Stat(origPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("%s: no such file or directory", orig)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if fi.IsDir() {
|
|
||||||
return copyAsDirectory(origPath, destPath, destExists)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are adding a remote file (or we've been told not to decompress), do not try to untar it
|
|
||||||
if decompress {
|
|
||||||
// First try to unpack the source as an archive
|
|
||||||
// to support the untar feature we need to clean up the path a little bit
|
|
||||||
// because tar is very forgiving. First we need to strip off the archive's
|
|
||||||
// filename from the path but this is only added if it does not end in slash
|
|
||||||
tarDest := destPath
|
|
||||||
if strings.HasSuffix(tarDest, string(os.PathSeparator)) {
|
|
||||||
tarDest = filepath.Dir(destPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to successfully untar the orig
|
|
||||||
if err := chrootarchive.UntarPath(origPath, tarDest); err == nil {
|
|
||||||
return nil
|
|
||||||
} else if err != io.EOF {
|
|
||||||
logrus.Debugf("Couldn't untar %s to %s: %s", origPath, tarDest, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := system.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := chrootarchive.CopyWithTar(origPath, destPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resPath := destPath
|
|
||||||
if destExists && destStat.IsDir() {
|
|
||||||
resPath = filepath.Join(destPath, filepath.Base(origPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixPermissions(origPath, resPath, 0, 0, destExists)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyAsDirectory(source, destination string, destExisted bool) error {
|
|
||||||
if err := chrootarchive.CopyWithTar(source, destination); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fixPermissions(source, destination, 0, 0, destExisted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *builder) clearTmp() {
|
|
||||||
for c := range b.TmpContainers {
|
|
||||||
rmConfig := &daemon.ContainerRmConfig{
|
|
||||||
ForceRemove: true,
|
|
||||||
RemoveVolume: true,
|
|
||||||
}
|
|
||||||
if err := b.Daemon.ContainerRm(c, rmConfig); err != nil {
|
|
||||||
fmt.Fprintf(b.OutStream, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(b.TmpContainers, c)
|
|
||||||
fmt.Fprintf(b.OutStream, "Removing intermediate container %s\n", stringid.TruncateID(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// +build linux
|
|
||||||
|
|
||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
|
||||||
// If the destination didn't already exist, or the destination isn't a
|
|
||||||
// directory, then we should Lchown the destination. Otherwise, we shouldn't
|
|
||||||
// Lchown the destination.
|
|
||||||
destStat, err := os.Stat(destination)
|
|
||||||
if err != nil {
|
|
||||||
// This should *never* be reached, because the destination must've already
|
|
||||||
// been created while untar-ing the context.
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
doChownDestination := !destExisted || !destStat.IsDir()
|
|
||||||
|
|
||||||
// We Walk on the source rather than on the destination because we don't
|
|
||||||
// want to change permissions on things we haven't created or modified.
|
|
||||||
return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error {
|
|
||||||
// Do not alter the walk root iff. it existed before, as it doesn't fall under
|
|
||||||
// the domain of "things we should chown".
|
|
||||||
if !doChownDestination && (source == fullpath) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path is prefixed by source: substitute with destination instead.
|
|
||||||
cleaned, err := filepath.Rel(source, fullpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fullpath = filepath.Join(destination, cleaned)
|
|
||||||
return os.Lchown(fullpath, uid, gid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// +build windows
|
|
||||||
|
|
||||||
package builder
|
|
||||||
|
|
||||||
func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
|
||||||
// chown is not supported on Windows
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-355
@@ -1,355 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api"
|
|
||||||
"github.com/docker/docker/builder/parser"
|
|
||||||
"github.com/docker/docker/cliconfig"
|
|
||||||
"github.com/docker/docker/daemon"
|
|
||||||
"github.com/docker/docker/graph/tags"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
"github.com/docker/docker/pkg/parsers"
|
|
||||||
"github.com/docker/docker/pkg/progressreader"
|
|
||||||
"github.com/docker/docker/pkg/streamformatter"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/ulimit"
|
|
||||||
"github.com/docker/docker/pkg/urlutil"
|
|
||||||
"github.com/docker/docker/registry"
|
|
||||||
"github.com/docker/docker/runconfig"
|
|
||||||
"github.com/docker/docker/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// When downloading remote contexts, limit the amount (in bytes)
|
|
||||||
// to be read from the response body in order to detect its Content-Type
|
|
||||||
const maxPreambleLength = 100
|
|
||||||
|
|
||||||
// whitelist of commands allowed for a commit/import
|
|
||||||
var validCommitCommands = map[string]bool{
|
|
||||||
"cmd": true,
|
|
||||||
"entrypoint": true,
|
|
||||||
"env": true,
|
|
||||||
"expose": true,
|
|
||||||
"label": true,
|
|
||||||
"onbuild": true,
|
|
||||||
"user": true,
|
|
||||||
"volume": true,
|
|
||||||
"workdir": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config contains all configs for a build job
|
|
||||||
type Config struct {
|
|
||||||
DockerfileName string
|
|
||||||
RemoteURL string
|
|
||||||
RepoName string
|
|
||||||
SuppressOutput bool
|
|
||||||
NoCache bool
|
|
||||||
Remove bool
|
|
||||||
ForceRemove bool
|
|
||||||
Pull bool
|
|
||||||
Memory int64
|
|
||||||
MemorySwap int64
|
|
||||||
CPUShares int64
|
|
||||||
CPUPeriod int64
|
|
||||||
CPUQuota int64
|
|
||||||
CPUSetCpus string
|
|
||||||
CPUSetMems string
|
|
||||||
CgroupParent string
|
|
||||||
Ulimits []*ulimit.Ulimit
|
|
||||||
AuthConfigs map[string]cliconfig.AuthConfig
|
|
||||||
|
|
||||||
Stdout io.Writer
|
|
||||||
Context io.ReadCloser
|
|
||||||
// When closed, the job has been cancelled.
|
|
||||||
// Note: not all jobs implement cancellation.
|
|
||||||
// See Job.Cancel() and Job.WaitCancelled()
|
|
||||||
cancelled chan struct{}
|
|
||||||
cancelOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel signals the build job to cancel
|
|
||||||
func (b *Config) Cancel() {
|
|
||||||
b.cancelOnce.Do(func() {
|
|
||||||
close(b.cancelled)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitCancelled returns a channel which is closed ("never blocks") when
|
|
||||||
// the job is cancelled.
|
|
||||||
func (b *Config) WaitCancelled() <-chan struct{} {
|
|
||||||
return b.cancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBuildConfig returns a new Config struct
|
|
||||||
func NewBuildConfig() *Config {
|
|
||||||
return &Config{
|
|
||||||
AuthConfigs: map[string]cliconfig.AuthConfig{},
|
|
||||||
cancelled: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build is the main interface of the package, it gathers the Builder
|
|
||||||
// struct and calls builder.Run() to do all the real build job.
|
|
||||||
func Build(d *daemon.Daemon, buildConfig *Config) error {
|
|
||||||
var (
|
|
||||||
repoName string
|
|
||||||
tag string
|
|
||||||
context io.ReadCloser
|
|
||||||
)
|
|
||||||
sf := streamformatter.NewJSONStreamFormatter()
|
|
||||||
|
|
||||||
repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName)
|
|
||||||
if repoName != "" {
|
|
||||||
if err := registry.ValidateRepositoryName(repoName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(tag) > 0 {
|
|
||||||
if err := tags.ValidateTagName(tag); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildConfig.RemoteURL == "" {
|
|
||||||
context = ioutil.NopCloser(buildConfig.Context)
|
|
||||||
} else if urlutil.IsGitURL(buildConfig.RemoteURL) {
|
|
||||||
root, err := utils.GitClone(buildConfig.RemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
c, err := archive.Tar(root, archive.Uncompressed)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
context = c
|
|
||||||
} else if urlutil.IsURL(buildConfig.RemoteURL) {
|
|
||||||
f, err := httputils.Download(buildConfig.RemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error downloading remote context %s: %v", buildConfig.RemoteURL, err)
|
|
||||||
}
|
|
||||||
defer f.Body.Close()
|
|
||||||
ct := f.Header.Get("Content-Type")
|
|
||||||
clen := int(f.ContentLength)
|
|
||||||
contentType, bodyReader, err := inspectResponse(ct, f.Body, clen)
|
|
||||||
|
|
||||||
defer bodyReader.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error detecting content type for remote %s: %v", buildConfig.RemoteURL, err)
|
|
||||||
}
|
|
||||||
if contentType == httputils.MimeTypes.TextPlain {
|
|
||||||
dockerFile, err := ioutil.ReadAll(bodyReader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we're downloading just a Dockerfile put it in
|
|
||||||
// the default name - don't allow the client to move/specify it
|
|
||||||
buildConfig.DockerfileName = api.DefaultDockerfileName
|
|
||||||
|
|
||||||
c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
context = c
|
|
||||||
} else {
|
|
||||||
// Pass through - this is a pre-packaged context, presumably
|
|
||||||
// with a Dockerfile with the right name inside it.
|
|
||||||
prCfg := progressreader.Config{
|
|
||||||
In: bodyReader,
|
|
||||||
Out: buildConfig.Stdout,
|
|
||||||
Formatter: sf,
|
|
||||||
Size: clen,
|
|
||||||
NewLines: true,
|
|
||||||
ID: "Downloading context",
|
|
||||||
Action: buildConfig.RemoteURL,
|
|
||||||
}
|
|
||||||
context = progressreader.New(prCfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defer context.Close()
|
|
||||||
|
|
||||||
builder := &builder{
|
|
||||||
Daemon: d,
|
|
||||||
OutStream: &streamformatter.StdoutFormater{
|
|
||||||
Writer: buildConfig.Stdout,
|
|
||||||
StreamFormatter: sf,
|
|
||||||
},
|
|
||||||
ErrStream: &streamformatter.StderrFormater{
|
|
||||||
Writer: buildConfig.Stdout,
|
|
||||||
StreamFormatter: sf,
|
|
||||||
},
|
|
||||||
Verbose: !buildConfig.SuppressOutput,
|
|
||||||
UtilizeCache: !buildConfig.NoCache,
|
|
||||||
Remove: buildConfig.Remove,
|
|
||||||
ForceRemove: buildConfig.ForceRemove,
|
|
||||||
Pull: buildConfig.Pull,
|
|
||||||
OutOld: buildConfig.Stdout,
|
|
||||||
StreamFormatter: sf,
|
|
||||||
AuthConfigs: buildConfig.AuthConfigs,
|
|
||||||
dockerfileName: buildConfig.DockerfileName,
|
|
||||||
cpuShares: buildConfig.CPUShares,
|
|
||||||
cpuPeriod: buildConfig.CPUPeriod,
|
|
||||||
cpuQuota: buildConfig.CPUQuota,
|
|
||||||
cpuSetCpus: buildConfig.CPUSetCpus,
|
|
||||||
cpuSetMems: buildConfig.CPUSetMems,
|
|
||||||
cgroupParent: buildConfig.CgroupParent,
|
|
||||||
memory: buildConfig.Memory,
|
|
||||||
memorySwap: buildConfig.MemorySwap,
|
|
||||||
ulimits: buildConfig.Ulimits,
|
|
||||||
cancelled: buildConfig.WaitCancelled(),
|
|
||||||
id: stringid.GenerateRandomID(),
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
builder.Daemon.Graph().Release(builder.id, builder.activeImages...)
|
|
||||||
}()
|
|
||||||
|
|
||||||
id, err := builder.Run(context)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if repoName != "" {
|
|
||||||
return d.Repositories().Tag(repoName, tag, id, true)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildFromConfig will do build directly from parameter 'changes', which comes
|
|
||||||
// from Dockerfile entries, it will:
|
|
||||||
//
|
|
||||||
// - call parse.Parse() to get AST root from Dockerfile entries
|
|
||||||
// - do build by calling builder.dispatch() to call all entries' handling routines
|
|
||||||
func BuildFromConfig(d *daemon.Daemon, c *runconfig.Config, changes []string) (*runconfig.Config, error) {
|
|
||||||
ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that the commands are valid
|
|
||||||
for _, n := range ast.Children {
|
|
||||||
if !validCommitCommands[n.Value] {
|
|
||||||
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder := &builder{
|
|
||||||
Daemon: d,
|
|
||||||
Config: c,
|
|
||||||
OutStream: ioutil.Discard,
|
|
||||||
ErrStream: ioutil.Discard,
|
|
||||||
disableCommit: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, n := range ast.Children {
|
|
||||||
if err := builder.dispatch(i, n); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.Config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfig contains build configs for commit operation
|
|
||||||
type CommitConfig struct {
|
|
||||||
Pause bool
|
|
||||||
Repo string
|
|
||||||
Tag string
|
|
||||||
Author string
|
|
||||||
Comment string
|
|
||||||
Changes []string
|
|
||||||
Config *runconfig.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit will create a new image from a container's changes
|
|
||||||
func Commit(name string, d *daemon.Daemon, c *CommitConfig) (string, error) {
|
|
||||||
container, err := d.Get(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Config == nil {
|
|
||||||
c.Config = &runconfig.Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig, err := BuildFromConfig(d, c.Config, c.Changes)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runconfig.Merge(newConfig, container.Config); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
commitCfg := &daemon.ContainerCommitConfig{
|
|
||||||
Pause: c.Pause,
|
|
||||||
Repo: c.Repo,
|
|
||||||
Tag: c.Tag,
|
|
||||||
Author: c.Author,
|
|
||||||
Comment: c.Comment,
|
|
||||||
Config: newConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
img, err := d.Commit(container, commitCfg)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return img.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// inspectResponse looks into the http response data at r to determine whether its
|
|
||||||
// content-type is on the list of acceptable content types for remote build contexts.
|
|
||||||
// This function returns:
|
|
||||||
// - a string representation of the detected content-type
|
|
||||||
// - an io.Reader for the response body
|
|
||||||
// - an error value which will be non-nil either when something goes wrong while
|
|
||||||
// reading bytes from r or when the detected content-type is not acceptable.
|
|
||||||
func inspectResponse(ct string, r io.ReadCloser, clen int) (string, io.ReadCloser, error) {
|
|
||||||
plen := clen
|
|
||||||
if plen <= 0 || plen > maxPreambleLength {
|
|
||||||
plen = maxPreambleLength
|
|
||||||
}
|
|
||||||
|
|
||||||
preamble := make([]byte, plen, plen)
|
|
||||||
rlen, err := r.Read(preamble)
|
|
||||||
if rlen == 0 {
|
|
||||||
return ct, r, errors.New("Empty response")
|
|
||||||
}
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return ct, r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
preambleR := bytes.NewReader(preamble)
|
|
||||||
bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
|
|
||||||
// Some web servers will use application/octet-stream as the default
|
|
||||||
// content type for files without an extension (e.g. 'Dockerfile')
|
|
||||||
// so if we receive this value we better check for text content
|
|
||||||
contentType := ct
|
|
||||||
if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
|
|
||||||
contentType, _, err = httputils.DetectContentType(preamble)
|
|
||||||
if err != nil {
|
|
||||||
return contentType, bodyReader, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType = selectAcceptableMIME(contentType)
|
|
||||||
var cterr error
|
|
||||||
if len(contentType) == 0 {
|
|
||||||
cterr = fmt.Errorf("unsupported Content-Type %q", ct)
|
|
||||||
contentType = ct
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentType, bodyReader, cterr
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var textPlainDockerfile = "FROM busybox"
|
|
||||||
var binaryContext = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00} //xz magic
|
|
||||||
|
|
||||||
func TestInspectEmptyResponse(t *testing.T) {
|
|
||||||
ct := "application/octet-stream"
|
|
||||||
br := ioutil.NopCloser(bytes.NewReader([]byte("")))
|
|
||||||
contentType, bReader, err := inspectResponse(ct, br, 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Should have generated an error for an empty response")
|
|
||||||
}
|
|
||||||
if contentType != "application/octet-stream" {
|
|
||||||
t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(bReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(body) != 0 {
|
|
||||||
t.Fatal("response body should remain empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInspectResponseBinary(t *testing.T) {
|
|
||||||
ct := "application/octet-stream"
|
|
||||||
br := ioutil.NopCloser(bytes.NewReader(binaryContext))
|
|
||||||
contentType, bReader, err := inspectResponse(ct, br, len(binaryContext))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if contentType != "application/octet-stream" {
|
|
||||||
t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(bReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(body) != len(binaryContext) {
|
|
||||||
t.Fatalf("Wrong response size %d, should be == len(binaryContext)", len(body))
|
|
||||||
}
|
|
||||||
for i := range body {
|
|
||||||
if body[i] != binaryContext[i] {
|
|
||||||
t.Fatalf("Corrupted response body at byte index %d", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResponseUnsupportedContentType(t *testing.T) {
|
|
||||||
content := []byte(textPlainDockerfile)
|
|
||||||
ct := "application/json"
|
|
||||||
br := ioutil.NopCloser(bytes.NewReader(content))
|
|
||||||
contentType, bReader, err := inspectResponse(ct, br, len(textPlainDockerfile))
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Should have returned an error on content-type 'application/json'")
|
|
||||||
}
|
|
||||||
if contentType != ct {
|
|
||||||
t.Fatalf("Should not have altered content-type: orig: %s, altered: %s", ct, contentType)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(bReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if string(body) != textPlainDockerfile {
|
|
||||||
t.Fatalf("Corrupted response body %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInspectResponseTextSimple(t *testing.T) {
|
|
||||||
content := []byte(textPlainDockerfile)
|
|
||||||
ct := "text/plain"
|
|
||||||
br := ioutil.NopCloser(bytes.NewReader(content))
|
|
||||||
contentType, bReader, err := inspectResponse(ct, br, len(content))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if contentType != "text/plain" {
|
|
||||||
t.Fatalf("Content type should be 'text/plain' but is %q", contentType)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(bReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if string(body) != textPlainDockerfile {
|
|
||||||
t.Fatalf("Corrupted response body %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInspectResponseEmptyContentType(t *testing.T) {
|
|
||||||
content := []byte(textPlainDockerfile)
|
|
||||||
br := ioutil.NopCloser(bytes.NewReader(content))
|
|
||||||
contentType, bodyReader, err := inspectResponse("", br, len(content))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if contentType != "text/plain" {
|
|
||||||
t.Fatalf("Content type should be 'text/plain' but is %q", contentType)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(bodyReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if string(body) != textPlainDockerfile {
|
|
||||||
t.Fatalf("Corrupted response body %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/docker/docker/builder/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var f *os.File
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
fmt.Println("please supply filename(s)")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fn := range os.Args[1:] {
|
|
||||||
f, err = os.Open(fn)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ast, err := parser.Parse(f)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(ast.Dump())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user