├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── include └── nkdocker.hrl ├── priv └── nkdocker.schema ├── rebar ├── rebar.config ├── src ├── nkdocker.app.src ├── nkdocker.erl ├── nkdocker_app.erl ├── nkdocker_monitor.erl ├── nkdocker_opts.erl ├── nkdocker_protocol.erl ├── nkdocker_server.erl ├── nkdocker_sup.erl ├── nkdocker_tar.erl └── nkdocker_util.erl ├── test ├── Dockerfile ├── app.config ├── basic_test.erl ├── image1.tar ├── proxy.erl └── vm.args └── util ├── shell_app.config └── shell_vm.args /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | log 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO ?= nkdocker 2 | 3 | .PHONY: deps release 4 | 5 | all: deps compile 6 | 7 | compile: 8 | ./rebar compile 9 | 10 | compile-nodeps: 11 | ./rebar compile skip_deps=true 12 | 13 | deps: 14 | ./rebar get-deps 15 | 16 | clean: 17 | ./rebar clean 18 | 19 | distclean: clean 20 | ./rebar delete-deps 21 | 22 | tests: compile eunit 23 | 24 | eunit: 25 | export ERL_FLAGS="-config test/app.config -args_file test/vm.args"; \ 26 | ./rebar eunit skip_deps=true 27 | 28 | shell: 29 | erl -config util/shell_app.config -args_file util/shell_vm.args -s nkdocker_app 30 | 31 | 32 | docs: 33 | ./rebar skip_deps=true doc 34 | 35 | 36 | APPS = kernel stdlib sasl erts ssl tools os_mon runtime_tools crypto inets \ 37 | xmerl webtool snmp public_key mnesia eunit syntax_tools compiler 38 | COMBO_PLT = $(HOME)/.$(REPO)_combo_dialyzer_plt 39 | 40 | check_plt: 41 | dialyzer --check_plt --plt $(COMBO_PLT) --apps $(APPS) deps/*/ebin 42 | 43 | build_plt: 44 | dialyzer --build_plt --output_plt $(COMBO_PLT) --apps $(APPS) deps/*/ebin 45 | 46 | dialyzer: 47 | dialyzer -Wno_return --plt $(COMBO_PLT) ebin/nkdocker*.beam #| \ 48 | # fgrep -v -f ./dialyzer.ignore-warnings 49 | 50 | cleanplt: 51 | @echo 52 | @echo "Are you sure? It takes about 1/2 hour to re-build." 53 | @echo Deleting $(COMBO_PLT) in 5 seconds. 54 | @echo 55 | sleep 5 56 | rm $(COMBO_PLT) 57 | 58 | 59 | build_tests: 60 | erlc -pa ebin -pa deps/lager/ebin -pa deps/nkpacket/ebin -o ebin -I include \ 61 | +export_all +debug_info +"{parse_transform, lager_transform}" \ 62 | test/*.erl 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NkDOCKER: Erlang Docker Client 2 | 3 | NkDOCKER is a native, 100% Erlang [Docker](https://www.docker.com) client, fully supporting the [Docker Remote API](https://docs.docker.com/reference/api/docker_remote_api_v1.18/) v1.18. Every single command and option in the standard Docker client is available. 4 | 5 | * **Full v1.18 (Docker 1.6) API supported**. 6 | * It supports bidirectional attach. 7 | * It can control any number of local or remote Docker daemons. 8 | * Events, stdin/stdout/stderr attachs, logs, etc., are sent as Erlang messages. 9 | * It can reuse existing connections to speed up the message sending. 10 | * It supports TCP and TLS transports. Unix socket transport is not supported yet. 11 | 12 | NkDOCKER needs Erlang >= 17, and it is tested on Linux and OSX (using boot2docker). The minimum required Docker server version is _1.5_ (Remote API v1.17). It is part of the Nekso software suite, but can be used stand-alone. 13 | 14 | ## Starting a connection 15 | 16 | Before sending any operation, you must connect to the Docker daemon, calling [`nkdocker:start/1`](src/nkdocker.erl) or `nkdocker:start_link/1`. NkDOCKER will start a new standard `gen_server`, returning its `pid()`. You must specify the connection options: 17 | 18 | ```erlang 19 | -type text() :: string() | binary(). 20 | 21 | -type conn_opts() :: 22 | #{ 23 | host => text(), % Default "127.0.0.1" 24 | port => inet:port_number(), % Default 2375 25 | proto => tcp | tls, % Default tcp 26 | certfile => text(), 27 | keyfile => text() 28 | }. 29 | ``` 30 | 31 | You can now start sending commands to the Docker daemon. Some commands (usually quick, non-blocking commands) will try to reuse the same command, while other commands will start a fresh connection and will close it when finished (See the documentation of each command at [nkdocker.erl](src/nkdocker.erl)). 32 | 33 | These parameters (host, port, proto, certfile and keyfile) can also be included as standard Erlang application environment variable for `nkdocker`. You can also indicate the connection parameters using standard OS environment variables. The follow keys are recognized: 34 | 35 | Key|Value 36 | ---|--- 37 | DOCKER_HOST|Host to connect to, i.e "tcp://127.0.0.1:2375" 38 | DOCKER_TLS|If "1" or "true" TLS will be used 39 | DOCKER_TLS_VERIFY|If "1" or "true" TLS will be usedta 40 | DOCKER_CERT_PATH|Path to the directory containing 'cert.pem' and 'key.pem' 41 | 42 | 43 | ## Sending commands 44 | 45 | After connection to the daemon, you can start sending commands, for example (the OS variables DOCKER_HOST, DOCKER_TLS and DOCKER_CERT must be setted for this to work): 46 | 47 | ```erlang 48 | > {ok, P} = nkdocker:start_link(). 49 | {ok, <...>} 50 | 51 | > nkdocker:version(P). 52 | {ok,#{<<"ApiVersion">> => <<"1.18">>, 53 | <<"Arch">> => <<"amd64">>, 54 | <<"GitCommit">> => <<"4749651">>, 55 | <<"GoVersion">> => <<"go1.4.2">>, 56 | <<"KernelVersion">> => <<"3.18.11-tinycore64">>, 57 | <<"Os">> => <<"linux">>, 58 | <<"Version">> => <<"1.6.0">>}} 59 | ``` 60 | 61 | Let's create a new container: 62 | 63 | ```erlang 64 | nkdocker:create(P, "busybox:latest", 65 | #{ 66 | name => "nkdocker1", 67 | interactive => true, 68 | tty => true, 69 | cmds => ["/bin/sh"] 70 | }). 71 | {ok, #{<<"Id">> => ...}} 72 | ``` 73 | 74 | ## Async comamnds 75 | 76 | NkPACKET allows several async commands, for example to get Docker events: 77 | 78 | ```erlang 79 | > nkdocker:events(P). 80 | {ok, #Ref<0.0.3.103165>} 81 | ``` 82 | 83 | Now every selected event (all for this example) will be sent to the process: 84 | 85 | ```erlang 86 | > nkdocker:start(P, "nkdocker1"). 87 | ok 88 | 89 | > flush(). 90 | Shell got {nkdocker,#Ref<0.0.3.103165>, 91 | #{<<"from">> => <<"busybox:latest">>, 92 | <<"id">> => ..., 93 | <<"status">> => <<"start">>, ... }} 94 | 95 | ``` 96 | 97 | See [nkdocker.erl](nkdocker.erl) to find all available commands. 98 | 99 | 100 | ## Docker Monitor 101 | 102 | Its is possible to start a long-running docker client calling nkdocker_monitor:register/1,2. 103 | The server will be started with the first registration, and removed after the last one is unregistered. 104 | 105 | All events from docker will be sent to the callback module, as well as stats and other specific information. 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /include/nkdocker.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -ifndef(NKDOCKER_HRL_). 22 | -define(NKDOCKER_HRL_, 1). 23 | 24 | %% =================================================================== 25 | %% Defines 26 | %% =================================================================== 27 | 28 | 29 | 30 | %% =================================================================== 31 | %% Records 32 | %% =================================================================== 33 | 34 | 35 | 36 | -endif. 37 | 38 | -------------------------------------------------------------------------------- /priv/nkdocker.schema: -------------------------------------------------------------------------------- 1 | 2 | %% @doc Default Docker daemon host 3 | {mapping, "docker.host", "nkdocker.host", [ 4 | {default, "127.0.0.1"}, 5 | {datatype, string} 6 | ]}. 7 | 8 | 9 | %% @doc Default Docker daemon protocol 10 | {mapping, "docker.proto", "nkdocker.proto", [ 11 | {default, tcp}, 12 | {datatype, {enum, [tcp, tls]}} 13 | ]}. 14 | 15 | 16 | %% @doc Default Docker daemon port 17 | {mapping, "docker.port", "nkdocker.port", [ 18 | {default, 2375}, 19 | {datatype, integer} 20 | ]}. 21 | 22 | 23 | %% @doc Default Docker certfile location 24 | {mapping, "docker.certfile", "nkdocker.certfile", [ 25 | {datatype, file}, 26 | {commented, "{{platform_etc_dir}}/cert.pem"} 27 | ]}. 28 | 29 | 30 | %% @doc Default Docker keyfile location 31 | {mapping, "docker.keyfile", "nkdocker.keyfile", [ 32 | {datatype, file}, 33 | {commented, "{{platform_etc_dir}}/key.pem"} 34 | ]}. 35 | 36 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetComposer/nkdocker/1cdb22beaa4bad5eded3ebddd6256caa062c8a97/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {cover_enabled, true}. 2 | 3 | {erl_opts, [ 4 | debug_info, 5 | fail_on_warning, 6 | {parse_transform, lager_transform} 7 | ]}. 8 | 9 | {deps, [ 10 | {nkpacket, ".*", {git, "http://github.com/nekso/nkpacket.git", {tag, "v0.6.0"}}} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/nkdocker.app.src: -------------------------------------------------------------------------------- 1 | {application, nkdocker, [ 2 | {description, "NkDOCKER Docker Interface"}, 3 | {vsn, "v0.118.3"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {mod, {nkdocker_app, []}}, 7 | {applications, [ 8 | kernel, 9 | stdlib, 10 | crypto, 11 | sasl, 12 | ssl, 13 | lager, 14 | nkpacket 15 | ]}, 16 | {env, [ 17 | ]} 18 | ]}. 19 | -------------------------------------------------------------------------------- /src/nkdocker.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Main management module. 22 | -module(nkdocker). 23 | -author('Carlos Gonzalez '). 24 | 25 | -export_type([conn_opts/0, create_opts/0, async_msg/0]). 26 | 27 | -include_lib("nklib/include/nklib.hrl"). 28 | -include_lib("nkpacket/include/nkpacket.hrl"). 29 | -include("nkdocker.hrl"). 30 | 31 | -export([start_link/0, start_link/1, start/0, start/1, stop/1, finish_async/2]). 32 | -export([version/1, info/1, ping/1, events/1, events/2, login/4, login/5]). 33 | -export([attach/2, attach/3, attach_send/3, commit/2, commit/3, cp/4, create/3, diff/2, 34 | export/3, inspect/2, kill/2, kill/3, logs/2, logs/3, pause/2, ps/1, ps/2, 35 | rename/3, restart/2, restart/3, rm/2, rm/3, start/2, resize/4, 36 | stats/2, stop/2, stop/3, top/2, top/3, unpause/2, wait/2, wait/3]). 37 | -export([images/1, images/2, build/2, build/3, create_image/2, history/2, push/3, 38 | tag/3, inspect_image/2, search/2, rmi/2, rmi/3, get_image/3, get_images/3, 39 | load/2]). 40 | -export([exec_create/3, exec_create/4, exec_start/2, exec_start/3, exec_inspect/2, 41 | exec_resize/4]). 42 | -import(nklib_util, [to_binary/1]). 43 | 44 | -define(HUB, <<"https://index.docker.io/v1/">>). 45 | 46 | 47 | %% =================================================================== 48 | %% Types 49 | %% =================================================================== 50 | 51 | -type text() :: string() | binary(). 52 | 53 | -type conn_opts() :: 54 | #{ 55 | host => text(), % Default "127.0.0.1" 56 | port => inet:port_number(), % Default 2375 57 | proto => tcp | tls, % Default tcp 58 | ?TLS_TYPES 59 | }. 60 | 61 | -type docker_device() :: 62 | text() | % Host 63 | {text(), text()} | % Host, Container 64 | {text(), text(), text()}. % Host, Container, Perm 65 | 66 | -type docker_port() :: inet:port_number() | {inet:port_number(), tcp|udp}. 67 | 68 | -type docker_publish() :: 69 | docker_port() | % Container 70 | {docker_port(), inet:port_number()} | % Container, Host 71 | {docker_port(), inet:port_number(), inet:ip_address()}. % Container, Host, Ip 72 | 73 | 74 | -type create_opts() :: 75 | #{ 76 | attach => [stdin | stdout | stderr], 77 | add_hosts => [{Host::text(), Ip::inet:ip_address()}], 78 | cap_add => [text()], 79 | cap_drop => [text()], 80 | cgroup_parent => text(), % New in 1.18 81 | cidfile => text(), 82 | cmds => [text()], 83 | cpu_set => text(), % Deprecated 84 | cpu_set_cpus => text(), % New in 1.18 85 | cpu_shares => pos_integer(), 86 | devices => [docker_device()], 87 | dns => [text()], 88 | dns_search => [text()], 89 | domain_name => text(), 90 | env => [{text(), text()}], 91 | entrypoints => [text()], 92 | expose => [docker_port()], 93 | hostname => text(), 94 | interactive => boolean(), 95 | labels => [{Key::text(), Val::text()}], 96 | links => [{Cont::text(), Alias::text()}], 97 | lxc_confs => [{text(), text()}], 98 | log_config => Text::text() | {Type::text(), Config::map()}, % New in 1.18 99 | mac_address => text(), 100 | memory => pos_integer(), 101 | memory_swap => -1 | pos_integer(), 102 | net => none | bridge | host | text(), 103 | publish_all => boolean(), 104 | publish => [docker_publish()], 105 | % pid_mode => text(), 106 | privileged => boolean(), 107 | read_only => boolean(), 108 | restart => always | on_failure | {on_failure, integer()}, 109 | security_opts => [text()], 110 | tty => boolean(), 111 | ulimits => [{Name::text(), Soft::integer(), Hard::integer()}], % New in 1.18 112 | user => text(), 113 | volumes => [ 114 | Cont::text() | {Host::text(), Cont::text()} | {Host::text(), Cont::text(), ro} 115 | ], 116 | volumes_from => [text() | {text(), ro|rw}], 117 | workdir => text() 118 | }. 119 | 120 | 121 | -type error() :: 122 | {not_modified | bad_parameter | not_found | not_running | conflict | 123 | server_error | pos_integer(), binary()}. 124 | 125 | -type async_msg() :: 126 | {nkdocker, reference(), ok | {error, term()} | {data, term()}}. 127 | 128 | -type event_type() :: 129 | create | destroy | die | exec_create | exec_start | export | kill | oom | 130 | pause | restart | start | stop | unpause | untag | delete. 131 | 132 | 133 | %% =================================================================== 134 | %% Server functions 135 | %% =================================================================== 136 | 137 | 138 | %% @doc Starts and links new docker connection 139 | -spec start_link() -> 140 | {ok, pid()} | {error, term()}. 141 | 142 | start_link() -> 143 | start_link(#{}). 144 | 145 | 146 | %% @doc Starts and links new docker connection 147 | -spec start_link(conn_opts()) -> 148 | {ok, pid()} | {error, term()}. 149 | 150 | start_link(Opts) -> 151 | nkdocker_server:start_link(Opts). 152 | 153 | 154 | %% @doc Starts a new docker connection 155 | -spec start() -> 156 | {ok, pid()} | {error, term()}. 157 | 158 | start() -> 159 | start(#{}). 160 | 161 | 162 | %% @doc Starts a new docker connection 163 | -spec start(conn_opts()) -> 164 | {ok, pid()} | {error, term()}. 165 | 166 | start(Opts) -> 167 | nkdocker_server:start(Opts). 168 | 169 | 170 | %% @doc Stops a docker connection 171 | -spec stop(pid()) -> 172 | ok. 173 | 174 | stop(Pid) -> 175 | nkdocker_server:stop(Pid). 176 | 177 | 178 | %% @doc Finishes an asynchronous command (see logs/3) 179 | -spec finish_async(pid(), reference()) -> 180 | ok | {error, term()}. 181 | 182 | finish_async(Pid, Ref) -> 183 | nkdocker_server:finish(Pid, Ref). 184 | 185 | 186 | 187 | %% =================================================================== 188 | %% Common Docker Functions 189 | %% =================================================================== 190 | 191 | 192 | %% @doc Shows docker daemon version. 193 | %% It tries to reuse a previous connection. 194 | -spec version(pid()) -> 195 | {ok, map()} | {error, error()}. 196 | 197 | version(Pid) -> 198 | get(Pid, <<"/version">>, #{}). 199 | 200 | 201 | %% @doc Gets info about the docker daemon. 202 | %% It tries to reuse a previous connection. 203 | -spec info(pid()) -> 204 | {ok, map()} | {error, error()}. 205 | 206 | info(Pid) -> 207 | get(Pid, <<"/info">>, #{}). 208 | 209 | 210 | %% @doc Pings to the the docker daemon. 211 | %% It tries to reuse a previous connection. 212 | -spec ping(pid()) -> 213 | ok | {error, error()}. 214 | 215 | ping(Pid) -> 216 | case get(Pid, <<"/_ping">>, #{}) of 217 | {ok, <<"OK">>} -> ok; 218 | {error, Error} -> {error, Error} 219 | end. 220 | 221 | 222 | %% @doc Equivalent to events(Pid, #{}) 223 | -spec events(pid()) -> 224 | {async, reference()} | {error, error()}. 225 | 226 | events(Pid) -> 227 | events(Pid, #{}). 228 | 229 | 230 | %% @doc Subscribe to docker events 231 | %% If ok, a new referece will be returned. 232 | %% Each new docker event will be sent to the calling process as an async_msg(). 233 | %% Should the connection stop, an error mesage will be sent. 234 | %% You can use finish_async/2 to remove the subscription using the reference. 235 | %% When the calling process dies, the connection is automatically removed. 236 | -spec events(pid(), 237 | #{ 238 | filters => #{ 239 | event => [event_type()], % Receive only this event types 240 | image => text(), % Only for this image 241 | container => text() % Only for this container 242 | }, 243 | since => text(), % Since this time 244 | until => text() % Up to this time 245 | }) -> 246 | {async, reference()} | {error, error()}. 247 | 248 | events(Pid, Opts) -> 249 | Path = make_path(<<"/events">>, get_filters(Opts), [filters, since, until]), 250 | DockerOpts = #{chunks=>true, async=>true, force_new=>true, timeout=>0}, 251 | get(Pid, Path, DockerOpts). 252 | 253 | 254 | %% @doc Equivalent to login(Pid, User, Pass, Email, ?HUB) 255 | -spec login(pid(), text(), text(), text()) -> 256 | {ok, map()} | {error, error()}. 257 | 258 | login(Pid, User, Pass, Email) -> 259 | login(Pid, User, Pass, Email, ?HUB). 260 | 261 | 262 | %% @doc Logins to a repository hub to check credentials 263 | -spec login(pid(), text(), text(), text(), text()) -> 264 | ok | {error, error()}. 265 | 266 | login(Pid, User, Passwd, Email, Repo) -> 267 | Spec1 = #{ 268 | username => to_binary(User), 269 | password => to_binary(Passwd), 270 | email => to_binary(Email), 271 | serveraddress => to_binary(Repo) 272 | }, 273 | case post(Pid, <<"/auth">>, Spec1, #{force_new=>true}) of 274 | {ok, _} -> ok; 275 | {error, Error} -> {error, Error} 276 | end. 277 | 278 | 279 | 280 | %% =================================================================== 281 | %% Container Docker Functions 282 | %% =================================================================== 283 | 284 | 285 | %% @doc Equivalent to pid(Pid, #{}) 286 | -spec ps(pid()) -> 287 | {ok, [map()]} | {error, error()}. 288 | 289 | ps(Pid) -> 290 | ps(Pid, #{}). 291 | 292 | 293 | %% @doc List containers. 294 | %% It tries to reuse a previous connection. 295 | -spec ps(pid(), 296 | #{ 297 | all => boolean(), % 298 | before => text(), % Create before this Id 299 | filters => #{ 300 | status => [restarting|running|paused|exited], 301 | exited => Code::integer() 302 | }, 303 | limit => integer(), % 304 | size => boolean(), % Show sizes 305 | since => text() % Created since this Id 306 | }) -> 307 | {ok, [map()]} | {error, error()}. 308 | 309 | ps(Pid, Opts) -> 310 | UrlOpts = [all, before, filters, limit, size, since], 311 | Path = make_path(<<"/containers/json">>, get_filters(Opts), UrlOpts), 312 | get(Pid, Path, #{}). 313 | 314 | 315 | %% @doc Create a container 316 | -spec create(pid(), text(), create_opts() | #{name => text()}) -> 317 | {ok, map()} | {error, error()}. 318 | 319 | create(Pid, Image, Opts) -> 320 | Path = make_path(<<"/containers/create">>, Opts, [name]), 321 | case nkdocker_server:create_spec(Pid, Opts#{image=>Image}) of 322 | {ok, Spec} -> 323 | post(Pid, Path, Spec, #{force_new=>true}); 324 | {error, Error} -> 325 | {error, Error} 326 | end. 327 | 328 | 329 | %% @doc Inspect a container. 330 | %% It tries to reuse a previous connection. 331 | -spec inspect(pid(), text()) -> 332 | {ok, map()} | {error, error()}. 333 | 334 | inspect(Pid, Id) -> 335 | Path = list_to_binary([<<"/containers/">>, Id, <<"/json">>]), 336 | get(Pid, Path, #{}). 337 | 338 | 339 | %% @doc Equivalent to top(Pid, Container, #{}) 340 | -spec top(pid(), text()) -> 341 | {ok, map()} | {error, error()}. 342 | 343 | top(Pid, Container) -> 344 | top(Pid, Container, #{}). 345 | 346 | 347 | %% @doc List processes running inside a container. 348 | %% It tries to reuse a previous connection. 349 | -spec top(pid(), text(), #{ps_args=>text()}) -> 350 | {ok, map()} | {error, error()}. 351 | 352 | top(Pid, Container, Opts) -> 353 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/top">>]), 354 | Path2 = make_path(Path1, Opts, [ps_args]), 355 | get(Pid, Path2, #{}). 356 | 357 | 358 | %% @doc Equivalent to logs(Pid, Container, #{stdout=>true}) 359 | -spec logs(pid(), text()) -> 360 | {ok, binary()} | {error, error()}. 361 | 362 | logs(Pid, Container) -> 363 | logs(Pid, Container, #{stdout=>true}). 364 | 365 | 366 | %% @doc Get stdout and stderr logs from the container id 367 | %% You must select on stream at least (stdin, stdout or stderr). 368 | %% If you use the 'async' option, a reference() an will be returned. 369 | %% (see events/2) 370 | %% If you use the 'follow' option, the connection will remain opened 371 | %% (async is automatically selected) 372 | -spec logs(pid(), text(), 373 | #{ 374 | async => boolean(), 375 | follow => boolean(), 376 | stdout => boolean(), 377 | stderr => boolean(), 378 | since => integer(), 379 | timestamps => boolean(), 380 | tail => text() 381 | }) -> 382 | {ok, [binary()]} | {async, reference()} | {error, error()}. 383 | 384 | logs(Pid, Container, Opts) -> 385 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/logs">>]), 386 | UrlOpts = [follow, timestamps, stdout, stderr, tail, since], 387 | Path2 = make_path(Path1, Opts, UrlOpts), 388 | case Opts of 389 | #{follow:=true} -> 390 | get(Pid, Path2, #{chunks=>true, async=>true, timeout=>0}); 391 | #{async:=true} -> 392 | get(Pid, Path2, add_timeout(Opts, #{chunks=>true, async=>true})); 393 | _ -> 394 | get(Pid, Path2, add_timeout(Opts, #{chunks=>true, force_new=>true})) 395 | end. 396 | 397 | 398 | %% @doc Inspect changes on a container's filesystem. 399 | %% It tries to reuse a previous connection. 400 | -spec diff(pid(), text()) -> 401 | {ok, [map()]} | {error, error()}. 402 | 403 | diff(Pid, Container) -> 404 | Path = list_to_binary([<<"/containers/">>, Container, <<"/changes">>]), 405 | get(Pid, Path, #{}). 406 | 407 | 408 | %% @doc Export the contents of container id to a TAR file. 409 | -spec export(pid(), text(), text()) -> 410 | ok | {error, error()}. 411 | 412 | export(Pid, Container, File) -> 413 | Path = list_to_binary([<<"/containers/">>, Container, <<"/export">>]), 414 | Redirect = nklib_util:to_list(File), 415 | get(Pid, Path, #{redirect=>Redirect}). 416 | 417 | 418 | %% @doc Get container stats based on resource usage. 419 | %% A reference will be returned (see events/2). 420 | -spec stats(pid(), text()) -> 421 | {async, reference()} | {error, error()}. 422 | 423 | stats(Pid, Container) -> 424 | Path = list_to_binary([<<"/containers/">>, Container, <<"/stats">>]), 425 | get(Pid, Path, #{async=>true, chunks=>true}). 426 | 427 | 428 | %% @doc Resize the TTY for container with id. 429 | %% The container must be restarted for the resize to take effect. 430 | %% It tries to reuse a previous connection. 431 | -spec resize(pid(), text(), integer(), integer()) -> 432 | ok | {error, error()}. 433 | 434 | resize(Pid, Container, W, H) -> 435 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/resize">>]), 436 | Path2 = make_path(Path1, #{h=>H, w=>W}, [h, w]), 437 | case post(Pid, Path2, #{}) of 438 | {ok, _} -> ok; 439 | {error, Error} -> {error, Error} 440 | end. 441 | 442 | 443 | %% @doc Start a container 444 | -spec start(pid(), text()) -> 445 | ok | {error, error()}. 446 | 447 | start(Pid, Container) -> 448 | Path = list_to_binary([<<"/containers/">>, Container, <<"/start">>]), 449 | case post(Pid, Path, #{force_new=>true}) of 450 | {ok, _} -> ok; 451 | {error, Error} -> {error, Error} 452 | end. 453 | 454 | 455 | %% @doc Equivalent to stop(Pid, Container, #{}) 456 | -spec stop(pid(), text()) -> 457 | ok | {error, error()}. 458 | 459 | stop(Pid, Container) -> 460 | stop(Pid, Container, #{}). 461 | 462 | 463 | %% @doc Stops a container 464 | %% Can specify the maximum time (in seconds) before killing it. 465 | -spec stop(pid(), text(), #{t=>pos_integer()}) -> 466 | ok | {error, error()}. 467 | 468 | stop(Pid, Container, Opts) -> 469 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/stop">>]), 470 | Path2 = make_path(Path1, Opts, [t]), 471 | case post(Pid, Path2, #{force_new=>true}) of 472 | {ok, _} -> ok; 473 | {error, Error} -> {error, Error} 474 | end. 475 | 476 | 477 | %% @doc Equivalent to restart(Pid, Text, #{}) 478 | -spec restart(pid(), text()) -> 479 | ok | {error, error()}. 480 | 481 | restart(Pid, Container) -> 482 | restart(Pid, Container, #{}). 483 | 484 | 485 | %% @doc Restart a container 486 | %% Can specify the maximum time (in seconds) before killing it 487 | -spec restart(pid(), text(), #{t=>pos_integer()}) -> 488 | ok | {error, error()}. 489 | 490 | restart(Pid, Container, Opts) -> 491 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/restart">>]), 492 | Path2 = make_path(Path1, Opts, [t]), 493 | case post(Pid, Path2, #{force_new=>true}) of 494 | {ok, _} -> ok; 495 | {error, Error} -> {error, Error} 496 | end. 497 | 498 | 499 | %% @doc Equivalent to kill(Pid, Container, #{}) 500 | -spec kill(pid(), text()) -> 501 | ok | {error, error()}. 502 | 503 | kill(Pid, Container) -> 504 | kill(Pid, Container, #{}). 505 | 506 | 507 | %% @doc Kill a container 508 | %% Can specify the signal to send 509 | -spec kill(pid(), text(), #{signal=>integer()|text()}) -> 510 | ok | {error, error()}. 511 | 512 | kill(Pid, Container, Opts) -> 513 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/kill">>]), 514 | Path2 = make_path(Path1, Opts, [signal]), 515 | case post(Pid, Path2, #{force_new=>true}) of 516 | {ok, _} -> ok; 517 | {error, Error} -> {error, Error} 518 | end. 519 | 520 | 521 | %% @doc Rename a container with a new name. 522 | %% It tries to reuse a previous connection. 523 | -spec rename(pid(), text(), text()) -> 524 | ok | {error, error()}. 525 | 526 | rename(Pid, Id, Name) -> 527 | Path1 = list_to_binary([<<"/containers/">>, Id, <<"/rename">>]), 528 | Path2 = make_path(Path1, #{name=>Name}, [name]), 529 | case post(Pid, Path2, #{}) of 530 | {ok, _} -> ok; 531 | {error, Error} -> {error, Error} 532 | end. 533 | 534 | 535 | %% @doc Pause a container. 536 | %% It tries to reuse a previous connection. 537 | -spec pause(pid(), text()) -> 538 | ok | {error, error()}. 539 | 540 | pause(Pid, Container) -> 541 | Path = list_to_binary([<<"/containers/">>, Container, <<"/pause">>]), 542 | case post(Pid, Path, #{}) of 543 | {ok, _} -> ok; 544 | {error, Error} -> {error, Error} 545 | end. 546 | 547 | 548 | %% @doc Unpause a container. 549 | %% It tries to reuse a previous connection. 550 | -spec unpause(pid(), text()) -> 551 | ok | {error, error()}. 552 | 553 | unpause(Pid, Container) -> 554 | Path = list_to_binary([<<"/containers/">>, Container, <<"/unpause">>]), 555 | case post(Pid, Path, #{}) of 556 | {ok, _} -> ok; 557 | {error, Error} -> {error, Error} 558 | end. 559 | 560 | 561 | %% @doc Equivalent to attach(Pid, Container, #{stream=>true, stdin=>true, stdout=>true} 562 | -spec attach(pid(), text()) -> 563 | {async, reference()} | {error, error()}. 564 | 565 | attach(Pid, Container) -> 566 | attach(Pid, Container, #{stream=>true, stdin=>true, stdout=>true}). 567 | 568 | 569 | %% @doc Attach to a container input/output/error 570 | %% When using 'async', a reference is returned (see events/2) 571 | %% When using 'stream' the connection reamins opened (async is automatically selected), 572 | %% and you can send commands using attach_send/3 573 | %% When created with the TTY setting, the stream is the raw data from the process 574 | %% PTY and client's stdin. When the TTY is disabled, then the stream is multiplexed 575 | %% to separate stdout and stderr. 576 | %% (received messages will be like "0:...", "1:...", "2:..." or "X:...") 577 | -spec attach(pid(), text(), 578 | #{ 579 | async => boolean(), 580 | stream => boolean(), % Do streaming 581 | logs => boolean(), % Return logs 582 | stdin => boolean(), % If stream, attach to stdin 583 | stdout => boolean(), % If logs, return stdout log. If stream, attach 584 | stderr => boolean(), % If logs, return stderr log. If stream, attach 585 | timeout => pos_integer() % Timeout before closing the connection (secs) 586 | }) -> 587 | {ok, [binary()]} | {async, reference()} | {error, error()}. 588 | 589 | attach(Pid, Container, Opts) -> 590 | Path1 = list_to_binary([<<"/containers/">>, Container, <<"/attach">>]), 591 | UrlOpts = [logs, stream, stdin, stdout, stderr], 592 | Path2 = make_path(Path1, Opts, UrlOpts), 593 | case Opts of 594 | #{stream:=true} -> 595 | post(Pid, Path2, add_timeout(Opts, #{chunks=>true, async=>true})); 596 | #{async:=true} -> 597 | post(Pid, Path2, add_timeout(Opts, #{chunks=>true, async=>true})); 598 | _ -> 599 | post(Pid, Path2, add_timeout(Opts, #{chunks=>true, force_new=>true})) 600 | end. 601 | 602 | 603 | %% @doc Sends text to an attached 'stream' session 604 | -spec attach_send(pid(), reference(), iolist()) -> 605 | ok. 606 | 607 | attach_send(Pid, Ref, Data) -> 608 | nkdocker_server:data(Pid, Ref, Data). 609 | 610 | 611 | %% @doc Equivalent to wait(Pid, Container, 60000) 612 | -spec wait(pid(), text()) -> 613 | {ok, integer()} | {error, error()}. 614 | 615 | wait(Pid, Container) -> 616 | wait(Pid, Container, 60000). 617 | 618 | 619 | %% @doc Waits for a container to stop, returning the exit code 620 | -spec wait(pid(), text(), integer()) -> 621 | {ok, map()} | {error, error()}. 622 | 623 | wait(Pid, Container, Timeout) -> 624 | Path = list_to_binary([<<"/containers/">>, Container, <<"/wait">>]), 625 | post(Pid, Path, #{force_new=>true, timeout=>Timeout}). 626 | 627 | 628 | %% @doc Equivalent to rm(Pid, Container, #{}) 629 | -spec rm(pid(), text()) -> 630 | ok | {error, error()}. 631 | 632 | rm(Pid, Container) -> 633 | rm(Pid, Container, #{}). 634 | 635 | 636 | %% @doc Removes a container 637 | -spec rm(pid(), text(), 638 | #{ 639 | force => boolean(), % Kill the container before removing 640 | v => boolean() % Remove associated volumes 641 | }) -> 642 | ok | {error, error()}. 643 | 644 | rm(Pid, Container, Opts) -> 645 | Path1 = list_to_binary([<<"/containers/">>, Container]), 646 | Path2 = make_path(Path1, Opts, [force, v]), 647 | case del(Pid, Path2, #{force_new=>true}) of 648 | {ok, _} -> ok; 649 | {error, Error} -> {error, Error} 650 | end. 651 | 652 | 653 | %% @doc Copy files or folders from a container to a TAR file 654 | -spec cp(pid(), text(), text(), text()) -> 655 | ok | {error, error()}. 656 | 657 | cp(Pid, Container, ContPath, File) -> 658 | Path = list_to_binary([<<"/containers/">>, Container, <<"/copy">>]), 659 | Body = #{'Resource' => to_binary(ContPath)}, 660 | Redirect = nklib_util:to_list(File), 661 | post(Pid, Path, Body, #{redirect=>Redirect}). 662 | 663 | 664 | 665 | %% =================================================================== 666 | %% Images Docker Functions 667 | %% =================================================================== 668 | 669 | 670 | %% @doc Equivalent to images(Pid, #{}) 671 | -spec images(pid()) -> 672 | {ok, [map()]} | {error, error()}. 673 | 674 | images(Pid) -> 675 | images(Pid, #{}). 676 | 677 | 678 | %% @doc List images. 679 | %% It tries to reuse a previous connection. 680 | -spec images(pid(), 681 | #{ 682 | all => boolean(), 683 | filters => #{dangling => true} 684 | }) -> 685 | {ok, [map()]} | {error, error()}. 686 | 687 | images(Pid, Opts) -> 688 | Path = make_path(<<"/images/json">>, get_filters(Opts), [all, filters]), 689 | get(Pid, Path, #{}). 690 | 691 | 692 | %% @doc Equivalent to build(Path, TarBin, #{}). 693 | -spec build(pid(),iolist()) -> 694 | {ok, [map()]} | {error, error()}. 695 | 696 | build(Pid, TarBin) -> 697 | build(Pid, TarBin, #{}). 698 | 699 | 700 | %% @doc Build an image from a Dockerfile 701 | %% The TarBin must be a binary with a TAR archive format, compressed with 702 | %% one of the following algorithms: identity (no compression), gzip, bzip2, xz. 703 | %% The archive must include a build instructions file, typically called Dockerfile 704 | %% at the root of the archive. The dockerfile parameter may be used to specify 705 | %% a different build instructions file by having its value be the path to 706 | %% the alternate build instructions file to use. 707 | %% The archive may include any number of other files, which will be accessible 708 | %% in the build context (See the ADD build command). 709 | %% If you the 'async' option, a reference will be returned (see events/2). 710 | -spec build(pid(), binary(), 711 | #{ 712 | async => boolean(), % See description for logs/3 713 | dockerfile => text(), % path within the build context to the Dockerfile 714 | t => text(), % repository name (and optionally a tag) 715 | remote => text(), % git or HTTP/HTTPS URI build source 716 | q => binary(), % suppress verbose build output 717 | nocache => boolean(), % do not use the cache when building the image 718 | pull => boolean(), % attempt to pull the image even if exists locally 719 | rm => boolean(), % remove intermediate containers (default) 720 | forcerm => boolean(), % always remove intermediate containers (includes rm) 721 | memory => integer(), % set memory limit for build 722 | memswap => integer(), % Total memory (memory + swap), -1 to disable swap 723 | cpushares => integer(), % CPU shares (relative weight) 724 | cpusetcpus => integer(),% CPUs in which to allow exection, e.g., 0-3, 0,1 725 | timeout => integer(), % time to wait before timeout 726 | username => text(), % 727 | password => text(), % Use this info to log to a remote 728 | email => text(), % registry to pull 729 | serveraddress => text() % 730 | }) -> 731 | {ok, [map()]} | {async, reference()} | {error, error()}. 732 | 733 | build(Pid, TarBin, Opts) -> 734 | UrlOpts = [dockerfile, t, remote, q, nocache, pull, rm, forcerm, memory, memswap, 735 | cpushares, cpusetcpus], 736 | Path = make_path(<<"/build">>, Opts, UrlOpts), 737 | DockerOpts1 = #{ 738 | chunks => true, 739 | async => maps:get(async, Opts, false), 740 | force_new => true, 741 | headers => [{<<"content_type">>, <<"application/tar">>}] 742 | }, 743 | DockerOpts2 = add_authconfig(Opts, DockerOpts1), 744 | post(Pid, Path, TarBin, add_timeout(Opts, DockerOpts2)). 745 | 746 | 747 | %% @doc Create an image, either by pulling it from the registry or by importing it 748 | %% If you the 'async' option, a reference will be returned (see events/2). 749 | -spec create_image(pid(), 750 | #{ 751 | async => boolean(), % See description for logs/2 752 | fromImage => text(), % name of the image to pull 753 | fromSrc => text(), % source to import 754 | repo => text(), % 755 | tag => text(), % 756 | registry => text(), % the registry to pull from 757 | username => text(), % Use this info to log to a remote 758 | password => text(), % registry to pull 759 | email => text(), 760 | serveraddress => text() 761 | }) -> 762 | {ok, [map()]} | {async, reference()} | {error, error()}. 763 | 764 | create_image(Pid, Opts) -> 765 | UrlOpts = [fromImage, fromSrc, repo, tag, registry], 766 | Path = make_path(<<"/images/create">>, Opts, UrlOpts), 767 | DockerOpts1 = #{ 768 | chunks => true, 769 | async => maps:get(async, Opts, false), 770 | force_new => true, 771 | headers => [{<<"content_type">>, <<"application/tar">>}] 772 | }, 773 | DockerOpts2 = add_authconfig(Opts, DockerOpts1), 774 | post(Pid, Path, add_timeout(Opts, DockerOpts2)). 775 | 776 | 777 | %% @doc Inspect an image 778 | %% It tries to reuse a previous connection. 779 | -spec inspect_image(pid(), text()) -> 780 | {ok, map()} | {error, error()}. 781 | 782 | inspect_image(Pid, Id) -> 783 | Path2 = list_to_binary([<<"/images/">>, Id, <<"/json">>]), 784 | get(Pid, Path2, #{}). 785 | 786 | 787 | %% @doc Return the history of the image. 788 | %% It tries to reuse a previous connection. 789 | -spec history(pid(), text()) -> 790 | {ok, [map()]} | {error, error()}. 791 | 792 | history(Pid, Image) -> 793 | Path = list_to_binary([<<"/images/">>, Image, <<"/history">>]), 794 | get(Pid, Path, #{}). 795 | 796 | 797 | %% @doc Push an image on the registry 798 | %% If you wish to push an image on to a private registry, that image 799 | %% must already have been tagged into a repository which references 800 | %% that registry host name and port. This repository name should 801 | %% then be used in the URL. This mirrors the flow of the CLI. 802 | %% If you the 'async' option, a reference will be returned (see events/2). 803 | -spec push(pid(), text(), 804 | #{ 805 | async => boolean(), % See description for logs/2 806 | tag => text(), % the tag to associate with the image on the registry 807 | username => text(), % 808 | password => text(), % Use this info to log to a remote 809 | email => text(), % registry to pull 810 | serveraddress => text() % 811 | }) -> 812 | {ok, [map()]} | {async, pid()} | {error, error()}. 813 | 814 | push(Pid, Name, Opts) -> 815 | UrlOpts = [tag], 816 | Path1 = list_to_binary([<<"/images/">>, Name, <<"/push">>]), 817 | Path2 = make_path(Path1, Opts, UrlOpts), 818 | DockerOpts1 = #{ 819 | async => maps:get(async, Opts, false), 820 | force_new => true 821 | }, 822 | DockerOpts2 = add_authconfig(Opts, DockerOpts1), 823 | post(Pid, Path2, add_timeout(Opts, DockerOpts2)). 824 | 825 | 826 | %% @doc Tag an image into a repository. 827 | %% It tries to reuse a previous connection. 828 | -spec tag(pid(), text(), 829 | #{ 830 | repo => text(), % The repository to tag in 831 | tag => text(), % The new tag name 832 | force => boolean() % 833 | }) -> 834 | ok | {error, error()}. 835 | 836 | tag(Pid, Name, Opts) -> 837 | UrlOpts = [repo, force, tag], 838 | Path1 = list_to_binary([<<"/images/">>, Name, <<"/tag">>]), 839 | Path2 = make_path(Path1, Opts, UrlOpts), 840 | case post(Pid, Path2, #{}) of 841 | {ok, _} -> ok; 842 | {error, Error} -> {error, Error} 843 | end. 844 | 845 | 846 | %% @doc Equivalent to commit(Pid, Container, #{}) 847 | -spec commit(pid(), text()) -> 848 | {ok, Id::binary(), map()} | {error, error()}. 849 | 850 | commit(Pid, Container) -> 851 | commit(Pid, Container, #{}). 852 | 853 | %% @doc Create a new image from a container's changes 854 | -spec commit(pid(), text(), 855 | #{ 856 | repo => text(), 857 | tag => text(), 858 | author => text(), 859 | comment => text(), 860 | timeout => pos_integer() 861 | }) -> 862 | {ok, map()} | {error, error()}. 863 | 864 | commit(Pid, Container, Opts) -> 865 | UrlOpts = [container, repo, tag, author, comment], 866 | Path = make_path(<<"/commit">>, Opts#{container=>Container}, UrlOpts), 867 | case nkdocker_server:create_spec(Pid, Opts) of 868 | {ok, Spec} -> 869 | post(Pid, Path, Spec, add_timeout(Opts, #{force_new=>true})); 870 | {error, Error} -> 871 | {error, Error} 872 | end. 873 | 874 | 875 | %% @doc Equivalent to rmi(Pid, Image, #{}) 876 | -spec rmi(pid(), text()) -> 877 | {ok, [map()]} | {error, error()}. 878 | 879 | rmi(Pid, Image) -> 880 | rmi(Pid, Image, #{}). 881 | 882 | 883 | %% @doc Removes an image. 884 | %% It tries to reuse a previous connection. 885 | -spec rmi(pid(), text(), 886 | #{ 887 | force => boolean(), 888 | noprune => boolean() 889 | }) -> 890 | {ok, map()} | {error, error()}. 891 | 892 | rmi(Pid, Image, Opts) -> 893 | Path1 = list_to_binary([<<"/images/">>, Image]), 894 | Path2 = make_path(Path1, Opts, [force, noprune]), 895 | del(Pid, Path2, #{}). 896 | 897 | 898 | 899 | %% @doc Search images on the repository 900 | -spec search(pid(), text()) -> 901 | {ok, [map()]} | {error, error()}. 902 | 903 | search(Pid, Term) -> 904 | Path = make_path(<<"/images/search">>, #{term=>Term}, [term]), 905 | get(Pid, Path, #{force_new=>true}). 906 | 907 | 908 | %% @doc Get a tarball containing all images in a repository 909 | %% Get a tarball containing all images and metadata for the repository specified by name. 910 | %% If name is a specific name and tag (e.g. ubuntu:latest), then only that image 911 | %% (and its parents) are returned. If name is an image ID, similarly only that image 912 | %% (and its parents) are returned, but with the exclusion of the 'repositories' file 913 | %% in the tarball, as there were no image names referenced. 914 | -spec get_image(pid(), text(), text()) -> 915 | ok | {error, error()}. 916 | 917 | get_image(Pid, Name, File) -> 918 | Path = list_to_binary([<<"/images/">>, Name, <<"/get">>]), 919 | Redirect = nklib_util:to_list(File), 920 | get(Pid, Path, #{redirect=>Redirect}). 921 | 922 | 923 | %% @doc Get a tarball containing all images 924 | %% Get a tarball containing all images and metadata for one or more repositories. 925 | %% For each value of the names parameter: if it is a specific name and tag 926 | %% (e.g. ubuntu:latest), then only that image (and its parents) are returned; 927 | %% if it is an image ID, similarly only that image (and its parents) are returned 928 | % and there would be no names referenced in the 'repositories' file for this image ID. 929 | -spec get_images(pid(), [text()], text()) -> 930 | ok | {error, error()}. 931 | 932 | get_images(Pid, NameList, File) -> 933 | Names1 = [["names=", http_uri:encode(nklib_util:to_list(N))] || N <- NameList], 934 | Names2 = nklib_util:bjoin(Names1, <<"&">>), 935 | Path = <<"/images/get?", Names2/binary>>, 936 | Redirect = nklib_util:to_list(File), 937 | get(Pid, Path, #{redirect=>Redirect}). 938 | 939 | 940 | %% @doc Loads a binary with a TAR image file into docker 941 | -spec load(pid(), iolist()) -> 942 | ok | {error, error()}. 943 | 944 | load(Pid, TarBin) -> 945 | DockerOpts = #{ 946 | force_new => true, 947 | headers => [{<<"content_type">>, <<"application/tar">>}] 948 | }, 949 | case post(Pid, <<"/images/load">>, TarBin, DockerOpts) of 950 | {ok, _} -> ok; 951 | {error, Error} -> {error, Error} 952 | end. 953 | 954 | 955 | 956 | %% =================================================================== 957 | %% Exec Docker Functions 958 | %% =================================================================== 959 | 960 | %% @doc Equivalent to exec_create(Pid, Container, Cmds, #{}) 961 | -spec exec_create(pid(), text(), [text()]) -> 962 | {ok, binary()} | {error, error()}. 963 | 964 | exec_create(Pid, Container, Cmds) -> 965 | exec_create(Pid, Container, Cmds, #{}). 966 | 967 | 968 | %% @doc Sets up an exec instance in a running container 969 | -spec exec_create(pid(), text(), [text()], 970 | #{ 971 | stdin => boolean(), 972 | stdout => boolean(), 973 | stderr => boolean(), 974 | tty => boolean() 975 | }) -> 976 | {ok, binary()} | {error, error()}. 977 | 978 | exec_create(Pid, Container, Cmds, Opts) -> 979 | Path = list_to_binary([<<"/containers/">>, Container, <<"/exec">>]), 980 | Spec = #{ 981 | 'AttachStdin' => maps:get(stdin, Opts, true), 982 | 'AttachStdout' => maps:get(stdout, Opts, true), 983 | 'AttachStderr' => maps:get(stderr, Opts, true), 984 | 'Tty' => maps:get(tty, Opts, true), 985 | 'Cmd'=> [to_binary(C) || C <- Cmds] 986 | }, 987 | case post(Pid, Path, Spec, #{force_new=>true}) of 988 | {ok, #{<<"Id">>:=Id}} -> {ok, Id}; 989 | {error, Error} -> {error, Error} 990 | end. 991 | 992 | 993 | %% @doc Equivalent to exec_start(Pid, Id, #{}) 994 | -spec exec_start(pid(), text()) -> 995 | {ok, binary()} | {error, error()}. 996 | 997 | exec_start(Pid, Id) -> 998 | exec_start(Pid, Id, #{}). 999 | 1000 | 1001 | %% @doc Starts a previously set up exec instance id. 1002 | %% TODO: Detach does not seem to work... 1003 | %% If you the 'async' option, a reference will be returned (see events/2). 1004 | -spec exec_start(pid(), text(), 1005 | #{ 1006 | detach => boolean(), 1007 | tty => boolean() 1008 | }) -> 1009 | {ok, map()} | {async, pid()} | {error, error()}. 1010 | 1011 | exec_start(Pid, Id, Opts) -> 1012 | Path = list_to_binary([<<"/exec/">>, Id, <<"/start">>]), 1013 | Spec = #{ 1014 | 'Detach' => maps:get(detach, Opts, false), 1015 | 'Tty' => maps:get(tty, Opts, true) 1016 | }, 1017 | case Opts of 1018 | #{detach:=true} -> 1019 | post(Pid, Path, Spec, add_timeout(Opts, #{force_new=>true})); 1020 | _ -> 1021 | post(Pid, Path, Spec, add_timeout(Opts, #{async=>true})) 1022 | end. 1023 | 1024 | 1025 | %% @doc Return low-level information about the exec command. 1026 | %% It tries to reuse a previous connection. 1027 | -spec exec_inspect(pid(), text()) -> 1028 | {ok, binary()} | {error, error()}. 1029 | 1030 | exec_inspect(Pid, Id) -> 1031 | Path = list_to_binary([<<"/exec/">>, Id, <<"/json">>]), 1032 | get(Pid, Path, #{}). 1033 | 1034 | 1035 | %% @doc Resizes the tty session used by the exec command id. 1036 | %% This API is valid only if tty was specified as part of creating 1037 | %% and starting the exec command. 1038 | %% It tries to reuse a previous connection. 1039 | -spec exec_resize(pid(), text(), integer(), integer()) -> 1040 | ok | {error, error()}. 1041 | 1042 | exec_resize(Pid, Id, W, H) -> 1043 | Path1 = list_to_binary([<<"/exec/">>, Id, <<"/resize">>]), 1044 | Path2 = make_path(Path1, #{h=>H, w=>W}, [h, w]), 1045 | case post(Pid, Path2, #{}) of 1046 | {ok, _} -> ok; 1047 | {error, Error} -> {error, Error} 1048 | end. 1049 | 1050 | 1051 | 1052 | 1053 | %% =================================================================== 1054 | %% Internal 1055 | %% =================================================================== 1056 | 1057 | 1058 | %% @private 1059 | -spec get(pid(), binary(), nkdocker_server:cmd_opts()) -> 1060 | {ok, map()|binary()} | {error, error()}. 1061 | 1062 | get(Pid, Path, Opts) -> 1063 | nkdocker_server:cmd(Pid, <<"GET">>, Path, <<>>, Opts). 1064 | 1065 | 1066 | %% @private 1067 | -spec post(pid(), binary(), nkdocker_server:cmd_opts()) -> 1068 | {ok, map()|binary()} | {error, error()}. 1069 | 1070 | post(Pid, Path, Opts) -> 1071 | post(Pid, Path, <<>>, Opts). 1072 | 1073 | 1074 | %% @private 1075 | -spec post(pid(), binary(), binary()|iolist()|map(), nkdocker_server:cmd_opts()) -> 1076 | {ok, map()|binary()} | {error, error()}. 1077 | 1078 | post(Pid, Path, Body, Opts) -> 1079 | nkdocker_server:cmd(Pid, <<"POST">>, Path, Body, Opts). 1080 | 1081 | 1082 | %% @private 1083 | -spec del(pid(), binary(), nkdocker_server:cmd_opts()) -> 1084 | {ok, map()|binary()} | {error, error()}. 1085 | 1086 | del(Pid, Path, Opts) -> 1087 | nkdocker_server:cmd(Pid, <<"DELETE">>, Path, <<>>, Opts). 1088 | 1089 | 1090 | %% @private 1091 | make_path(Path, Opts, Valid) -> 1092 | OptsList = [{K, V} || {K, V} <- maps:to_list(Opts), lists:member(K, Valid)], 1093 | nkdocker_opts:make_path(Path, OptsList). 1094 | 1095 | 1096 | %% @private 1097 | get_filters(#{filters:=Filters}=Opts) -> 1098 | Filters1 = maps:map( 1099 | fun(_K, V) -> 1100 | case is_list(V) of 1101 | true -> [to_binary(T) || T <- V]; 1102 | false -> [to_binary(V)] 1103 | end 1104 | end, 1105 | Filters), 1106 | Opts#{filters:=Filters1}; 1107 | get_filters(Opts) -> 1108 | Opts. 1109 | 1110 | 1111 | %% @private 1112 | add_authconfig(#{username:=User, password:=Pass, email:=Email}=Opts, Res) -> 1113 | Json = 1114 | nklib_json:encode( 1115 | #{ 1116 | username => to_binary(User), 1117 | password => to_binary(Pass), 1118 | email => to_binary(Email), 1119 | serveraddress => to_binary(maps:get(serveraddress, Opts, ?HUB)) 1120 | }), 1121 | Hds = maps:get(headers, Res, []), 1122 | Res#{headers => Hds ++ [{<<"x-registry-auth">>, base64:encode(Json)}]}; 1123 | 1124 | add_authconfig(_, Res) -> 1125 | Res. 1126 | 1127 | 1128 | %% @private 1129 | add_timeout(Opts, Map) -> 1130 | Timeout = maps:get(timeout, Opts, 180000), 1131 | Map#{timeout=>Timeout}. 1132 | 1133 | 1134 | 1135 | -------------------------------------------------------------------------------- /src/nkdocker_app.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkDOCKER OTP Application Module 22 | -module(nkdocker_app). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(application). 25 | 26 | -export([get/1, get/2, put/2, del/1]). 27 | -export([start/0, start/2, stop/1]). 28 | -export([get_env/2]). 29 | 30 | -include("nkdocker.hrl"). 31 | -include_lib("nklib/include/nklib.hrl"). 32 | 33 | 34 | -define(APP, nkdocker). 35 | 36 | %% =================================================================== 37 | %% Private 38 | %% =================================================================== 39 | 40 | %% @doc Starts NkDOCKER stand alone. 41 | -spec start() -> 42 | ok | {error, Reason::term()}. 43 | 44 | start() -> 45 | case nklib_util:ensure_all_started(?APP, permanent) of 46 | {ok, _Started} -> 47 | ok; 48 | Error -> 49 | Error 50 | end. 51 | 52 | %% @private OTP standard start callback 53 | start(_Type, _Args) -> 54 | _ = code:ensure_loaded(nkdocker_protocol), 55 | {ok, Pid} = nkdocker_sup:start_link(), 56 | {ok, Vsn} = application:get_key(nkdocker, vsn), 57 | lager:info("NkDOCKER v~s has started.", [Vsn]), 58 | ConnOpts = get_config(), 59 | lager:info("Default config: ~p", [ConnOpts]), 60 | application:set_env(?APP, conn_config, ConnOpts), 61 | {ok, Pid}. 62 | 63 | 64 | %% @private OTP standard stop callback 65 | stop(_) -> 66 | ok. 67 | 68 | 69 | %% @private 70 | get_config() -> 71 | case os:getenv("DOCKER_HOST") of 72 | false -> 73 | case get_env(proto, tcp) of 74 | tcp -> 75 | #{ 76 | host => get_env(host, "127.0.0.1"), 77 | port => get_env(port, 2375), 78 | proto => tcp 79 | }; 80 | tls -> 81 | #{ 82 | host => get_env(host, "127.0.0.1"), 83 | port => get_env(port, 2376), 84 | proto => tls 85 | } 86 | end; 87 | _ -> 88 | get_env_config() 89 | end. 90 | 91 | 92 | get_env_config() -> 93 | case nklib_parse:uris(os:getenv("DOCKER_HOST")) of 94 | [#uri{scheme=tcp, domain=Domain, port=Port}] -> 95 | ok; 96 | _ -> 97 | Domain = Port = error("Unrecognized DOCKER_HOST env") 98 | end, 99 | case 100 | os:getenv("DOCKER_TLS") == "1" orelse 101 | os:getenv("DOCKER_TLS") == "true" orelse 102 | os:getenv("DOCKER_TLS_VERIFY") == "1" orelse 103 | os:getenv("DOCKER_TLS_VERIFY") == "true" 104 | of 105 | true -> 106 | case os:getenv("DOCKER_CERT_PATH") of 107 | false -> 108 | #{ 109 | host => Domain, 110 | port => Port, 111 | proto => tls 112 | }; 113 | Path -> 114 | #{ 115 | host => Domain, 116 | port => Port, 117 | proto => tls, 118 | tls_certfile => filename:join(Path, "cert.pem"), 119 | tls_keyfile => filename:join(Path, "key.pem") 120 | } 121 | end; 122 | false -> 123 | #{ 124 | host => Domain, 125 | port => Port, 126 | proto => tcp 127 | } 128 | end. 129 | 130 | 131 | %% @private 132 | get_env(Key, Default) -> 133 | application:get_env(?APP, Key, Default). 134 | 135 | 136 | %% Configuration access 137 | get(Key) -> 138 | nklib_config:get(?APP, Key). 139 | 140 | get(Key, Default) -> 141 | nklib_config:get(?APP, Key, Default). 142 | 143 | put(Key, Val) -> 144 | nklib_config:put(?APP, Key, Val). 145 | 146 | del(Key) -> 147 | nklib_config:del(?APP, Key). 148 | -------------------------------------------------------------------------------- /src/nkdocker_monitor.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(nkdocker_monitor). 22 | -author('Carlos Gonzalez '). 23 | -behaviour(gen_server). 24 | 25 | -export([register/1, register/2, unregister/1, unregister/2, get_docker/1]). 26 | -export([get_running/1, get_data/2, start_stats/2, get_all/0]). 27 | -export([start_link/3]). 28 | -export([init/1, terminate/2, code_change/3, handle_call/3, 29 | handle_cast/2, handle_info/2]). 30 | -export_type([monitor_id/0, notify/0, data/0]). 31 | 32 | -define(LLOG(Type, Txt, Args), lager:Type("NkDOCKER Monitor "++Txt, Args)). 33 | 34 | -define(UPDATE_TIME, 30000). 35 | 36 | -callback nkdocker_notify(monitor_id(), notify()) -> 37 | ok. 38 | 39 | 40 | %% =================================================================== 41 | %% Types 42 | %% =================================================================== 43 | 44 | -type monitor_id() :: {inet:ip_address(), inet:port()}. 45 | 46 | -type notify() :: 47 | {docker, {Id::binary(), Status::atom(), From::binary(), Time::integer()}} | 48 | {stats, {Id::binary(), Data::binary()}} | 49 | {start, {Name::binary(), Data::data()}} | 50 | {stop, {Name::binary(), Data::data()}} | 51 | {ping, {Name::binary(), Data::data()}}. % Send periodically 52 | 53 | 54 | -type data() :: 55 | #{ 56 | name => binary(), 57 | id => binary(), 58 | labels => map(), 59 | env => map(), 60 | image => binary(), 61 | stats => map() 62 | }. 63 | 64 | 65 | 66 | 67 | %% =================================================================== 68 | %% Public 69 | %% =================================================================== 70 | 71 | 72 | %% @doc Equivalent to register(CallBack, #{}) 73 | -spec register(module()) -> 74 | {ok, monitor_id()} | {error, term()}. 75 | 76 | register(CallBack) -> 77 | nkdocker_monitor:register(CallBack, #{}). 78 | 79 | 80 | %% @doc Registers a server to listen to docker events 81 | %% For each new event, Callback:nkdocker_notify(id(), event()) will be called 82 | %% The first caller for a specific ip and port daemon will start the server 83 | -spec register(module(), nkdocker:conn_opts()) -> 84 | {ok, monitor_id()} | {error, term()}. 85 | 86 | register(CallBack, Opts) -> 87 | case find_monitor(Opts) of 88 | {ok, MonId, none} -> 89 | case nkdocker_sup:start_monitor(MonId, CallBack, Opts) of 90 | {ok, _Pid} -> 91 | {ok, MonId}; 92 | {error, Error} -> 93 | {error, Error} 94 | end; 95 | {ok, _MonId, Pid} -> 96 | gen_server:call(Pid, {register, CallBack}); 97 | {error, Error} -> 98 | {error, Error} 99 | end. 100 | 101 | 102 | %% @doc Equivalent to unregister(CallBack, #{}) 103 | -spec unregister(module()) -> 104 | ok | {error, term()}. 105 | 106 | unregister(CallBack) -> 107 | unregister(CallBack, #{}). 108 | 109 | 110 | %% @doc Unregisters a callback 111 | %% After the last callback is unregistered, the server stops 112 | unregister(Callback, Opts) -> 113 | case find_monitor(Opts) of 114 | {ok, _MonId, none} -> 115 | ok; 116 | {ok, _MonId, Pid} -> 117 | gen_server:cast(Pid, {unregister, Callback}); 118 | {error, Error} -> 119 | {error, Error} 120 | end. 121 | 122 | 123 | %% @doc Get the docker server pid 124 | -spec get_docker(monitor_id()) -> 125 | {ok, pid()} | {error, term()}. 126 | 127 | get_docker(MonId) -> 128 | case nklib_proc:values({?MODULE, MonId}) of 129 | [] -> 130 | {error, unknown_monitor}; 131 | [{DockerPid, _Pid}] -> 132 | {ok, DockerPid} 133 | end. 134 | 135 | 136 | %% @doc Gets all containers info 137 | -spec get_running(monitor_id()) -> 138 | {ok, #{binary() => data()}} | {error, term()}. 139 | 140 | get_running(MonId) -> 141 | do_call(MonId, get_running). 142 | 143 | 144 | %% @doc Get specific container info 145 | -spec get_data(monitor_id(), binary()) -> 146 | {ok, data()} | {error, term()}. 147 | 148 | get_data(MonId, Id) -> 149 | do_call(MonId, {get_data, nklib_util:to_binary(Id)}). 150 | 151 | 152 | %% @doc Start stats for a containers 153 | -spec start_stats(monitor_id(), binary()) -> 154 | ok | {error, term()}. 155 | 156 | start_stats(MonId, Id) -> 157 | do_call(MonId, {start_stats, nklib_util:to_binary(Id)}). 158 | 159 | 160 | %% @doc Gets all started monitors 161 | -spec get_all() -> 162 | [{monitor_id(), pid()}]. 163 | 164 | get_all() -> 165 | nklib_proc:values(?MODULE). 166 | 167 | 168 | % =================================================================== 169 | %% gen_server behaviour 170 | %% =================================================================== 171 | 172 | %% @private 173 | start_link(MonId, CallBack, Opts) -> 174 | gen_server:start_link(?MODULE, [MonId, CallBack, Opts], []). 175 | 176 | 177 | -record(state, { 178 | id :: monitor_id(), 179 | server :: pid(), 180 | event_ref :: reference(), 181 | regs = [] :: [module()], 182 | time = 0 :: integer(), 183 | running = #{} :: #{Name::binary() => map()}, 184 | running_ids = #{} :: #{Id::binary() => Name::binary()}, 185 | stats_refs = #{} :: #{reference() => Name::binary()} 186 | }). 187 | 188 | 189 | %% @private 190 | -spec init(term()) -> 191 | {ok, tuple()} | {ok, tuple(), timeout()|hibernate} | 192 | {stop, term()} | ignore. 193 | 194 | init([MonId, CallBack, Opts]) -> 195 | case nkdocker_server:start_link(Opts) of 196 | {ok, Pid} -> 197 | monitor(process, Pid), 198 | true = nklib_proc:reg({?MODULE, MonId}, Pid), 199 | nklib_proc:put(?MODULE, MonId), 200 | Regs = nkdocker_app:get({monitor_callbacks, MonId}, []), 201 | case Regs of 202 | [] -> ok; 203 | _ -> ?LLOG(warning, "recovered callbacks: ~p", [Regs]) 204 | end, 205 | Regs2 = nklib_util:store_value(CallBack, Regs), 206 | nkdocker_app:put({monitor_callbacks, MonId}, Regs2), 207 | State = #state{id=MonId, server=Pid, regs=Regs2}, 208 | self() ! ping_all, 209 | case start_events(State) of 210 | {ok, State2} -> 211 | {ok, State2}; 212 | error -> 213 | {stop, events_stop} 214 | end; 215 | {error, Error} -> 216 | {stop, Error} 217 | end. 218 | 219 | 220 | %% @private 221 | -spec handle_call(term(), {pid(), term()}, #state{}) -> 222 | {noreply, #state{}} | {reply, term(), #state{}} | 223 | {stop, Reason::term(), #state{}} | {stop, Reason::term(), Reply::term(), #state{}}. 224 | 225 | handle_call({register, Module}, _From, #state{id=MonId, regs=Regs}=State) -> 226 | Regs2 = nklib_util:store_value(Module, Regs), 227 | nkdocker_app:put({monitor_callbacks, MonId}, Regs2), 228 | State2 = ping_all(State#state{regs=Regs2}), 229 | {reply, {ok, MonId}, State2}; 230 | 231 | handle_call(get_running, _From, #state{running=Running}=State) -> 232 | {reply, {ok, Running}, State}; 233 | 234 | handle_call({get_data, Id}, _From, State) -> 235 | Reply = case find_data(Id, State) of 236 | {ok, _Name, Data} -> {ok, Data}; 237 | not_found -> {error, container_not_found} 238 | end, 239 | {reply, Reply, State}; 240 | 241 | handle_call({start_stats, Id}, _From, State) -> 242 | #state{server=Server, running=Running, stats_refs=Refs} = State, 243 | case find_data(Id, State) of 244 | {ok, _Name, #{stats_ref:=_}} -> 245 | {reply, ok, State}; 246 | {ok, Name, Data} -> 247 | {async, Ref} = nkdocker:stats(Server, Name), 248 | Data2 = Data#{stats_ref=>Ref}, 249 | Running2 = maps:put(Name, Data2, Running), 250 | Refs2 = maps:put(Ref, Name, Refs), 251 | {reply, ok, State#state{running=Running2, stats_refs=Refs2}}; 252 | not_found -> 253 | {reply, {error, container_not_found}, State} 254 | end; 255 | 256 | handle_call(state, _From, State) -> 257 | {reply, State, State}; 258 | 259 | handle_call(Msg, _From, State) -> 260 | lager:error("Module ~p received unexpected call ~p", [?MODULE, Msg]), 261 | {noreply, State}. 262 | 263 | 264 | %% @private 265 | -spec handle_cast(term(), #state{}) -> 266 | {noreply, #state{}} | {stop, term(), #state{}}. 267 | 268 | handle_cast({unregister, Module}, #state{id=MonId, regs=Regs}=State) -> 269 | case Regs -- [Module] of 270 | [] -> 271 | {stop, normal, State}; 272 | Regs2 -> 273 | nkdocker_app:put({monitor_callbacks, MonId}, Regs2), 274 | {noreply, State#state{regs=Regs2}} 275 | end; 276 | 277 | handle_cast(Msg, State) -> 278 | lager:error("Module ~p received unexpected cast ~p", [?MODULE, Msg]), 279 | {noreply, State}. 280 | 281 | 282 | %% @private 283 | -spec handle_info(term(), #state{}) -> 284 | {noreply, #state{}} | {stop, term(), #state{}}. 285 | 286 | %% Why are we receiving this? 287 | handle_info({nkdocker, Ref, {ok, <<>>}}, #state{event_ref=Ref}=State) -> 288 | ?LLOG(warning, "server stopped events!", []), 289 | case start_events(State) of 290 | {ok, State2} -> 291 | {noreply, State2}; 292 | error -> 293 | {stop, ev_ok, State} 294 | end; 295 | 296 | handle_info({nkdocker, Ref, EvData}, #state{event_ref=Ref}=State) -> 297 | {noreply, event(EvData, State)}; 298 | 299 | handle_info({nkdocker, Ref, EvData}, #state{stats_refs=Refs}=State) -> 300 | case maps:find(Ref, Refs) of 301 | {ok, Name} -> 302 | {noreply, stats(Name, EvData, State)}; 303 | error when EvData == {ok, <<>>} -> 304 | {noreply, State}; 305 | error -> 306 | ?LLOG(warning, "unexpected event: ~p", [EvData]), 307 | {noreply, State} 308 | end; 309 | 310 | handle_info(ping_all, State) -> 311 | State2 = ping_all(State), 312 | erlang:send_after(?UPDATE_TIME, self(), ping_all), 313 | {noreply, State2}; 314 | 315 | handle_info({'DOWN', _Ref, process, Pid, Reason}, #state{server=Pid}=State) -> 316 | ?LLOG(warning, "docker sever stopped: ~p", [Reason]), 317 | {stop, docker_server_stop, State}; 318 | 319 | handle_info(Info, State) -> 320 | lager:warning("Module ~p received unexpected info: ~p (~p)", [?MODULE, Info, State]), 321 | {noreply, State}. 322 | 323 | 324 | %% @private 325 | -spec code_change(term(), #state{}, term()) -> 326 | {ok, #state{}}. 327 | 328 | code_change(_OldVsn, State, _Extra) -> 329 | {ok, State}. 330 | 331 | 332 | %% @private 333 | -spec terminate(term(), #state{}) -> 334 | ok. 335 | 336 | terminate(Reason, State) -> 337 | #state{id=MonId, server=Server, event_ref=Ref, stats_refs=Refs} = State, 338 | lists:foreach( 339 | fun(StatRef) -> nkdocker:finish_async(Server, StatRef) end, 340 | maps:keys(Refs)), 341 | nkdocker:finish_async(Server, Ref), 342 | case Reason of 343 | normal -> 344 | ?LLOG(info, "server stop normal", []), 345 | nkdocker_app:del({monitor_callbacks, MonId}); 346 | _ -> 347 | ?LLOG(warning, "server stop anormal: ~p", [Reason]), 348 | ok 349 | end, 350 | ok. 351 | 352 | 353 | 354 | % =================================================================== 355 | %% Internal 356 | %% =================================================================== 357 | 358 | %% @private 359 | find_monitor(Opts) -> 360 | case nkdocker_util:get_conn_info(Opts) of 361 | {ok, #{ip:=Ip, port:=Port}} -> 362 | MonId = {Ip, Port}, 363 | case nklib_proc:values({?MODULE, MonId}) of 364 | [] -> 365 | {ok, MonId, none}; 366 | [{_, Pid}] -> 367 | {ok, MonId, Pid} 368 | end; 369 | {error, Error} -> 370 | {error, Error} 371 | end. 372 | 373 | 374 | %% @private 375 | do_call(Id, Msg) -> 376 | do_call(Id, Msg, 5000). 377 | 378 | 379 | %% @private 380 | do_call(Pid, Msg, Timeout) when is_pid(Pid) -> 381 | nklib_util:call(Pid, Msg, Timeout); 382 | 383 | do_call(MonId, Msg, Timeout) -> 384 | case nklib_proc:values({?MODULE, MonId}) of 385 | [] -> 386 | {error, unknown_monitor}; 387 | [{_DockerPid, Pid}] -> 388 | nklib_util:call(Pid, Msg, Timeout) 389 | end. 390 | 391 | 392 | %% @private 393 | start_events(#state{id=MonId, server=Pid}=State) -> 394 | case nkdocker:events(Pid, #{}) of 395 | {async, Ref} -> 396 | ?LLOG(info, "events started (~p)", [MonId]), 397 | {ok, State#state{event_ref=Ref}}; 398 | {error, Error} -> 399 | ?LLOG(warning, "could not start events: ~p", [Error]), 400 | error 401 | end. 402 | 403 | 404 | %% @private 405 | -spec find_data(binary(), #state{}) -> 406 | {ok, binary(), data()} | not_found. 407 | 408 | find_data(Term, #state{running=Running, running_ids=Ids}) -> 409 | case maps:find(Term, Running) of 410 | {ok, Data} -> 411 | {ok, Term, Data}; 412 | error -> 413 | case maps:find(Term, Ids) of 414 | {ok, Name} -> 415 | {ok, Data} = maps:find(Name, Running), 416 | {ok, Name, Data}; 417 | error -> 418 | not_found 419 | end 420 | end. 421 | 422 | 423 | %% @private 424 | event({data, Event}, State) -> 425 | case Event of 426 | #{<<"status">>:=Status, <<"id">>:=Id, <<"time">>:=Time} -> 427 | From = maps:get(<<"from">>, Event, <<>>), 428 | Status2 = binary_to_atom(Status, latin1), 429 | notify({docker, {Id, Status2, From, Time}}, State), 430 | #state{time=Last} = State, 431 | State2 = case Time > Last of 432 | true -> State#state{time=Time}; 433 | false -> State 434 | end, 435 | update(Status2, Id, State2); 436 | #{<<"Action">>:=Action} -> 437 | ?LLOG(info, "action without status: ~s", [Action]), 438 | State; 439 | _ -> 440 | ?LLOG(warning, "unrecognized event: ~p", [Event]), 441 | State 442 | end; 443 | 444 | event({error, Error}, _State) -> 445 | ?LLOG(warning, "events returned error: ~p", [Error]), 446 | error({events_error, Error}); 447 | 448 | event(Other, State) -> 449 | ?LLOG(warning, "unrecognized events: ~p", [Other]), 450 | State. 451 | 452 | 453 | %% @private 454 | stats(Name, {data, Stats}, State) -> 455 | notify({stats, {Name, Stats}}, State); 456 | 457 | stats(Name, EvData, State) -> 458 | ?LLOG(warning, "unexpected stats for ~s: ~p", [Name, EvData]), 459 | State. 460 | 461 | 462 | %% @private 463 | notify(Data, #state{id=MonId, regs=Regs}=State) -> 464 | lists:foreach( 465 | fun(Module) -> 466 | case catch Module:nkdocker_notify(MonId, Data) of 467 | ok -> 468 | ok; 469 | Error -> 470 | ?LLOG(warning, "error calling ~p: ~p", [Module, Error]) 471 | end 472 | end, 473 | Regs), 474 | State. 475 | 476 | 477 | %% @private 478 | -spec update(atom(), binary(), #state{}) -> 479 | #state{}. 480 | 481 | update(start, Id, State) -> 482 | #state{server=Server, running_ids=Ids, running=Running} = State, 483 | case maps:is_key(Id, Ids) of 484 | true -> 485 | State; 486 | false -> 487 | {ok, Inspect} = nkdocker:inspect(Server, Id), 488 | #{<<"Name">>:=Name0, <<"Config">>:=Config} = Inspect, 489 | #{<<"Labels">>:=Labels, <<"Env">>:=Env, <<"Image">>:=Image} = Config, 490 | Name = case Name0 of 491 | <<"/", SubName/binary>> -> SubName; 492 | _ -> Name0 493 | end, 494 | ?LLOG(info, "monitoring container ~s (~s)", [Name, Image]), 495 | % ?LLOG(info, "Inspect: ~s", [nklib_json:encode_pretty(Inspect)]), 496 | Data = #{ 497 | name => Name, 498 | id => Id, 499 | labels => Labels, 500 | env => get_env(Env, #{}), 501 | image => Image 502 | }, 503 | notify({start, {Name, Data}}, State), 504 | Running2 = maps:put(Name, Data, Running), 505 | Ids2 = maps:put(Id, Name, Ids), 506 | State#state{running_ids=Ids2, running=Running2} 507 | end; 508 | 509 | update(die, Id, State) -> 510 | #state{running=Running, running_ids=Ids, stats_refs=Refs} = State, 511 | case maps:find(Id, Ids) of 512 | {ok, Name} -> 513 | {ok, #{name:=Name, image:=Image}=Data} = maps:find(Name, Running), 514 | ?LLOG(info, "stopping monitoring container ~s (~s)", [Name, Image]), 515 | notify({stop, {Name, Data}}, State), 516 | Running2 = maps:remove(Name, Running), 517 | Ids2 = maps:remove(Id, Ids), 518 | Refs2 = case Data of 519 | #{stats_ref:=Ref} -> 520 | maps:remove(Ref, Refs); 521 | _ -> 522 | Refs 523 | end, 524 | State#state{running=Running2, running_ids=Ids2, stats_refs=Refs2}; 525 | error -> 526 | State 527 | end; 528 | 529 | update(_Status, _Id, State) -> 530 | State. 531 | 532 | 533 | %% @private 534 | ping_all(State) -> 535 | #state{running=Running} = State2 = update_all(State), 536 | lists:foreach( 537 | fun({Name, Data}) -> notify({ping, {Name, Data}}, State) end, 538 | maps:to_list(Running)), 539 | State2. 540 | 541 | 542 | %% @private 543 | update_all(#state{server=Server, running_ids=Ids}=State) -> 544 | {ok, List} = nkdocker:ps(Server), 545 | OldIds = maps:keys(Ids), 546 | NewIds = [Id || #{<<"Id">>:=Id} <- List], 547 | State2 = remove_old(OldIds--NewIds, State), 548 | start_new(NewIds--OldIds, State2). 549 | 550 | 551 | %% @private 552 | remove_old([], State) -> 553 | State; 554 | remove_old([Id|Rest], State) -> 555 | ?LLOG(notice, "detected unexpected removal of container ~s", [Id]), 556 | remove_old(Rest, update(die, Id, State)). 557 | 558 | 559 | %% @private 560 | start_new([], State) -> 561 | State; 562 | start_new([Id|Rest], State) -> 563 | ?LLOG(notice, "detected unexpected start of container ~s", [Id]), 564 | start_new(Rest, update(start, Id, State)). 565 | 566 | 567 | %% @private 568 | get_env(null, Map) -> 569 | Map; 570 | 571 | get_env([], Map) -> 572 | Map; 573 | 574 | get_env([Bin|Rest], Map) -> 575 | case binary:split(Bin, <<"=">>) of 576 | [Name, Val] -> 577 | get_env(Rest, maps:put(Name, Val, Map)); 578 | _ -> 579 | get_env(Rest, maps:put(Bin, <<>>, Map)) 580 | end. 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | -------------------------------------------------------------------------------- /src/nkdocker_opts.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distri ted under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkDOCKER Options 22 | -module(nkdocker_opts). 23 | -author('Carlos Gonzalez '). 24 | 25 | -compile(export_all). 26 | 27 | -export([make_path/2, create_spec/2]). 28 | -export([parse_text/0]). 29 | -import(nklib_util, [to_binary/1, to_host/1]). 30 | 31 | -include("nkdocker.hrl"). 32 | 33 | 34 | %% =================================================================== 35 | %% Path generator parser 36 | %% =================================================================== 37 | 38 | %% @private 39 | -spec make_path(binary(), [{atom(), term()}]) -> 40 | binary(). 41 | 42 | make_path(Path, List) -> 43 | case iter_path(List, <<>>) of 44 | <<>> -> 45 | Path; 46 | <<$&, Encoded/binary>> -> 47 | <> 48 | end. 49 | 50 | 51 | %% @private 52 | iter_path([], Acc) -> 53 | Acc; 54 | 55 | iter_path([{Key, Val}|Rest], Acc) when is_map(Val) -> 56 | iter_path([{Key, nklib_json:encode(Val)}|Rest], Acc); 57 | 58 | iter_path([{Key, Val}|Rest], Acc) -> 59 | iter_path(Rest, list_to_binary([Acc, $&, to_binary(Key), $=, to_urlencode(Val)])). 60 | 61 | 62 | 63 | %% =================================================================== 64 | %% Create spec parser 65 | %% =================================================================== 66 | 67 | %% @private 68 | all_opts() -> 69 | [ 70 | attach, add_hosts, 71 | cap_add, cap_drop, cgroup_parent, cidfile, cmds, cpu_set, 72 | cpu_set_cpus, cpu_shares, 73 | devices, dns, dns_search, domain_name, 74 | env, entrypoints, expose, 75 | hostname, 76 | interactive, 77 | labels, links, lxc_confs, log_config, 78 | mac_address, memory, memory_swap, 79 | net, 80 | publish_all, publish, privilege, 81 | read_only, restart, 82 | security_opts, 83 | tty, 84 | ulimits, user, 85 | volumes, volumes_from, 86 | workdir 87 | ]. 88 | 89 | %% @private 90 | -spec create_spec(binary(), nkdocker:create_opts()) -> 91 | {ok, map()} | {error, term()}. 92 | 93 | create_spec(Vsn, Map) -> 94 | try 95 | create_spec(Vsn, maps:to_list(Map), create_default_a(Vsn), create_default_b(Vsn)) 96 | catch 97 | throw:TError -> {error, TError} 98 | end. 99 | 100 | 101 | %% @private 102 | create_spec(_, [], AccA, AccB) -> 103 | {ok, AccA#{'HostConfig' => AccB}}; 104 | 105 | create_spec(Vsn, [{attach, List}|Rest], AccA, AccB) when is_list(List) -> 106 | {In, Out, Err} = case lists:sort(List) of 107 | [stdin] -> {true, false, false}; 108 | [stdout] -> {false, true, true}; 109 | [stderr] -> {false, true, true}; 110 | [stdin, stdout] -> {true, true, false}; 111 | [stderr, stdin] -> {true, false, true}; 112 | [stderr, stdout] -> {false, true, true}; 113 | [stderr, stdin, stdout] -> {true, true, true}; 114 | _ -> throw({invalid_option, {attach, List}}) 115 | end, 116 | AccA1 = AccA#{'AttachStdin'=>In, 'AttachStdout'=>Out, 'AttachStderr'=>Err}, 117 | create_spec(Vsn, Rest, AccA1, AccB); 118 | 119 | create_spec(Vsn, [{add_hosts, List}|Rest], AccA, AccB) when is_list(List) -> 120 | List1 = lists:map( 121 | fun(Term) -> 122 | case Term of 123 | {Host, Ip} -> list_to_binary([to_binary(Host), $:, to_host(Ip)]); 124 | _ -> throw({invalid_option, {add_hosts, List}}) 125 | end 126 | end, 127 | List), 128 | create_spec(Vsn, Rest, AccA, AccB#{'ExtraHosts'=>List1}); 129 | 130 | create_spec(Vsn, [{cgroup_parent, Text}|Rest], AccA, AccB) -> 131 | create_spec(Vsn, Rest, AccA, AccB#{'CgroupParent'=>to_binary(Text)}); 132 | 133 | create_spec(Vsn, [{cidfile, Text}|Rest], AccA, AccB) -> 134 | create_spec(Vsn, Rest, AccA, AccB#{'ContainerIDFile'=>to_binary(Text)}); 135 | 136 | create_spec(Vsn, [{cpu_shares, Int}|Rest], AccA, AccB) when is_integer(Int) -> 137 | AccA1 = AccA#{'CpuShares'=>Int}, 138 | AccB1 = case Vsn >= <<"1.18">> of 139 | true -> AccB#{'CpuShares'=>Int}; 140 | false -> AccB 141 | end, 142 | create_spec(Vsn, Rest, AccA1, AccB1); 143 | 144 | create_spec(Vsn, [{cpu_set, Text}|Rest], AccA, AccB) -> 145 | create_spec(Vsn, Rest, AccA#{'Cpuset'=>to_binary(Text)}, AccB); 146 | 147 | create_spec(Vsn, [{cpu_set_cpus, Text}|Rest], AccA, AccB) -> 148 | AccA1 = AccA#{'Cpuset'=>to_binary(Text)}, 149 | AccB1 = AccB#{'CpusetCpus'=>to_binary(Text)}, 150 | create_spec(Vsn, Rest, AccA1, AccB1); 151 | 152 | create_spec(Vsn, [{cap_add, List}|Rest], AccA, AccB) when is_list(List) -> 153 | create_spec(Vsn, Rest, AccA, AccB#{'CapAdd'=>to_list(List)}); 154 | 155 | create_spec(Vsn, [{cap_drop, List}|Rest], AccA, AccB) when is_list(List) -> 156 | create_spec(Vsn, Rest, AccA, AccB#{'CapDrop'=>to_list(List)}); 157 | 158 | create_spec(Vsn, [{cmds, List}|Rest], AccA, AccB) when is_list(List) -> 159 | create_spec(Vsn, Rest, AccA#{'Cmd'=>to_list(List)}, AccB); 160 | 161 | create_spec(Vsn, [{devices, List}|Rest], AccA, AccB) when is_list(List) -> 162 | Devices = lists:map( 163 | fun(Term) -> 164 | {PathHost, PathCont, Perm} = case Term of 165 | {A, B, C} -> {A, B, C}; 166 | {A, B} -> {A, B, <<"rwm">>}; 167 | A -> {A, A, <<"rwm">>} 168 | end, 169 | #{ 170 | 'PathOnHost' => to_binary(PathHost), 171 | 'PathInContainer' => to_binary(PathCont), 172 | 'CgroupPermissions' => to_binary(Perm) 173 | } 174 | end, 175 | List), 176 | create_spec(Vsn, Rest, AccA, AccB#{'Devices'=>Devices}); 177 | 178 | create_spec(Vsn, [{dns, List}|Rest], AccA, AccB) when is_list(List) -> 179 | create_spec(Vsn, Rest, AccA, AccB#{'Dns'=>to_list(List)}); 180 | 181 | create_spec(Vsn, [{dns_search, List}|Rest], AccA, AccB) when is_list(List) -> 182 | create_spec(Vsn, Rest, AccA, AccB#{'DnsSearch'=>to_list(List)}); 183 | 184 | create_spec(Vsn, [{domain_name, Text}|Rest], AccA, AccB) -> 185 | create_spec(Vsn, Rest, AccA#{'Domainname'=>to_binary(Text)}, AccB); 186 | 187 | create_spec(Vsn, [{env, List}|Rest], AccA, AccB) when is_list(List) -> 188 | List1 = lists:map( 189 | fun(Term) -> 190 | case Term of 191 | {K, V} -> list_to_binary([to_binary(K), $=, to_binary(V)]); 192 | _ -> throw({invalid_option, {env, List}}) 193 | end 194 | end, 195 | List), 196 | create_spec(Vsn, Rest, AccA#{'Env'=>List1}, AccB); 197 | 198 | create_spec(Vsn, [{entrypoints, List}|Rest], AccA, AccB) when is_list(List) -> 199 | create_spec(Vsn, Rest, AccA#{'Entrypoint'=>to_list(List)}, AccB); 200 | 201 | create_spec(Vsn, [{expose, List}|Rest], AccA, AccB) when is_list(List) -> 202 | List1 = lists:map( 203 | fun(Term) -> 204 | case Term of 205 | {Port, tcp} when is_integer(Port)-> {to_port(Port, tcp), #{}}; 206 | {Port, udp} when is_integer(Port)-> {to_port(Port, udp), #{}}; 207 | Port when is_integer(Port) -> {to_port(Port, tcp), #{}}; 208 | _ -> throw({invalid_option, {expose, List}}) 209 | end 210 | end, 211 | List), 212 | create_spec(Vsn, Rest, AccA#{'ExposedPorts'=>maps:from_list(List1)}, AccB); 213 | 214 | create_spec(Vsn, [{hostname, Text}|Rest], AccA, AccB) -> 215 | create_spec(Vsn, Rest, AccA#{'Hostname'=>to_binary(Text)}, AccB); 216 | 217 | create_spec(Vsn, [{interactive, true}|Rest], AccA, AccB) -> 218 | create_spec(Vsn, Rest, AccA#{ 219 | 'AttachStdin' => true, 220 | 'OpenStdin' => true, 221 | 'StdinOnce' => true 222 | }, AccB); 223 | 224 | create_spec(Vsn, [{interactive, false}|Rest], AccA, AccB) -> 225 | create_spec(Vsn, Rest, AccA, AccB); 226 | 227 | create_spec(Vsn, [{image, Text}|Rest], AccA, AccB) -> 228 | create_spec(Vsn, Rest, AccA#{'Image'=>to_binary(Text)}, AccB); 229 | 230 | create_spec(Vsn, [{ipc, Text}|Rest], AccA, AccB) -> 231 | create_spec(Vsn, Rest, AccA#{'IpcMode'=>to_binary(Text)}, AccB); 232 | 233 | create_spec(Vsn, [{labels, List}|Rest], AccA, AccB) when is_list(List) -> 234 | List1 = lists:map( 235 | fun(Term) -> 236 | case Term of 237 | {K, V} -> {to_binary(K), to_binary(V)}; 238 | _ -> throw({invalid_option, {labels, List}}) 239 | end 240 | end, 241 | List), 242 | create_spec(Vsn, Rest, AccA#{'Labels'=>maps:from_list(List1)}, AccB); 243 | 244 | create_spec(Vsn, [{links, List}|Rest], AccA, AccB) -> 245 | List1 = lists:map( 246 | fun(Term) -> 247 | case Term of 248 | {Cont, Alias} -> list_to_binary([to_binary(Cont), $:, to_binary(Alias)]); 249 | _ -> throw({invalid_option, {links, List}}) 250 | end 251 | end, 252 | List), 253 | create_spec(Vsn, Rest, AccA, AccB#{'Links'=>List1}); 254 | 255 | create_spec(Vsn, [{lxc_confs, List}|Rest], AccA, AccB) when is_list(List) -> 256 | List1 = lists:map( 257 | fun(Term) -> 258 | case Term of 259 | {K, V} -> #{'Key'=>to_binary(K), 'Val'=>to_binary(V)}; 260 | _ -> throw({invalid_option, {lxc_confs, List}}) 261 | end 262 | end, 263 | List), 264 | create_spec(Vsn, Rest, AccA, AccB#{'LxcConf'=>List1}); 265 | 266 | create_spec(Vsn, [{log_config, {Type, Config}}|Rest], AccA, AccB) when is_map(Config) -> 267 | Config = #{'Type'=>to_binary(Type), 'Config'=>Config}, 268 | create_spec(Vsn, Rest, AccA, AccB#{'LogConfig'=>Config}); 269 | 270 | create_spec(Vsn, [{log_config, Type}|Rest], AccA, AccB) -> 271 | Config = #{'Type'=>to_binary(Type), 'Config'=>null}, 272 | create_spec(Vsn, Rest, AccA, AccB#{'LogConfig'=>Config}); 273 | 274 | create_spec(Vsn, [{mac_address, Text}|Rest], AccA, AccB) -> 275 | create_spec(Vsn, Rest, AccA#{'MacAddress'=>to_binary(Text)}, AccB); 276 | 277 | create_spec(Vsn, [{memory, Int}|Rest], AccA, AccB) when is_integer(Int) -> 278 | AccA1 = AccA#{'Memory'=>Int}, 279 | AccB1 = case Vsn >= <<"1.18">> of 280 | true -> AccB#{'Memory'=>Int}; 281 | false -> AccB 282 | end, 283 | create_spec(Vsn, Rest, AccA1, AccB1); 284 | 285 | create_spec(Vsn, [{memory_swap, Int}|Rest], AccA, AccB) when is_integer(Int) -> 286 | AccA1 = AccA#{'MemorySwap'=>Int}, 287 | AccB1 = case Vsn >= <<"1.18">> of 288 | true -> AccB#{'MemorySwap'=>Int}; 289 | false -> AccB 290 | end, 291 | create_spec(Vsn, Rest, AccA1, AccB1); 292 | 293 | create_spec(Vsn, [{net, Text}|Rest], AccA, AccB) -> 294 | create_spec(Vsn, Rest, AccA, AccB#{'NetworkMode'=>to_binary(Text)}); 295 | 296 | create_spec(Vsn, [{publish_all, Bool}|Rest], AccA, AccB) when is_boolean(Bool) -> 297 | create_spec(Vsn, Rest, AccA, AccB#{'PublishAllPorts'=>Bool}); 298 | 299 | create_spec(Vsn, [{publish, List}|Rest], AccA, AccB) when is_list(List) -> 300 | Expose = maps:get('ExposedPorts', AccA, #{}), 301 | {Expose1, Bind1} = lists:foldl( 302 | fun(Term, {ExpAcc, BindAcc}) -> 303 | {ContPort, HostPort, Ip} = case Term of 304 | {A, B, C} when is_integer(B) -> {A, to_binary(B), to_host(C)}; 305 | {A, B} when is_integer(B) -> {A, to_binary(B), <<>>}; 306 | A -> {A, <<>>, <<>>} 307 | end, 308 | Cont = case ContPort of 309 | {D, tcp} when is_integer(D) -> to_port(D, tcp); 310 | {D, udp} when is_integer(D) -> to_port(D, udp); 311 | D when is_integer(D) -> to_port(D, tcp); 312 | _ -> throw({invalid_option, {publish, List}}) 313 | end, 314 | { 315 | maps:put(Cont, #{}, ExpAcc), 316 | maps:put(Cont, 317 | lists:usort( 318 | [ #{'HostIp'=>Ip, 'HostPort'=>HostPort} | 319 | maps:get(Cont, BindAcc, [])]), 320 | BindAcc) 321 | } 322 | end, 323 | {Expose, #{}}, 324 | List), 325 | create_spec(Vsn, Rest, AccA#{'ExposedPorts'=>Expose1}, AccB#{'PortBindings'=>Bind1}); 326 | 327 | % create_spec(Vsn, [{pid, Text}|Rest], AccA, AccB) -> 328 | % create_spec(Vsn, Rest, AccA, AccB#{'PidMode'=>to_binary(Text)}); 329 | 330 | create_spec(Vsn, [{privileged, Bool}|Rest], AccA, AccB) when is_boolean(Bool) -> 331 | create_spec(Vsn, Rest, AccA, AccB#{'Privileged'=>Bool}); 332 | 333 | create_spec(Vsn, [{read_only, Bool}|Rest], AccA, AccB) when is_boolean(Bool) -> 334 | create_spec(Vsn, Rest, AccA, AccB#{'ReadonlyRootfs'=>Bool}); 335 | 336 | create_spec(Vsn, [{restart, {on_failure, Retry}}|Rest], AccA, AccB) when is_integer(Retry) -> 337 | Restart = #{'Name'=><<"on-failure">>, 'MaximumRetryCount'=>Retry}, 338 | create_spec(Vsn, Rest, AccA, AccB#{'RestartPolicy'=>Restart}); 339 | 340 | create_spec(Vsn, [{restart, Name}|Rest], AccA, AccB) when Name==no; Name==always -> 341 | Restart = #{'Name'=>to_binary(Name), 'MaximumRetryCount'=>0}, 342 | create_spec(Vsn, Rest, AccA, AccB#{'RestartPolicy'=>Restart}); 343 | 344 | create_spec(Vsn, [{security_opts, List}|Rest], AccA, AccB) when is_list(List) -> 345 | create_spec(Vsn, Rest, AccA, AccB#{'SecurityOpt'=>to_list(List)}); 346 | 347 | create_spec(Vsn, [{tty, Bool}|Rest], AccA, AccB) when is_boolean(Bool) -> 348 | create_spec(Vsn, Rest, AccA#{'Tty'=>Bool}, AccB); 349 | 350 | create_spec(Vsn, [{ulimits, List}|Rest], AccA, AccB) when is_list(List) -> 351 | Limits = lists:map( 352 | fun(Term) -> 353 | case Term of 354 | {Name, Soft, Hard} when is_integer(Soft), is_integer(Hard) -> 355 | #{'Name'=>to_binary(Name), 'Soft'=>Soft, 'Hard'=>Hard}; 356 | _ -> 357 | throw({invalid_option, ulimits}) 358 | end 359 | end, 360 | List), 361 | create_spec(Vsn, Rest, AccA, AccB#{'Ulimits'=>Limits}); 362 | 363 | create_spec(Vsn, [{user, String}|Rest], AccA, AccB) -> 364 | create_spec(Vsn, Rest, AccA#{'User'=>to_binary(String)}, AccB); 365 | 366 | create_spec(Vsn, [{volumes, List}|Rest], AccA, AccB) when is_list(List) -> 367 | Volumes = maps:get('Volumes', AccA, #{}), 368 | {Volumes1, Binds1} = lists:foldl( 369 | fun(Term, {VolAcc, BindAcc}) -> 370 | case Term of 371 | {Host, Cont, ro} -> 372 | {VolAcc, [list_to_binary([Host, $:, Cont, ":ro"])|BindAcc]}; 373 | {Host, Cont} -> 374 | {VolAcc, [list_to_binary([Host, $:, Cont])|BindAcc]}; 375 | Cont -> 376 | {maps:put(to_binary(Cont), #{}, VolAcc), BindAcc} 377 | end 378 | end, 379 | {Volumes, []}, 380 | List), 381 | create_spec(Vsn, Rest, AccA#{'Volumes'=>Volumes1}, AccB#{'Binds'=>Binds1}); 382 | 383 | create_spec(Vsn, [{volumes_from, List}|Rest], AccA, AccB) when is_list(List) -> 384 | Volumes = lists:foldl( 385 | fun(Term, Acc) -> 386 | case Term of 387 | {Cont, ro} -> [list_to_binary([Cont, ":ro"])|Acc]; 388 | {Cont, rw} -> [list_to_binary([Cont, ":rw"])|Acc]; 389 | Cont -> [to_binary(Cont)|Acc] 390 | end 391 | end, 392 | [], 393 | List), 394 | create_spec(Vsn, Rest, AccA, AccB#{'VolumesFrom'=>Volumes}); 395 | 396 | create_spec(Vsn, [{workdir, Text}|Rest], AccA, AccB) -> 397 | create_spec(Vsn, Rest, AccA#{'WorkingDir'=>to_binary(Text)}, AccB); 398 | 399 | create_spec(Vsn, [{Key, _}=Term|Rest], AccA, AccB) -> 400 | case lists:member(Key, all_opts()) of 401 | true -> throw({invalid_option, Term}); 402 | false -> create_spec(Vsn, Rest, AccA, AccB) 403 | end. 404 | 405 | 406 | 407 | %% =================================================================== 408 | %% Utilities 409 | %% =================================================================== 410 | 411 | 412 | %% @private 413 | to_list(Text) when is_list(Text) -> 414 | [to_binary(Term) || Term <- Text]. 415 | 416 | 417 | %% @private 418 | to_urlencode(Text) -> 419 | to_binary(http_uri:encode(nklib_util:to_list(Text))). 420 | 421 | 422 | %% @private 423 | to_port(Port, Transp) -> 424 | <<(to_binary(Port))/binary, $/, (to_binary(Transp))/binary>>. 425 | 426 | 427 | %% @private 428 | create_default(Vsn) -> 429 | (create_default_a(Vsn))#{'HostConfig'=>create_default_b(Vsn)}. 430 | 431 | 432 | %% @private Default create using client v1.5 433 | create_default_a(<<"1.17">>) -> 434 | #{ 435 | 'AttachStderr' => true, 436 | 'AttachStdin' => false, 437 | 'AttachStdout' => true, 438 | 'Cmd' => null, 439 | 'CpuShares' => 0, 440 | 'Cpuset' => <<>>, 441 | 'Domainname' => <<>>, 442 | 'Entrypoint' => null, 443 | 'Env' => [], 444 | 'ExposedPorts' => #{}, 445 | 'Hostname' => <<>>, 446 | % 'Image' => 'ubuntu', 447 | 'MacAddress' => <<>>, 448 | 'Memory' => 0, 449 | 'MemorySwap' => 0, 450 | 'NetworkDisabled' => false, 451 | 'OnBuild' => null, 452 | 'OpenStdin' => false, 453 | 'PortSpecs' => null, 454 | 'StdinOnce' => false, 455 | 'Tty' => false, 456 | 'User' => <<>>, 457 | 'Volumes' => #{}, 458 | 'WorkingDir' => <<>> 459 | }; 460 | 461 | % >= 1.18 462 | create_default_a(_) -> 463 | (create_default_a(<<"1.17">>))#{ 464 | 'Labels' => #{} 465 | }. 466 | 467 | 468 | 469 | 470 | create_default_b(<<"1.17">>) -> 471 | #{ 472 | 'Binds' => null, 473 | 'CapAdd' => null, 474 | 'CapDrop' => null, 475 | 'ContainerIDFile' => <<>>, 476 | 'Devices' => [], 477 | 'Dns' => null, 478 | 'DnsSearch' => null, 479 | 'ExtraHosts' => null, 480 | 'IpcMode' => <<>>, 481 | 'Links' => null, 482 | 'LxcConf' => [], 483 | 'NetworkMode' => 'bridge', 484 | 'PidMode' => <<>>, % not documented? 485 | 'PortBindings' => #{}, 486 | 'Privileged' => false, 487 | 'PublishAllPorts' => false, 488 | 'ReadonlyRootfs' => false, 489 | 'RestartPolicy' => #{'MaximumRetryCount' => 0,'Name' => <<>>}, 490 | 'SecurityOpt' => null, 491 | 'VolumesFrom' => null 492 | }; 493 | 494 | create_default_b(_) -> 495 | (create_default_b(<<"1.17">>))#{ 496 | 'CgroupParent' => <<>>, 497 | 'CpusetCpus' => <<>>, 498 | 'CpuShares' => 0, 499 | 'LogConfig' => #{'Config' => null,'Type' => <<>>}, 500 | 'Memory' => 0, 501 | 'MemorySwap' => 0, 502 | 'RestartPolicy' => #{'MaximumRetryCount' => 0,'Name' => <<"no">>}, 503 | 'Ulimits' => null 504 | }. 505 | 506 | 507 | 508 | 509 | 510 | 511 | %% =================================================================== 512 | %% Test 513 | %% =================================================================== 514 | 515 | 516 | parse_text() -> 517 | Spec = #{ 518 | attach => [stdin, stdout], 519 | add_hosts => [{"host1", {1,2,3,4}}, {"host2", <<"5.6.7.8">>}], 520 | cap_add => ["cap1", "cap2"], 521 | cap_drop => ["drop1", "drop2"], 522 | cgroup_parent => "path", 523 | cidfile => "mycidfile", 524 | cmds => ["cmd1"], 525 | cpu_set => <<"0,1">>, 526 | cpu_set_cpus => <<"2,3">>, 527 | cpu_shares => 500, 528 | devices => ["p1", {<<"p2">>, "p3"}, {"p4", "p5", "p6"}], 529 | dns => ["dns1", "dns2"], 530 | dns_search => ["dns3", <<"dns4">>], 531 | domain_name => "domain", 532 | env => [{"env1", "val1"}, {"env2", "val2"}], 533 | entrypoints => ["a", "b"], 534 | expose => [1000, {1001, tcp}, {1002, udp}], 535 | hostname => "hostname1", 536 | interactive => true, 537 | image => <<"image1">>, 538 | labels => [{"lk", lv}], 539 | links => [{"link1", "value1"}], 540 | lxc_confs => [{"lx1", "lv1"}], 541 | log_config => "none", 542 | mac_address => "1:2:3:4:5:6:7:8", 543 | memory => 1000, 544 | memory_swap => -1, 545 | net => none, 546 | publish_all => true, 547 | publish => [2000, {2001, udp}, {2002, 2102}, {{2003, udp}, 2103, {1,2,3,4}}], 548 | % pid_mode => <<"what_is_this?">>, 549 | privileged => true, 550 | read_only => true, 551 | restart => {on_failure, 5}, 552 | security_opts => ["s1", "s2"], 553 | tty => true, 554 | ulimits => [{"name", 1, 2}], 555 | user => "user", 556 | volumes => ["vol1", {<<"vol2">>, <<"vol3">>}, {"vol4", "vol5", ro}], 557 | volumes_from => ["from1", {"from2", ro}], 558 | workdir => "work" 559 | }, 560 | {ok, Op} = create_spec(<<"1.18">>, Spec), 561 | #{ 562 | 'AttachStdin' := true, 563 | 'AttachStdout' := true, 564 | 'AttachStderr' := false, 565 | 'Cmd' := [<<"cmd1">>], 566 | % 'Cpuset' := <<"0,1">>, % Ignored in v1.18, next one overwrites it! 567 | 'Cpuset' := <<"2,3">>, 568 | 'CpuShares' := 500, 569 | 'Domainname' := <<"domain">>, 570 | 'Env' := [<<"env1=val1">>,<<"env2=val2">>], 571 | 'Entrypoint' := [<<"a">>,<<"b">>], 572 | 'ExposedPorts' := #{ 573 | <<"1000/tcp">> := #{}, 574 | <<"1001/tcp">> := #{}, 575 | <<"1002/udp">> := #{}, 576 | <<"2000/tcp">> := #{}, 577 | <<"2001/udp">> := #{}, 578 | <<"2002/tcp">> := #{}, 579 | <<"2003/udp">> := #{} 580 | }, 581 | 'Hostname' := <<"hostname1">>, 582 | 'Image' := <<"image1">>, 583 | 'Labels' := #{ <<"lk">> := <<"lv">>}, 584 | 'MacAddress' := <<"1:2:3:4:5:6:7:8">>, 585 | 'Memory' := 1000, 586 | 'MemorySwap' := -1, 587 | 'NetworkDisabled' := false, 588 | 'OpenStdin' := true, 589 | 'StdinOnce' := true, 590 | 'Tty' := true, 591 | 'User' := <<"user">>, 592 | 'Volumes' := #{<<"vol1">> := #{}}, 593 | 'WorkingDir' := <<"work">>, 594 | 'HostConfig' := #{ 595 | 'Binds' := [<<"vol4:vol5:ro">>, <<"vol2:vol3">>], 596 | 'CapAdd' := [<<"cap1">>,<<"cap2">>], 597 | 'CapDrop' := [<<"drop1">>,<<"drop2">>], 598 | 'CpusetCpus' := <<"2,3">>, 599 | 'CpuShares' := 500, 600 | 'CgroupParent' := <<"path">>, 601 | 'ContainerIDFile' := <<"mycidfile">>, 602 | 'Devices' := [ 603 | #{ 604 | 'CgroupPermissions' := <<"rwm">>, 605 | 'PathInContainer' := <<"p1">>, 606 | 'PathOnHost' := <<"p1">> 607 | }, 608 | #{ 609 | 'CgroupPermissions' := <<"rwm">>, 610 | 'PathInContainer' := <<"p3">>, 611 | 'PathOnHost' := <<"p2">> 612 | }, 613 | #{ 614 | 'CgroupPermissions' := <<"p6">>, 615 | 'PathInContainer' := <<"p5">>, 616 | 'PathOnHost' := <<"p4">> 617 | } 618 | ], 619 | 'Dns' := [<<"dns1">>,<<"dns2">>], 620 | 'DnsSearch' := [<<"dns3">>,<<"dns4">>], 621 | 'ExtraHosts' := [<<"host1:1.2.3.4">>,<<"host2:5.6.7.8">>], 622 | 'Links' := [<<"link1:value1">>], 623 | 'LxcConf' := [#{'Key' := <<"lx1">>,'Val' := <<"lv1">>}], 624 | 'LogConfig' := #{'Type' := <<"none">>, 'Config' := null}, 625 | 'Memory' := 1000, 626 | 'MemorySwap' := -1, 627 | 'NetworkMode' := <<"none">>, 628 | 'PortBindings' := #{ 629 | <<"2000/tcp">> := #{'HostIp' := <<>>,'HostPort' := <<>>}, 630 | <<"2001/udp">> := #{'HostIp' := <<>>,'HostPort' := <<>>}, 631 | <<"2002/tcp">> := #{'HostIp' := <<>>,'HostPort' := <<"2102">>}, 632 | <<"2003/udp">> := #{'HostIp' := <<"1.2.3.4">>,'HostPort' := <<"2103">>} 633 | }, 634 | 'Privileged' := true, 635 | 'PublishAllPorts' := true, 636 | 'ReadonlyRootfs' := true, 637 | 'RestartPolicy' := #{'MaximumRetryCount' := 5,'Name' := <<"on-failure">>}, 638 | 'SecurityOpt' := [<<"s1">>,<<"s2">>], 639 | 'Ulimits' := [#{'Hard' := 2,'Name' := <<"name">>,'Soft' := 1}], 640 | 'VolumesFrom' := [<<"from2:ro">>, <<"from1">>] 641 | } 642 | } = Op. 643 | 644 | 645 | -------------------------------------------------------------------------------- /src/nkdocker_protocol.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Protocol behaviour 22 | -module(nkdocker_protocol). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(nkpacket_protocol). 25 | 26 | -export([transports/1]). 27 | -export([conn_init/1, conn_parse/3, conn_encode/3]). 28 | 29 | -include_lib("nklib/include/nklib.hrl"). 30 | 31 | -record(state, { 32 | notify :: pid(), 33 | buff = <<>> :: binary(), 34 | streams = [] :: [reference()], 35 | next = head :: head | {body, non_neg_integer()} | chunked | stream 36 | }). 37 | 38 | 39 | 40 | %% =================================================================== 41 | %% Protocol callbacks 42 | %% =================================================================== 43 | 44 | 45 | %% @private 46 | -spec transports(nklib:scheme()) -> 47 | [nkpacket:transport()]. 48 | 49 | transports(_) -> [tls]. 50 | 51 | 52 | %% @private 53 | -spec conn_init(nkpacket:nkport()) -> 54 | {ok, #state{}}. 55 | 56 | conn_init(NkPort) -> 57 | {ok, _SrvId, {notify, Pid}} = nkpacket:get_user(NkPort), 58 | % lager:notice("Protocol CONN init: ~p (~p)", [NkPort, self()]), 59 | {ok, #state{notify=Pid}}. 60 | 61 | 62 | %% @private 63 | -spec conn_parse(term()|close, nkpacket:nkport(), #state{}) -> 64 | {ok, #state{}} | {stop, normal, #state{}}. 65 | 66 | conn_parse(close, _NkPort, State) -> 67 | {ok, State}; 68 | 69 | conn_parse(Data, _NkPort, State) -> 70 | handle(Data, State). 71 | 72 | 73 | %% @private 74 | -spec conn_encode(term(), nkpacket:nkport(), #state{}) -> 75 | {ok, nkpacket:raw_msg(), #state{}} | {error, term(), #state{}} | 76 | {stop, Reason::term()}. 77 | 78 | conn_encode({http, Ref, Method, Path, Headers, Body}, _NkPort, State) -> 79 | request(Ref, Method, Path, Headers, Body, State); 80 | 81 | conn_encode({data, Ref, Data}, _NkPort, State) -> 82 | data(Ref, Data, State). 83 | 84 | 85 | %% =================================================================== 86 | %% HTTP handle 87 | %% =================================================================== 88 | 89 | %% @private 90 | -spec request(term(), binary(), binary(), list(), iolist(), #state{}) -> 91 | {ok, iolist(), #state{}}. 92 | 93 | request(Ref, Method, Path, Headers, Body, #state{streams=Streams}=State) -> 94 | Headers1 = case is_map(Body) of 95 | true -> [{<<"content-type">>, <<"application/json">>}|Headers]; 96 | false -> Headers 97 | end, 98 | Body1 = case is_map(Body) of 99 | true -> nklib_json:encode(Body); 100 | false -> Body 101 | end, 102 | Headers2 = [ 103 | {<<"content-length">>, integer_to_list(iolist_size(Body1))} 104 | | Headers1 105 | ], 106 | State1 = State#state{streams = Streams ++ [Ref]}, 107 | RawMsg = cow_http:request(Method, Path, 'HTTP/1.1', Headers2), 108 | {ok, [RawMsg, Body1], State1}. 109 | 110 | 111 | %% @private 112 | -spec data(term(), iolist(), #state{}) -> 113 | {ok, iolist(), #state{}} | {error, invalid_ref, #state{}}. 114 | 115 | data(_Ref, _Data, #state{streams=[]}=State) -> 116 | {error, invalid_ref, State}; 117 | 118 | data(Ref, Data, #state{streams=Streams}=State) -> 119 | case lists:last(Streams) of 120 | Ref -> 121 | {ok, Data, State}; 122 | _ -> 123 | {error, invalid_ref, State} 124 | end. 125 | 126 | 127 | -spec handle(binary(), #state{}) -> 128 | {ok, #state{}} | {stop, term(), #state{}}. 129 | 130 | handle(<<>>, State) -> 131 | {ok, State}; 132 | 133 | handle(_, #state{streams=[]}=State) -> 134 | {stop, normal, State}; 135 | 136 | handle(Data, #state{next=head, buff=Buff}=State) -> 137 | Data1 = << Buff/binary, Data/binary >>, 138 | case binary:match(Data1, <<"\r\n\r\n">>) of 139 | nomatch -> 140 | {ok, State#state{buff=Data1}}; 141 | {_, _} -> 142 | handle_head(Data1, State#state{buff = <<>>}) 143 | end; 144 | 145 | handle(Data, #state{next={body, Length}}=State) -> 146 | #state{buff=Buff, streams=[Ref|_], notify=Pid} = State, 147 | Data1 = << Buff/binary, Data/binary>>, 148 | case byte_size(Data1) of 149 | Length -> 150 | Pid ! {nkdocker, Ref, {body, Data1}}, 151 | {ok, do_next(State)}; 152 | Size when Size < Length -> 153 | {ok, State#state{buff=Data1}}; 154 | _ -> 155 | {Data2, Rest} = erlang:split_binary(Data1, Length), 156 | Pid ! {nkdocker, Ref, {body, Data2}}, 157 | handle(Rest, do_next(State)) 158 | end; 159 | 160 | handle(Data, #state{next=chunked}=State) -> 161 | #state{buff=Buff, streams=[Ref|_], notify=Pid} = State, 162 | Data1 = << Buff/binary, Data/binary>>, 163 | case parse_chunked(Data1) of 164 | {data, <<>>, Rest} -> 165 | Pid ! {nkdocker, Ref, {body, <<>>}}, 166 | handle(Rest, do_next(State)); 167 | {data, Chunk, Rest} -> 168 | Pid ! {nkdocker, Ref, {chunk, Chunk}}, 169 | handle(Rest, State#state{buff = <<>>}); 170 | more -> 171 | {ok, State#state{buff=Data1}} 172 | end; 173 | 174 | handle(Data, #state{next=stream, streams=[Ref|_], notify=Pid}=State) -> 175 | Pid ! {nkdocker, Ref, {chunk, Data}}, 176 | {ok, State}. 177 | 178 | 179 | %% @private 180 | -spec handle_head(binary(), #state{}) -> 181 | {ok, #state{}}. 182 | 183 | handle_head(Data, #state{streams=[Ref|_], notify=Pid}=State) -> 184 | {_Version, Status, _, Rest} = cow_http:parse_status_line(Data), 185 | {Headers, Rest2} = cow_http:parse_headers(Rest), 186 | Pid ! {nkdocker, Ref, {head, Status, Headers}}, 187 | Remaining = case lists:keyfind(<<"content-length">>, 1, Headers) of 188 | {_, <<"0">>} -> 189 | 0; 190 | {_, Length} -> 191 | cow_http_hd:parse_content_length(Length); 192 | false when Status==204; Status==304 -> 193 | 0; 194 | false -> 195 | case lists:keyfind(<<"transfer-encoding">>, 1, Headers) of 196 | false -> 197 | stream; 198 | {_, TE} -> 199 | case cow_http_hd:parse_transfer_encoding(TE) of 200 | [<<"chunked">>] -> chunked; 201 | [<<"identity">>] -> 0 202 | end 203 | end 204 | end, 205 | State1 = case Remaining of 206 | 0 -> 207 | Pid ! {nkdocker, Ref, {body, <<>>}}, 208 | do_next(State); 209 | chunked -> 210 | State#state{next=chunked}; 211 | stream -> 212 | State#state{next=stream}; 213 | _ -> 214 | State#state{next={body, Remaining}} 215 | end, 216 | handle(Rest2, State1). 217 | 218 | 219 | 220 | %% @private 221 | -spec parse_chunked(binary()) -> 222 | {ok, binary(), binary()} | more. 223 | 224 | parse_chunked(S) -> 225 | case find_chunked_length(S, []) of 226 | {ok, Length, Data} -> 227 | FullLength = Length + 2, 228 | case byte_size(Data) of 229 | FullLength -> 230 | <> = Data, 231 | {data, Data1, <<>>}; 232 | Size when Size < FullLength -> 233 | more; 234 | _ -> 235 | {Data1, Rest} = erlang:split_binary(Data, FullLength), 236 | <> = Data1, 237 | {data, Data2, Rest} 238 | end; 239 | more -> 240 | more 241 | end. 242 | 243 | 244 | %% @private 245 | -spec find_chunked_length(binary(), string()) -> 246 | {ok, integer(), binary()} | more. 247 | 248 | find_chunked_length(<>, Acc) -> 249 | {V, _} = lists:foldl( 250 | fun(Ch, {Sum, Mult}) -> 251 | if 252 | Ch >= $0, Ch =< $9 -> {Sum + (Ch-$0)*Mult, 16*Mult}; 253 | Ch >= $a, Ch =< $f -> {Sum + (Ch-$a+10)*Mult, 16*Mult}; 254 | Ch >= $A, Ch =< $F -> {Sum + (Ch-$A+10)*Mult, 16*Mult} 255 | end 256 | end, 257 | {0, 1}, 258 | [C|Acc]), 259 | {ok, V, Rest}; 260 | 261 | find_chunked_length(<>, Acc) -> 262 | find_chunked_length(Rest, [C|Acc]); 263 | 264 | find_chunked_length(<<>>, _Acc) -> 265 | more. 266 | 267 | 268 | %% @private 269 | do_next(#state{streams=[_|Rest]}=State) -> 270 | State#state{next=head, buff= <<>>, streams=Rest}. 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /src/nkdocker_server.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc NkDOCKER Management Server 22 | -module(nkdocker_server). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(gen_server). 25 | 26 | -export([start_link/1, start/1, stop/1, cmd/5, data/3, finish/2, create_spec/2]). 27 | -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, 28 | handle_info/2]). 29 | -export_type([cmd_opts/0]). 30 | 31 | -include("nkdocker.hrl"). 32 | 33 | -type cmd_opts() :: 34 | #{ 35 | async => boolean(), 36 | force_new => boolean(), 37 | headers => [{binary(), binary()}], 38 | redirect => string(), % Send to file 39 | timeout => pos_integer(), % Reply and closes connection if reached 40 | % refresh => boolean(), % Automatic refresh 41 | chunks => boolean() % Send data in chunks 42 | }. 43 | 44 | 45 | -define(TIMEOUT, 5000). 46 | 47 | %% =================================================================== 48 | %% Public 49 | %% =================================================================== 50 | 51 | 52 | %% @doc Starts a docker connection 53 | -spec start_link(nkdocker:conn_opts()) -> 54 | {ok, pid()} | {error, term()}. 55 | 56 | start_link(Opts) -> 57 | gen_server:start_link(?MODULE, [Opts], []). 58 | 59 | 60 | %% @doc Starts a docker connection 61 | -spec start(nkdocker:conn_opts()) -> 62 | {ok, pid()} | {error, term()}. 63 | 64 | start(Opts) -> 65 | gen_server:start(?MODULE, [Opts], []). 66 | 67 | 68 | %% @doc Stops a docker connection 69 | -spec stop(pid()) -> 70 | ok. 71 | 72 | stop(Pid) -> 73 | gen_server:cast(Pid, stop). 74 | 75 | 76 | %% @doc Sends a message 77 | %% Every connection has a timeout, so it will never block 78 | -spec cmd(pid(), binary(), binary(), binary()|iolist()|map(), cmd_opts()) -> 79 | ok | {ok, term()|binary()} | {error, term()}. 80 | 81 | cmd(Pid, Verb, Path, Body, Opts) -> 82 | % Path1 = <<"/v1.17", Path/binary>>, 83 | Timeout = max(maps:get(timeout, Opts, 5000), 5000), 84 | case nklib_util:call(Pid, {cmd, Verb, Path, Body, Opts}, Timeout) of 85 | {error, {exit, {{timeout, _}, _}}} -> {error, call_timeout}; 86 | Other -> Other 87 | end. 88 | 89 | 90 | %% @doc Sends a in-message data 91 | -spec data(pid(), reference(), iolist()) -> 92 | ok | {error, term()}. 93 | 94 | data(Pid, Ref, Data) -> 95 | nklib_util:call(Pid, {data, Ref, Data}). 96 | 97 | 98 | %% @doc Finished an asynchronous command 99 | -spec finish(pid(), reference()) -> 100 | ok | {error, term()}. 101 | 102 | finish(Pid, Ref) -> 103 | nklib_util:call(Pid, {finish_async, Ref}). 104 | 105 | 106 | %% @doc Generates creation options 107 | -spec create_spec(pid(), nkdocker:create_opts()) -> 108 | {ok, map()} | {error, term()}. 109 | 110 | create_spec(Pid, Opts) -> 111 | case nklib_util:call(Pid, get_vsn) of 112 | {ok, Vsn} -> nkdocker_opts:create_spec(Vsn, Opts); 113 | {error, Error} -> {error, Error} 114 | end. 115 | 116 | 117 | % %% @private 118 | % refresh_fun(NkPort) -> 119 | % lager:warning("Refreshing connection"), 120 | % case nkpacket_connection_lib:raw_send(NkPort, <<"\r\n">>) of 121 | % ok -> true; 122 | % _ -> false 123 | % end. 124 | 125 | 126 | %% =================================================================== 127 | %% gen_server 128 | %% =================================================================== 129 | 130 | 131 | -record(cmd, { 132 | from_pid :: pid(), 133 | from_ref :: reference(), 134 | conn_pid :: pid(), 135 | conn_ref :: reference(), 136 | mode :: shared | exclusive | async | {redirect, file:io_device()}, 137 | use_chunks :: boolean(), 138 | status = 0 :: integer(), 139 | ct :: json | stream | undefined, 140 | chunks = [] :: [term()], 141 | stream_buff = <<>> :: binary(), 142 | user_mon :: reference() 143 | }). 144 | 145 | -record(state, { 146 | conn :: nkpacket:raw_connection(), 147 | conn_opts :: map(), 148 | host :: binary(), 149 | vsn :: binary(), 150 | cmds = [] :: [#cmd{}] 151 | }). 152 | 153 | 154 | %% @private 155 | -spec init(term()) -> 156 | {ok, #state{}} | {stop, term()}. 157 | 158 | init([Opts]) -> 159 | process_flag(trap_exit, true), %% Allow calls to terminate/2 160 | case nkdocker_util:get_conn_info(Opts) of 161 | {ok, #{ip:=Ip, port:=Port, proto:=Proto}=Opts2} -> 162 | Conn = {nkdocker_protocol, Proto, Ip, Port}, 163 | TLSKeys = nkpacket_util:tls_keys(), 164 | TLSOpts = maps:with(TLSKeys, Opts2), 165 | ConnOpts = TLSOpts#{ 166 | class => {nkdocker, self()}, 167 | monitor => self(), 168 | user => {notify, self()}, 169 | idle_timeout => ?TIMEOUT 170 | }, 171 | Host = list_to_binary([ 172 | nklib_util:to_host(Ip), 173 | <<":">>, 174 | nklib_util:to_binary(Port) 175 | ]), 176 | lager:info("NkDOCKER connecting to ~s, (~p)", [Host, ConnOpts]), 177 | case nkpacket:connect(Conn, ConnOpts) of 178 | {ok, _Pid} -> 179 | State = #state{ 180 | conn = Conn, 181 | conn_opts = ConnOpts, 182 | host = Host, 183 | cmds = [] 184 | }, 185 | case get_version(State) of 186 | {ok, Vsn} when Vsn >= <<"1.17">> -> 187 | {ok, State#state{vsn=Vsn}}; 188 | {ok, Vsn} -> 189 | {stop, {invalid_docker_version, Vsn}}; 190 | {error, Error} -> 191 | {stop, Error} 192 | end; 193 | {error, Error} -> 194 | {stop, {connection_error, Error}} 195 | end; 196 | {error, Error} -> 197 | {stop, Error} 198 | end. 199 | 200 | 201 | %% @private 202 | -spec handle_call(term(), {pid(), reference()}, #state{}) -> 203 | {noreply, #state{}} | {reply, term(), #state{}} | {stop, term(), term(), #state{}}. 204 | 205 | handle_call({cmd, Verb, Path, Body, Opts}, From, State) -> 206 | try 207 | Opts1 = case Opts of 208 | #{redirect:=File} -> 209 | case file:open(nklib_util:to_list(File), [write, raw]) of 210 | {ok, Port} -> 211 | Opts#{redirect:=Port}; 212 | {error, FileError} -> 213 | throw({could_not_open, File, FileError}) 214 | end; 215 | _ -> 216 | Opts 217 | end, 218 | Async = maps:get(async, Opts, false), 219 | case send(Verb, Path, Body, Opts1, From, State) of 220 | {ok, State1} when Async -> 221 | {reply, {async, element(2, From)}, State1}; 222 | {ok, State1} -> 223 | {noreply, State1}; 224 | {error, Error} -> 225 | {stop, normal, {error, Error}, State} 226 | end 227 | catch 228 | throw:Throw -> {reply, {error, Throw}, State} 229 | end; 230 | 231 | handle_call({data, Ref, Data}, _From, #state{cmds=Cmds}=State) -> 232 | case lists:keyfind(Ref, #cmd.from_ref, Cmds) of 233 | #cmd{mode=async, conn_pid=ConnPid}=Cmd -> 234 | case catch nkpacket_connection:send(ConnPid, {data, Ref, Data}) of 235 | ok -> 236 | {reply, ok, State}; 237 | Error -> 238 | State1 = send_stop(Cmd, {error, send_error}, State), 239 | {reply, {error, Error}, State1} 240 | end; 241 | false -> 242 | {reply, {error, unknown_ref}, State} 243 | end; 244 | 245 | handle_call({finish_async, Ref}, _, #state{cmds=Cmds}=State) -> 246 | case lists:keyfind(Ref, #cmd.from_ref, Cmds) of 247 | #cmd{mode=async}=Cmd -> 248 | State1 = send_stop(Cmd, {ok, user_stop}, State), 249 | {reply, ok, State1}; 250 | _ -> 251 | {reply, {error, unknown_ref}, State} 252 | end; 253 | 254 | handle_call(get_vsn, _From, #state{vsn=Vsn}=State) -> 255 | {reply, {ok, Vsn}, State}; 256 | 257 | handle_call(get_state, _From, State) -> 258 | {reply, State, State}; 259 | 260 | handle_call(Msg, _From, State) -> 261 | lager:error("Module ~p received unexpected call ~p", [?MODULE, Msg]), 262 | {noreply, State}. 263 | 264 | 265 | %% @private 266 | -spec handle_cast(term(), #state{}) -> 267 | {noreply, #state{}} | {stop, normal, #state{}}. 268 | 269 | handle_cast(stop, State) -> 270 | {stop, normal, State}; 271 | 272 | handle_cast(Msg, State) -> 273 | lager:error("Module ~p received unexpected cast ~p", [?MODULE, Msg]), 274 | {noreply, State}. 275 | 276 | 277 | %% @private 278 | -spec handle_info(term(), #state{}) -> 279 | {noreply, #state{}} | {stop, term(), #state{}}. 280 | 281 | handle_info({nkdocker, Ref, {head, Status, Headers}}, #state{cmds=Cmds}=State) -> 282 | lager:debug("Head: ~p, ~p", [Status, Headers]), 283 | case lists:keyfind(Ref, #cmd.from_ref, Cmds) of 284 | #cmd{}=Cmd -> 285 | CT = case nklib_util:get_value(<<"content-type">>, Headers) of 286 | <<"application/json">> -> json; 287 | <<"application/vnd.docker.raw-stream">> -> stream; 288 | _ -> undefined 289 | end, 290 | Cmd1 = Cmd#cmd{status=Status, ct=CT}, 291 | Cmds1 = lists:keystore(Ref, #cmd.from_ref, Cmds, Cmd1), 292 | {noreply, State#state{cmds=Cmds1}}; 293 | false -> 294 | lager:warning("Received unexpected head!"), 295 | {noreply, State} 296 | end; 297 | 298 | handle_info({nkdocker, Ref, {chunk, Data}}, #state{cmds=Cmds}=State) -> 299 | % lager:debug("Chunk: ~p", [Data]), 300 | case lists:keyfind(Ref, #cmd.from_ref, Cmds) of 301 | #cmd{mode={redirect, _}}=Cmd -> 302 | {noreply, parse_chunk(Data, Cmd, State)}; 303 | #cmd{ct=stream}=Cmd -> 304 | case parse_stream(Data, Cmd) of 305 | {ok, Stream, Cmd1} -> 306 | Cmds1 = lists:keystore(Ref, #cmd.from_ref, Cmds, Cmd1), 307 | {noreply, parse_chunk(Stream, Cmd1, State#state{cmds=Cmds1})}; 308 | {more, Cmd1} -> 309 | Cmds1 = lists:keystore(Ref, #cmd.from_ref, Cmds, Cmd1), 310 | {noreply, State#state{cmds=Cmds1}} 311 | end; 312 | #cmd{}=Cmd -> 313 | {noreply, parse_chunk(Data, Cmd, State)}; 314 | false -> 315 | lager:warning("Received unexpected chunk!"), 316 | {noreply, State} 317 | end; 318 | 319 | handle_info({nkdocker, Ref, {body, Body}}, #state{cmds=Cmds}=State) -> 320 | lager:debug("Body: ~p", [Body]), 321 | case lists:keyfind(Ref, #cmd.from_ref, Cmds) of 322 | #cmd{status=Status}=Cmd when Status>=200, Status<300 -> 323 | {noreply, parse_body(Body, Cmd, State)}; 324 | #cmd{status=Status}=Cmd -> 325 | {noreply, send_stop(Cmd, {error, {get_error(Status), Body}}, State)}; 326 | false -> 327 | lager:warning("Received unexpected body!"), 328 | {noreply, State} 329 | end; 330 | 331 | handle_info({'DOWN', MRef, process, _MPid, _Reason}, #state{cmds=Cmds}=State) -> 332 | State1 = case lists:keyfind(MRef, #cmd.conn_ref, Cmds) of 333 | #cmd{mode=async}=Cmd -> 334 | send_stop(Cmd, {error, connection_failed}, State); 335 | #cmd{mode={redirect, _}}=Cmd -> 336 | send_stop(Cmd, {error, connection_failed}, State); 337 | #cmd{ct=stream}=Cmd -> 338 | parse_body(<<>>, Cmd, State); 339 | #cmd{}=Cmd -> 340 | send_stop(Cmd, {error, connection_failed}, State); 341 | false -> 342 | case lists:keyfind(MRef, #cmd.user_mon, Cmds) of 343 | #cmd{}=Cmd -> 344 | send_stop(Cmd, skip, State); 345 | false -> 346 | State 347 | end 348 | end, 349 | {noreply, State1}; 350 | 351 | handle_info(Info, State) -> 352 | lager:warning("Module ~p received unexpected info: ~p", [?MODULE, Info]), 353 | {noreply, State}. 354 | 355 | 356 | %% @private 357 | -spec code_change(term(), #state{}, term()) -> 358 | {ok, #state{}}. 359 | 360 | code_change(_OldVsn, State, _Extra) -> 361 | {ok, State}. 362 | 363 | 364 | %% @private 365 | -spec terminate(term(), #state{}) -> 366 | ok. 367 | 368 | terminate(_Reason, _State) -> 369 | ok. 370 | 371 | 372 | 373 | %% =================================================================== 374 | %% Private 375 | %% =================================================================== 376 | 377 | %% @private 378 | -spec get_version(#state{}) -> 379 | {ok, binary()} | {stop, term()}. 380 | 381 | get_version(#state{conn=Conn, conn_opts=ConnOpts}=State) -> 382 | Ref = make_ref(), 383 | Msg = {http, Ref, <<"GET">>, <<"/version">>, headers1(State), <<>>}, 384 | case nkpacket:send(Conn, Msg, ConnOpts) of 385 | {ok, _} -> 386 | case 387 | receive 388 | {nkdocker, Ref, {head, 200, _}} -> 389 | receive 390 | {nkdocker, Ref, {body, Data}} -> 391 | nklib_json:decode(Data) 392 | after 393 | 5000 -> timeout 394 | end 395 | after 396 | 5000 -> timeout 397 | end 398 | of 399 | #{<<"ApiVersion">>:=ApiVersion} -> 400 | {ok, ApiVersion}; 401 | timeout -> 402 | {error, connect_timeout}; 403 | Other -> 404 | {error, {invalid_return_from_get_version, Other}} 405 | end; 406 | {error, Error} -> 407 | {error, Error} 408 | end. 409 | 410 | 411 | %% @private 412 | -spec send(binary(), binary(), binary(), cmd_opts(), {pid(), reference()}, 413 | #state{}) -> 414 | {ok, #state{}} | {error, term()}. 415 | 416 | send(Method, Path, Body, Opts, From, State) -> 417 | #state{conn=Conn, conn_opts=ConnOpts, cmds=Cmds} = State, 418 | Mode = case Opts of 419 | #{async:=true} -> async; 420 | #{force_new:=true} -> exclusive; 421 | #{redirect:=Port} -> {redirect, Port}; 422 | _ -> shared 423 | end, 424 | {ConnOpts1, Hds1} = case Mode of 425 | shared -> 426 | { 427 | ConnOpts, 428 | headers1(State) 429 | }; 430 | _ -> 431 | { 432 | ConnOpts#{ 433 | class => {nkdocker, self(), exclusive}, 434 | idle_timeout => maps:get(timeout, Opts, ?TIMEOUT), 435 | force_new => true 436 | }, 437 | headers2(State) 438 | } 439 | end, 440 | % ConnOpts2 = case Opts of 441 | % #{refresh:=true} -> 442 | % ConnOpts1#{refresh_fun=>fun ?MODULE:refresh_fun/1}; 443 | % _ -> 444 | % ConnOpts1 445 | % end, 446 | ConnOpts2 = ConnOpts1, 447 | Hds2 = case Opts of 448 | #{headers:=Headers} -> 449 | Hds1 ++ Headers; 450 | _ -> 451 | Hds1 452 | end, 453 | {FromPid, FromRef} = From, 454 | Msg = {http, FromRef, Method, Path, Hds2, Body}, 455 | lager:debug("NkDOCKER SEND: ~p ~p", [Msg, ConnOpts2]), 456 | case nkpacket:send(Conn, Msg, ConnOpts2) of 457 | {ok, ConnPid} -> 458 | Cmd1 = #cmd{ 459 | from_pid = FromPid, 460 | from_ref = FromRef, 461 | conn_pid = ConnPid, 462 | conn_ref = erlang:monitor(process, ConnPid), 463 | mode = Mode, 464 | use_chunks = maps:get(chunks, Opts, false) 465 | }, 466 | Cmd2 = case Mode of 467 | async -> 468 | UserMon = erlang:monitor(process, element(1, From)), 469 | Cmd1#cmd{user_mon=UserMon}; 470 | _ -> 471 | Cmd1 472 | end, 473 | {ok, State#state{cmds=[Cmd2|Cmds]}}; 474 | {error, Error} -> 475 | {error, Error} 476 | end. 477 | 478 | 479 | %% @private 480 | -spec parse_chunk(binary(), #cmd{}, #state{}) -> 481 | #state{}. 482 | 483 | parse_chunk(Data, #cmd{mode={redirect, Port}}=Cmd, State) -> 484 | case file:write(Port, Data) of 485 | ok -> 486 | State; 487 | {error, Error} -> 488 | send_stop(Cmd, {error, {file_error, Error}}, State) 489 | end; 490 | 491 | parse_chunk(Data, #cmd{mode=async, use_chunks=true}=Cmd, State) -> 492 | #cmd{from_ref=Ref, from_pid=Pid} = Cmd, 493 | Pid ! {nkdocker, Ref, {data, decode(Data, Cmd)}}, 494 | State; 495 | 496 | parse_chunk(Data, Cmd, #state{cmds=Cmds}=State) -> 497 | #cmd{chunks=Chunks, use_chunks=UseChunks, from_ref=Ref} = Cmd, 498 | Chunk = case UseChunks of 499 | true -> decode(Data, Cmd); 500 | false -> Data 501 | end, 502 | Cmd1 = Cmd#cmd{chunks=[Chunk|Chunks]}, 503 | Cmds1 = lists:keystore(Ref, #cmd.from_ref, Cmds, Cmd1), 504 | State#state{cmds=Cmds1}. 505 | 506 | 507 | %% @private 508 | -spec parse_body(binary(), #cmd{}, #state{}) -> 509 | #state{}. 510 | 511 | parse_body(Body, #cmd{mode={redirect, Port}}=Cmd, State) -> 512 | case file:write(Port, Body) of 513 | ok -> 514 | send_stop(Cmd, ok, State); 515 | {error, Error} -> 516 | send_stop(Cmd, {error, {file_error, Error}}, State) 517 | end; 518 | 519 | parse_body(Body, #cmd{chunks=Chunks, use_chunks=UseChunks}=Cmd, State) -> 520 | Reply = case Chunks of 521 | [] when Body == <<>> -> 522 | {ok, <<>>}; 523 | [] -> 524 | {ok, decode(Body, Cmd)}; 525 | _ when Body == <<>>, UseChunks == false -> 526 | BigBody = list_to_binary(lists:reverse(Chunks)), 527 | {ok, decode(BigBody, Cmd)}; 528 | _ when Body == <<>>, UseChunks == true -> 529 | {ok, lists:reverse(Chunks)}; 530 | _ -> 531 | {ok, {invalid_chunked, Chunks, Body}} 532 | end, 533 | send_stop(Cmd, Reply, State). 534 | 535 | 536 | %% @private 537 | -spec parse_stream(binary(), #cmd{}) -> 538 | {ok, binary(), #cmd{}} | {more, #cmd{}}. 539 | 540 | parse_stream(Data, #cmd{stream_buff=Buff}=Cmd) -> 541 | Data1 = << Buff/binary, Data/binary>>, 542 | case Data1 of 543 | _ when byte_size(Data1) < 8 -> 544 | {more, Cmd#cmd{stream_buff=Data1}}; 545 | <> when T==0; T==1; T==2 -> 546 | D = case T of 0 -> <<"0:">>; 1 -> <<"1:">>; 2 -> <<"2:">> end, 547 | case byte_size(Msg) of 548 | Size -> 549 | {ok, <>, Cmd#cmd{stream_buff= <<>>}}; 550 | BinSize when BinSize < Size -> 551 | {more, Cmd#cmd{stream_buff=Data1}}; 552 | _ -> 553 | {Msg1, Rest} = erlang:split_binary(Msg, Size), 554 | {ok, <>, Cmd#cmd{stream_buff=Rest}} 555 | end; 556 | _ -> 557 | {ok, Data1, Cmd#cmd{stream_buff= <<>>}} 558 | end. 559 | 560 | 561 | %% @private 562 | -spec send_stop(#cmd{}, term(), #state{}) -> 563 | #state{}. 564 | 565 | send_stop(Cmd, Reply, #state{cmds=Cmds}=State) -> 566 | #cmd{ 567 | from_pid = Pid, 568 | from_ref = Ref, 569 | conn_pid = ConnPid, 570 | conn_ref = ConnRef, 571 | mode = Mode, 572 | user_mon = UserMon 573 | } = Cmd, 574 | case Mode of 575 | async -> 576 | erlang:demonitor(ConnRef), 577 | erlang:demonitor(UserMon), 578 | nkpacket_connection:stop(ConnPid, normal); 579 | shared -> 580 | ok; 581 | exclusive -> 582 | erlang:demonitor(ConnRef), 583 | nkpacket_connection:stop(ConnPid, normal); 584 | {redirect, Port} -> 585 | file:close(Port), 586 | erlang:demonitor(ConnRef), 587 | nkpacket_connection:stop(ConnPid, normal) 588 | end, 589 | case Reply of 590 | skip -> 591 | ok; 592 | _ when Mode==async -> 593 | Pid ! {nkdocker, Ref, Reply}; 594 | _ -> 595 | gen_server:reply({Pid, Ref}, Reply) 596 | end, 597 | Cmds1 = lists:keydelete(Ref, #cmd.from_ref, Cmds), 598 | State#state{cmds=Cmds1}. 599 | 600 | 601 | %% @private 602 | get_error(Status) -> 603 | case Status of 604 | 304 -> not_modified; 605 | 400 -> bad_parameter; 606 | 401 -> unauthorized; 607 | 404 -> not_found; 608 | 406 -> not_running; 609 | 409 -> conflict; 610 | 500 -> server_error; 611 | _ -> Status 612 | end. 613 | 614 | 615 | decode(Data, #cmd{ct=json}) -> 616 | case nklib_json:decode(Data) of 617 | {error, _} -> {invalid_json, Data}; 618 | Msg -> Msg 619 | end; 620 | 621 | decode(Data, _) -> 622 | Data. 623 | 624 | 625 | %% @private 626 | headers1(State) -> 627 | [ 628 | {<<"Connection">>, <<"keep-alive">>} | 629 | headers2(State) 630 | ]. 631 | 632 | 633 | %% @private 634 | headers2(#state{host=Host}) -> 635 | [ 636 | {<<"Host">>, Host}, 637 | {<<"User-Agent">>, <<"nkdocker/develop">>}, 638 | {<<"Accept">>, <<"*/*">>} 639 | ]. 640 | 641 | 642 | -------------------------------------------------------------------------------- /src/nkdocker_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @private NkDOCKER main supervisor 22 | -module(nkdocker_sup). 23 | -author('Carlos Gonzalez '). 24 | -behaviour(supervisor). 25 | 26 | -export([start_monitor/3, init/1, start_link/0]). 27 | 28 | start_monitor(Id, CallBack, Opts) -> 29 | Spec = { 30 | {monitor, Id}, 31 | {nkdocker_monitor, start_link, [Id, CallBack, Opts]}, 32 | transient, 33 | 5000, 34 | worker, 35 | [nkdocker_monitor] 36 | }, 37 | case supervisor:start_child(?MODULE, Spec) of 38 | {ok, Pid} -> 39 | {ok, Pid}; 40 | {error, already_present} -> 41 | ok = supervisor:delete_child(?MODULE, {monitor, Id}), 42 | start_monitor(Id, CallBack, Opts); 43 | {error, {already_started, Pid}} -> 44 | {ok, Pid}; 45 | {error, Error} -> 46 | {error, Error} 47 | end. 48 | 49 | 50 | %% @private 51 | start_link() -> 52 | Childs = [], 53 | supervisor:start_link({local, ?MODULE}, ?MODULE, {{one_for_one, 10, 60}, Childs}). 54 | 55 | 56 | %% @private 57 | init(ChildSpecs) -> 58 | {ok, ChildSpecs}. 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/nkdocker_tar.erl: -------------------------------------------------------------------------------- 1 | %% Module based on an old erl_tar.erl, but returning binaries and very simplified 2 | %% 3 | %% %CopyrightBegin% 4 | %% 5 | %% Copyright Ericsson AB 1997-2013. All Rights Reserved. 6 | %% 7 | %% The contents of this file are subject to the Erlang Public License, 8 | %% Version 1.1, (the "License"); you may not use this file except in 9 | %% compliance with the License. You should have received a copy of the 10 | %% Erlang Public License along with this software. If not, it can be 11 | %% retrieved online at http://www.erlang.org/. 12 | %% 13 | %% Software distributed under the License is distributed on an "AS IS" 14 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 15 | %% the License for the specific language governing rights and limitations 16 | %% under the License. 17 | %% 18 | %% %CopyrightEnd% 19 | %% 20 | 21 | 22 | -module(nkdocker_tar). 23 | -export([add/2]). 24 | 25 | add(Name, Bin) -> 26 | Mtime0 = calendar:now_to_local_time(now()), 27 | Mtime = posix_time(erlang:localtime_to_universaltime(Mtime0)), 28 | {Prefix,Suffix} = split_filename(Name), 29 | H0 = [ 30 | to_string(Suffix, 100), 31 | to_octal(8#100644, 8), 32 | to_octal(0, 8), 33 | to_octal(0, 8), 34 | to_octal(byte_size(Bin), 12), 35 | to_octal(Mtime, 12), 36 | <<" ">>, 37 | $0, 38 | to_string([], 100), 39 | "ustar", 0, 40 | "00", 41 | zeroes(80), 42 | to_string(Prefix, 167) 43 | ], 44 | H = list_to_binary(H0), 45 | 512 = byte_size(H), 46 | ChksumString = to_octal(checksum(H), 6, [0,$\s]), 47 | <> = H, 48 | [Before, ChksumString, After, Bin, padding(byte_size(Bin), 512)]. 49 | 50 | 51 | to_octal(Int, Count) when Count > 1 -> 52 | to_octal(Int, Count-1, [0]). 53 | 54 | to_octal(_, 0, Result) -> 55 | Result; 56 | to_octal(Int, Count, Result) -> 57 | to_octal(Int div 8, Count-1, [Int rem 8 + $0|Result]). 58 | 59 | 60 | to_string(Str0, Count) -> 61 | Str = case file:native_name_encoding() of 62 | utf8 -> 63 | unicode:characters_to_binary(Str0); 64 | latin1 -> 65 | list_to_binary(Str0) 66 | end, 67 | case byte_size(Str) of 68 | Size when Size < Count -> 69 | [Str|zeroes(Count-Size)]; 70 | _ -> 71 | Str 72 | end. 73 | 74 | 75 | split_filename(Name) when length(Name) =< 100 -> 76 | {"", Name}; 77 | split_filename(Name0) -> 78 | split_filename(lists:reverse(filename:split(Name0)), [], [], 0). 79 | 80 | split_filename([Comp|Rest], Prefix, Suffix, Len) when Len+length(Comp) < 100 -> 81 | split_filename(Rest, Prefix, [Comp|Suffix], Len+length(Comp)+1); 82 | split_filename([Comp|Rest], Prefix, Suffix, Len) -> 83 | split_filename(Rest, [Comp|Prefix], Suffix, Len+length(Comp)+1); 84 | split_filename([], Prefix, Suffix, _) -> 85 | {filename:join(Prefix),filename:join(Suffix)}. 86 | 87 | 88 | checksum(Bin) -> 89 | checksum(Bin, 0). 90 | 91 | checksum(<>, Sum) -> 92 | checksum(T, Sum+A+B+C+D+E+F+G+H); 93 | checksum(<>, Sum) -> 94 | checksum(T, Sum+A); 95 | 96 | checksum(<<>>, Sum) -> 97 | Sum. 98 | 99 | 100 | padding(Size, BlockSize) -> 101 | zeroes(pad_size(Size, BlockSize)). 102 | 103 | pad_size(Size, BlockSize) -> 104 | case Size rem BlockSize of 105 | 0 -> 0; 106 | Rem -> BlockSize-Rem 107 | end. 108 | 109 | 110 | zeroes(0) -> 111 | []; 112 | zeroes(1) -> 113 | [0]; 114 | zeroes(2) -> 115 | [0,0]; 116 | zeroes(Number) -> 117 | Half = zeroes(Number div 2), 118 | case Number rem 2 of 119 | 0 -> [Half|Half]; 120 | 1 -> [Half|[0|Half]] 121 | end. 122 | 123 | 124 | posix_time(Time) -> 125 | EpochStart = {{1970,1,1},{0,0,0}}, 126 | {Days,{Hour,Min,Sec}} = calendar:time_difference(EpochStart, Time), 127 | 86400*Days + 3600*Hour + 60*Min + Sec. 128 | -------------------------------------------------------------------------------- /src/nkdocker_util.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2016 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %% @doc Utility module. 22 | -module(nkdocker_util). 23 | -author('Carlos Gonzalez '). 24 | 25 | -export([get_conn_info/0, get_conn_info/1]). 26 | -export([remove_exited/0, build/2]). 27 | -export([make_tar/1, docker_exec/1, docker_exec/2]). 28 | 29 | %% =================================================================== 30 | %% Public 31 | %% =================================================================== 32 | 33 | 34 | 35 | %% @private 36 | -spec get_conn_info() -> 37 | {ok, nkdocker:conn_opts()|#{ip=>inet:ip_address()}} | {error, invalid_host}. 38 | 39 | get_conn_info() -> 40 | get_conn_info(#{}). 41 | 42 | 43 | %% @private 44 | -spec get_conn_info(nkdocker:conn_opts()) -> 45 | {ok, nkdocker:conn_opts()|#{ip=>inet:ip_address()}} | {error, invalid_host}. 46 | 47 | get_conn_info(Opts) -> 48 | {ok, EnvConfig} = application:get_env(nkdocker, conn_config), 49 | #{host:=Host} = Opts1 = maps:merge(EnvConfig, Opts), 50 | case nkpacket_dns:ips(Host) of 51 | [Ip|_] -> 52 | {ok, Opts1#{ip=>Ip}}; 53 | _ -> 54 | {error, invalid_host} 55 | end. 56 | 57 | 58 | 59 | %% @doc Removes all exited containers 60 | -spec remove_exited() -> 61 | ok | {error, term()}. 62 | 63 | remove_exited() -> 64 | Op = fun(Pid) -> 65 | case nkdocker:ps(Pid, #{filters=>#{status=>[exited]}}) of 66 | {ok, List} -> 67 | Ids = [Id || #{<<"Id">>:=Id} <- List], 68 | remove_exited(Pid, Ids); 69 | {error, Error} -> 70 | {error, Error} 71 | end 72 | end, 73 | docker_exec(Op). 74 | 75 | 76 | %% @doc 77 | -spec build(string()|binary(), binary()) -> 78 | ok | {error, term()}. 79 | 80 | build(Tag, TarBin) -> 81 | Op = fun(Pid) -> 82 | case nkdocker:inspect_image(Pid, Tag) of 83 | {ok, _} -> 84 | ok; 85 | {error, {not_found, _}} -> 86 | lager:notice("Building docker image ~s", [Tag]), 87 | Timeout = 3600 * 1000, 88 | case nkdocker:build(Pid, TarBin, #{t=>Tag, async=>true, timeout=>Timeout}) of 89 | {async, Ref} -> 90 | case wait_async(Pid, Ref, Timeout) of 91 | ok -> 92 | case nkdocker:inspect_image(Pid, Tag) of 93 | {ok, _} -> ok; 94 | _ -> {error, image_not_built} 95 | end; 96 | {error, Error} -> 97 | {error, Error} 98 | end; 99 | {error, Error} -> 100 | {error, {build_error, Error}} 101 | end; 102 | {error, Error} -> 103 | {error, {inspect_error, Error}} 104 | end 105 | end, 106 | docker_exec(Op). 107 | 108 | 109 | make_tar(List) -> 110 | list_to_binary([nkdocker_tar:add(Path, Bin) || {Path, Bin} <- List]). 111 | 112 | 113 | %% @private 114 | docker_exec(Fun) -> 115 | docker_exec(Fun, #{}). 116 | 117 | 118 | %% @private 119 | docker_exec(Fun, Opts) -> 120 | case nkdocker:start(Opts) of 121 | {ok, Pid} -> 122 | Res = (catch Fun(Pid)), 123 | nkdocker:stop(Pid), 124 | Res; 125 | {error, Error} -> 126 | {error, Error} 127 | end. 128 | 129 | 130 | 131 | 132 | %% =================================================================== 133 | %% Private 134 | %% =================================================================== 135 | 136 | 137 | %% @private 138 | remove_exited(_Pid, []) -> 139 | ok; 140 | 141 | remove_exited(Pid, [Id|Rest]) -> 142 | case nkdocker:rm(Pid, Id) of 143 | ok -> 144 | lager:info("Removed ~s", [Id]); 145 | {error, Error} -> 146 | lager:notice("NOT Removed ~s: ~p", [Id, Error]) 147 | end, 148 | remove_exited(Pid, Rest). 149 | 150 | 151 | %% @private 152 | wait_async(Pid, Ref, Timeout) -> 153 | Mon = monitor(process, Pid), 154 | Result = wait_async_iter(Ref, Mon, Timeout), 155 | demonitor(Mon), 156 | Result. 157 | 158 | 159 | wait_async_iter(Ref, Mon, Timeout) -> 160 | receive 161 | {nkdocker, Ref, {data, #{<<"stream">> := Text}}} -> 162 | io:format("~s", [Text]), 163 | wait_async_iter(Ref, Mon, Timeout); 164 | {nkdocker, Ref, {data, #{<<"status">> := Text}}} -> 165 | io:format("~s", [Text]), 166 | wait_async_iter(Ref, Mon, Timeout); 167 | {nkdocker, Ref, {data, Data}} -> 168 | io:format("~p\n", [Data]), 169 | wait_async_iter(Ref, Mon, Timeout); 170 | {nkdocker, Ref, {ok, _}} -> 171 | ok; 172 | {nkdocker, Ref, {error, Reason}} -> 173 | {error, Reason}; 174 | {nkdocker, Ref, Other} -> 175 | lager:warning("Unexpected msg: ~p", [Other]), 176 | wait_async_iter(Ref, Mon, Timeout); 177 | {'DOWN', Mon, process, _Pid, _Reason} -> 178 | {error, process_failed} 179 | after 180 | Timeout -> 181 | {error, timeout} 182 | end. 183 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox -------------------------------------------------------------------------------- /test/app.config: -------------------------------------------------------------------------------- 1 | [ 2 | {nkdocker, [ 3 | ]}, 4 | 5 | {lager, [ 6 | {handlers, [{lager_console_backend, warning}]}, 7 | {error_logger_redirect, false}, 8 | {colored, true} 9 | ]}, 10 | 11 | {sasl, [ 12 | {sasl_error_logger, false} 13 | ]} 14 | 15 | ]. 16 | -------------------------------------------------------------------------------- /test/basic_test.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(basic_test). 22 | -author('Carlos Gonzalez '). 23 | 24 | -compile([export_all]). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | -include_lib("nkpacket/include/nkpacket.hrl"). 27 | -include("nkdocker.hrl"). 28 | 29 | -define(RECV(M), receive M -> ok after 1000 -> error(?LINE) end). 30 | 31 | basic_test_() -> 32 | {setup, spawn, 33 | fun() -> 34 | nkdocker_app:start(), 35 | Opts = #{ % Read from environment vars or uncomment 36 | % host = "127.0.0.1", 37 | % port = 0, 38 | % proto = tls, 39 | % keyfile = "" 40 | % certfile ="" 41 | }, 42 | {ok, Pid} = nkdocker:start_link(Opts), 43 | ?debugMsg("Starting BASIC test"), 44 | Pid 45 | end, 46 | fun(Pid) -> 47 | nkdocker:stop(Pid) 48 | end, 49 | fun(Pid) -> 50 | [ 51 | fun() -> conns(Pid) end, 52 | {timeout, 60, fun() -> images(Pid) end}, 53 | {timeout, 60, fun() -> run(Pid) end} 54 | ] 55 | end 56 | }. 57 | 58 | 59 | conns(Pid) -> 60 | nkpacket_connection:stop_all({nkdocker, Pid}), 61 | nkpacket_connection:stop_all({nkdocker, Pid, exclusive}), 62 | timer:sleep(100), 63 | [] = nkpacket_connection:get_all({nkdocker, Pid}), 64 | [] = nkpacket_connection:get_all({nkdocker, Pid, exclusive}), 65 | 66 | {async, Ref} = nkdocker:events(Pid), 67 | timer:sleep(50), 68 | 69 | % We have only the events exclusive connection 70 | [] = nkpacket_connection:get_all({nkdocker, Pid}), 71 | [ConnPid1] = nkpacket_connection:get_all({nkdocker, Pid, exclusive}), 72 | ConnRef = erlang:monitor(process, ConnPid1), 73 | 74 | {ok, #{<<"ApiVersion">>:=_}} = nkdocker:version(Pid), 75 | {ok, #{<<"Containers">>:=_}} = nkdocker:info(Pid), 76 | ok = nkdocker:ping(Pid), 77 | % Now we have the previous and a 'shared' connection 78 | [ConnPid2] = nkpacket_connection:get_all({nkdocker, Pid}), 79 | [ConnPid1] = nkpacket_connection:get_all({nkdocker, Pid, exclusive}), 80 | ok = nkdocker:finish_async(Pid, Ref), 81 | ?RECV({nkdocker, Ref, {ok, user_stop}}), 82 | ?RECV({'DOWN', ConnRef, process, ConnPid1, normal}), 83 | [ConnPid2] = nkpacket_connection:get_all({nkdocker, Pid}), 84 | [] = nkpacket_connection:get_all({nkdocker, Pid, exclusive}), 85 | true = ConnPid1 /= ConnPid2, 86 | ok. 87 | 88 | 89 | images(Pid) -> 90 | ?debugMsg("Building image from image1.tar (imports busybox:latest)"), 91 | Dir = filename:join(filename:dirname(code:priv_dir(nkdocker)), "test"), 92 | {ok, ImageTar1} = file:read_file(filename:join(Dir, "image1.tar")), 93 | {ok, List1} = nkdocker:build(Pid, ImageTar1, #{t=>"nkdocker:test1", force_rm=>true}), 94 | [#{<<"stream">>:=<<"Successfully built ", Id1:12/binary, "\n">>}|_] = 95 | lists:reverse(List1), 96 | {ok, #{<<"Id">>:=FullId1}=Img1} = nkdocker:inspect_image(Pid, Id1), 97 | case FullId1 of 98 | <<"sha256:", Id1:12/binary, _/binary>> -> ok; 99 | <> -> ok 100 | end, 101 | % lager:warning("Id: ~p, FullId1: ~p", [Id1, FullId1]), 102 | 103 | {ok, Img1} = nkdocker:inspect_image(Pid, "nkdocker:test1"), 104 | {ok, [#{<<"Id">>:=FullId1}|_]} = nkdocker:history(Pid, Id1), 105 | case nkdocker:tag(Pid, Id1, #{repo=>"nkdocker", tag=>"test2", force=>true}) of 106 | ok -> ok; 107 | {error, {conflict, _}} -> ok 108 | end, 109 | {ok, #{<<"Id">>:=FullId1}} = nkdocker:inspect_image(Pid, "nkdocker:test2"), 110 | {ok, _} = nkdocker:rmi(Pid, <<"nkdocker:test1">>), 111 | {error, {not_found, _}} = nkdocker:inspect_image(Pid, "nkdocker:test1"), 112 | 113 | ?debugMsg("Building image from busybox:latest"), 114 | {ok, _} = nkdocker:create_image(Pid, #{fromImage=>"busybox:latest"}), 115 | {ok, #{<<"Id">>:=FullId1}} = nkdocker:inspect_image(Pid, "busybox:latest"), 116 | ok. 117 | 118 | 119 | run(Pid) -> 120 | ?debugMsg("Starting run test"), 121 | nkdocker:kill(Pid, "nkdocker1"), 122 | nkdocker:rm(Pid, "nkdocker1"), 123 | 124 | {async, Ref} = nkdocker:events(Pid), 125 | 126 | ?debugMsg("Start container from busybox:latest"), 127 | {ok, #{<<"Id">>:=Id1}} = nkdocker:create(Pid, "busybox:latest", 128 | #{ 129 | name => "nkdocker1", 130 | interactive => true, 131 | tty => true, 132 | cmd => ["/bin/sh"] 133 | }), 134 | ?debugMsg("... created"), 135 | receive_status(Ref, Id1, <<"create">>), 136 | 137 | {ok, #{<<"Id">>:=Id1}=Data1} = nkdocker:inspect(Pid, Id1), 138 | {ok, Data1} = nkdocker:inspect(Pid, "nkdocker1"), 139 | 140 | ok = nkdocker:start(Pid, Id1), 141 | receive_status(Ref, Id1, <<"start">>), 142 | 143 | ok = nkdocker:pause(Pid, "nkdocker1"), 144 | receive_status(Ref, Id1, <<"pause">>), 145 | 146 | ok = nkdocker:unpause(Pid, "nkdocker1"), 147 | receive_status(Ref, Id1, <<"unpause">>), 148 | 149 | Self = self(), 150 | spawn( 151 | fun() -> 152 | {ok, R} = nkdocker:wait(Pid, "nkdocker1", 5000), 153 | Self ! {wait, Ref, R} 154 | end), 155 | 156 | ok = nkdocker:kill(Pid, "nkdocker1"), 157 | receive_status(Ref, Id1, <<"die">>), 158 | receive_status(Ref, Id1, <<"kill">>), 159 | ?RECV({wait, Ref, #{<<"StatusCode">> := 137}}), 160 | 161 | ok = nkdocker:start(Pid, Id1), 162 | receive_status(Ref, Id1, <<"start">>), 163 | 164 | {async, Ref2} = nkdocker:attach(Pid, Id1), 165 | ok = nkdocker:attach_send(Pid, Ref2, "cat /etc/hostname\r\n"), 166 | Msg1 = receive_msg(Ref2, <<>>), 167 | <> = Id1, 168 | <<"cat /etc/hostname\r\n", ShortId1:12/binary, _/binary>> = Msg1, 169 | 170 | {ok, TList} = nkdocker:logs(Pid, ShortId1, #{stdout=>true}), 171 | {match, _} = re:run(TList, "cat /etc/hostname"), 172 | {match, _} = re:run(TList, ShortId1), 173 | 174 | ok = nkdocker:rename(Pid, ShortId1, "nkdocker2"), 175 | ok = nkdocker:restart(Pid, "nkdocker2"), 176 | ?RECV({nkdocker, Ref2, {error, connection_failed}}), 177 | 178 | receive_status(Ref, Id1, <<"die">>), 179 | receive_status(Ref, Id1, <<"start">>), 180 | receive_status(Ref, Id1, <<"restart">>), 181 | 182 | ok = nkdocker:kill(Pid, "nkdocker2"), 183 | receive_status(Ref, Id1, <<"kill">>), 184 | receive_status(Ref, Id1, <<"die">>), 185 | ok = nkdocker:rm(Pid, "nkdocker2"), 186 | receive_status(Ref, Id1, <<"destroy">>), 187 | 188 | nkdocker:finish_async(Pid, Ref), 189 | ?RECV({nkdocker, Ref, {ok, user_stop}}), 190 | ok. 191 | 192 | 193 | 194 | %% Internal 195 | 196 | receive_status(Ref, Id, Status) -> 197 | ?RECV({nkdocker, Ref, {data, #{<<"id">>:=Id, <<"status">>:=Status}}}). 198 | 199 | receive_msg(Ref, Buf) -> 200 | receive 201 | {nkdocker, Ref, {data, Data}} -> 202 | receive_msg(Ref, <>) 203 | after 204 | 500 -> Buf 205 | end. 206 | 207 | -------------------------------------------------------------------------------- /test/image1.tar: -------------------------------------------------------------------------------- 1 | Dockerfile000644 000766 000024 00000000014 12506541041 013642 0ustar00carlosjstaff000000 000000 FROM busybox -------------------------------------------------------------------------------- /test/proxy.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2015 Carlos Gonzalez Florido. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(proxy). 22 | -author('Carlos Gonzalez '). 23 | 24 | -export([start/1]). 25 | -export([transports/0, conn_init/1, conn_bridge/3]). 26 | 27 | -include("nkdocker.hrl"). 28 | 29 | start(Opts) -> 30 | EnvConfig = application:get_env(nkdocker, conn_config, #{}), 31 | Opts1 = maps:merge(EnvConfig, Opts), 32 | Host = maps:get(host, Opts1, "127.0.0.1"), 33 | Port = maps:get(port, Opts1, 2375), 34 | Proto = maps:get(proto, Opts1, tcp), 35 | [Ip] = nkpacket_dns:ips(?MODULE, Host), 36 | Conn = {?MODULE, Proto, Ip, Port}, 37 | ListenPort = maps:get(listen_port, Opts1, 12375), 38 | ListenConn = {?MODULE, tcp, {0,0,0,0}, ListenPort}, 39 | ListenOpts = #{ 40 | tcp_listeners => 1, 41 | user => #{conn=>Conn, conn_opts => maps:with([certfile, keyfile], Opts1)} 42 | }, 43 | nkpacket:start_listener(?MODULE, ListenConn, ListenOpts). 44 | 45 | 46 | 47 | %%% Callbacks 48 | 49 | -record(state, { 50 | type, 51 | conn_port 52 | }). 53 | 54 | 55 | transports() -> [tcp]. 56 | 57 | 58 | conn_init(Port) -> 59 | case nkpacket:get_user(Port) of 60 | {ok, _SrvId, #{conn:=Conn, conn_opts:=ConnOpts}} -> 61 | ConnOpts1 = ConnOpts#{force_new=>true, user=>secondary}, 62 | lager:debug("PROXY Starting Connection to ~p, ~p", [Conn, ConnOpts]), 63 | case nkpacket:connect(?MODULE, Conn, ConnOpts1) of 64 | {ok, ConnPort} -> 65 | {bridge, ConnPort, #state{type=up, conn_port=ConnPort}}; 66 | _ -> 67 | {stop, could_not_connect} 68 | end; 69 | {ok, secondary} -> 70 | {ok, #state{type=down, conn_port=Port}} 71 | end. 72 | 73 | 74 | conn_bridge(Data, Type, State) -> 75 | lager:warning("Data (~p)", [Type]), 76 | print(Data), 77 | {ok, Data, State}. 78 | 79 | 80 | print(Data) -> 81 | case binary:split(Data, <<"\r\n\r\n">>) of 82 | [Headers, <<${, _/binary>>=Json] -> 83 | case catch jiffy:decode(Json, []) of 84 | {'EXIT', _} -> 85 | lager:notice("~s", [Data]); 86 | {Map} -> 87 | lager:notice("~s\r\n\r\n~s", [Headers, jiffy:encode({lists:sort(Map)}, [pretty])]) 88 | % lager:notice("~s\r\n\r\n~s", [Headers, jiffy:encode(Map, [pretty])]) 89 | end; 90 | _ -> 91 | lager:notice("~s", [Data]) 92 | end. 93 | 94 | -------------------------------------------------------------------------------- /test/vm.args: -------------------------------------------------------------------------------- 1 | -pa deps/lager/ebin 2 | -pa deps/goldrush/ebin 3 | -pa deps/jiffy/ebin 4 | -pa deps/cowboy/ebin 5 | -pa deps/cowlib/ebin 6 | -pa deps/ranch/ebin 7 | -pa deps/gun/ebin 8 | -pa ../nklib/ebin 9 | -pa ebin 10 | 11 | 12 | ## Name of the node 13 | -name nkdocker_shell@127.0.0.1 14 | -setcookie nkcore 15 | 16 | ## More processes 17 | +P 1000000 18 | 19 | ## Treat error_logger warnings as warnings 20 | +W w 21 | 22 | ## Increase number of concurrent ports/sockets 23 | -env ERL_MAX_PORTS 65535 24 | 25 | ## Set the location of crash dumps 26 | -env ERL_CRASH_DUMP . 27 | 28 | 29 | -------------------------------------------------------------------------------- /util/shell_app.config: -------------------------------------------------------------------------------- 1 | [ 2 | {nkdocker, [ 3 | ]}, 4 | 5 | {sasl, [ 6 | {sasl_error_logger, false} 7 | ]}, 8 | 9 | {lager, [ 10 | {handlers, [ 11 | {lager_console_backend, info}, 12 | {lager_file_backend, [{file, "log/error.log"}, {level, error}]}, 13 | {lager_file_backend, [{file, "log/console.log"}, {level, info}]} 14 | ]}, 15 | {error_logger_redirect, false}, 16 | {crash_log, "log/crash.log"}, 17 | {colored, true}, 18 | {colors, [ 19 | {debug, "\e[0;38m" }, 20 | {info, "\e[0;32m" }, 21 | {notice, "\e[1;36m" }, 22 | {warning, "\e[1;33m" }, 23 | {error, "\e[1;31m" } 24 | ]} 25 | ]} 26 | 27 | ]. 28 | -------------------------------------------------------------------------------- /util/shell_vm.args: -------------------------------------------------------------------------------- 1 | -pa deps/lager/ebin 2 | -pa deps/goldrush/ebin 3 | -pa deps/jsx/ebin 4 | -pa deps/jiffy/ebin 5 | -pa deps/cowboy/ebin 6 | -pa deps/cowlib/ebin 7 | -pa deps/ranch/ebin 8 | -pa deps/gun/ebin 9 | -pa deps/nklib/ebin 10 | -pa deps/nkpacket/ebin 11 | -pa ../nkdocker/ebin 12 | 13 | ## Name of the node 14 | -name nkdocker_shell@127.0.0.1 15 | -setcookie nkcore 16 | 17 | ## More processes 18 | +P 1000000 19 | 20 | ## Treat error_logger warnings as warnings 21 | +W w 22 | 23 | ## Increase number of concurrent ports/sockets 24 | -env ERL_MAX_PORTS 65535 25 | 26 | ## Set the location of crash dumps 27 | -env ERL_CRASH_DUMP . 28 | 29 | 30 | --------------------------------------------------------------------------------