From 5a3b4baa653cd4cbb01eb5148974101b5c77e28f Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 30 Oct 2015 02:40:51 +0000 Subject: [PATCH 1/1] Import docker.io_1.8.3~ds1.orig-libnetwork.tar.gz [dgit import orig docker.io_1.8.3~ds1.orig-libnetwork.tar.gz] --- .gitignore | 33 + Godeps/Godeps.json | 137 ++ Godeps/Readme | 5 + LICENSE | 202 ++ MAINTAINERS | 5 + Makefile | 79 + README.md | 88 + ROADMAP.md | 20 + api/api.go | 807 +++++++ api/api_test.go | 2130 +++++++++++++++++ api/types.go | 81 + bitseq/sequence.go | 454 ++++ bitseq/sequence_test.go | 427 ++++ bitseq/store.go | 138 ++ circle.yml | 12 + client/client.go | 115 + client/client_service_test.go | 122 + client/client_test.go | 217 ++ client/network.go | 231 ++ client/service.go | 362 +++ client/types.go | 73 + cmd/dnet/dnet.go | 288 +++ cmd/dnet/dnet_test.go | 132 + cmd/dnet/flags.go | 51 + cmd/dnet/libnetwork.toml | 12 + cmd/ovrouter/ovrouter.go | 149 ++ cmd/readme_test/readme.go | 65 + cmd/test/libnetwork.toml | 12 + cmd/test/main.go | 49 + config/config.go | 116 + config/config_test.go | 55 + config/libnetwork.toml | 12 + controller.go | 399 +++ datastore/datastore.go | 194 ++ datastore/datastore_test.go | 241 ++ datastore/mock_store.go | 129 + docs/Vagrantfile | 57 + docs/bridge.md | 13 + docs/design.md | 158 ++ docs/legacy.md | 15 + docs/overlay.md | 153 ++ docs/remote.md | 203 ++ docs/vagrant.md | 185 ++ driverapi/driverapi.go | 137 ++ driverapi/errors.go | 56 + drivers/bridge/bridge.go | 1427 +++++++++++ drivers/bridge/bridge_test.go | 644 +++++ drivers/bridge/errors.go | 341 +++ drivers/bridge/interface.go | 63 + drivers/bridge/interface_test.go | 33 + drivers/bridge/link.go | 85 + drivers/bridge/link_test.go | 39 + drivers/bridge/netlink_deprecated_linux.go | 139 ++ .../netlink_deprecated_linux_armppc64.go | 7 + .../bridge/netlink_deprecated_linux_notarm.go | 7 + .../bridge/netlink_deprecated_unsupported.go | 18 + drivers/bridge/network_test.go | 203 ++ drivers/bridge/port_mapping.go | 123 + drivers/bridge/port_mapping_test.go | 71 + drivers/bridge/resolvconf.go | 67 + drivers/bridge/resolvconf_test.go | 53 + drivers/bridge/setup.go | 26 + drivers/bridge/setup_bridgenetfiltering.go | 162 ++ .../bridge/setup_bridgenetfiltering_test.go | 12 + drivers/bridge/setup_device.go | 66 + drivers/bridge/setup_device_test.go | 76 + drivers/bridge/setup_firewalld.go | 15 + drivers/bridge/setup_fixedcidrv4.go | 19 + drivers/bridge/setup_fixedcidrv4_test.go | 62 + drivers/bridge/setup_fixedcidrv6.go | 27 + drivers/bridge/setup_fixedcidrv6_test.go | 44 + drivers/bridge/setup_ip_forwarding.go | 25 + drivers/bridge/setup_ip_forwarding_test.go | 75 + drivers/bridge/setup_ip_tables.go | 208 ++ drivers/bridge/setup_ip_tables_test.go | 105 + drivers/bridge/setup_ipv4.go | 140 ++ drivers/bridge/setup_ipv4_test.go | 111 + drivers/bridge/setup_ipv6.go | 81 + drivers/bridge/setup_ipv6_test.go | 70 + drivers/bridge/setup_verify.go | 46 + drivers/bridge/setup_verify_test.go | 110 + drivers/host/host.go | 74 + drivers/host/host_test.go | 50 + drivers/null/null.go | 70 + drivers/null/null_test.go | 50 + drivers/overlay/joinleave.go | 99 + drivers/overlay/ov_endpoint.go | 110 + drivers/overlay/ov_network.go | 337 +++ drivers/overlay/ov_serf.go | 249 ++ drivers/overlay/ov_utils.go | 88 + drivers/overlay/overlay.go | 158 ++ drivers/overlay/overlay_test.go | 130 + drivers/overlay/peerdb.go | 284 +++ drivers/remote/driver.go | 227 ++ drivers/remote/driver_test.go | 436 ++++ drivers/remote/messages.go | 178 ++ drivers/windows/windows.go | 58 + drivers_freebsd.go | 19 + drivers_linux.go | 25 + drivers_windows.go | 17 + endpoint.go | 999 ++++++++ endpoint_info.go | 323 +++ error.go | 175 ++ errors_test.go | 51 + etchosts/etchosts.go | 127 + etchosts/etchosts_test.go | 249 ++ hostdiscovery/hostdiscovery.go | 154 ++ hostdiscovery/hostdiscovery_api.go | 23 + hostdiscovery/hostdiscovery_disabled.go | 28 + hostdiscovery/hostdiscovery_test.go | 142 ++ hostdiscovery/libnetwork.toml | 6 + idm/idm.go | 94 + idm/idm_test.go | 108 + ipallocator/allocator.go | 175 ++ ipallocator/allocator_test.go | 690 ++++++ ipam/allocator.go | 634 +++++ ipam/allocator_test.go | 587 +++++ ipam/contract.go | 100 + ipam/store.go | 178 ++ iptables/firewalld.go | 164 ++ iptables/firewalld_test.go | 83 + iptables/iptables.go | 328 +++ iptables/iptables_test.go | 230 ++ libnetwork_internal_test.go | 32 + libnetwork_test.go | 2115 ++++++++++++++++ netlabel/labels.go | 52 + netutils/test_utils.go | 10 + netutils/utils.go | 222 ++ netutils/utils_test.go | 211 ++ network.go | 471 ++++ options/options.go | 73 + options/options_test.go | 97 + portallocator/portallocator.go | 212 ++ portallocator/portallocator_test.go | 254 ++ portmapper/mapper.go | 219 ++ portmapper/mapper_test.go | 271 +++ portmapper/mock_proxy.go | 18 + portmapper/proxy.go | 209 ++ resolvconf/README.md | 1 + resolvconf/dns/resolvconf.go | 17 + resolvconf/resolvconf.go | 187 ++ resolvconf/resolvconf_test.go | 240 ++ sandbox/interface_freebsd.go | 4 + sandbox/interface_linux.go | 370 +++ sandbox/interface_windows.go | 4 + sandbox/namespace_linux.go | 327 +++ sandbox/namespace_unsupported.go | 8 + sandbox/namespace_windows.go | 28 + sandbox/neigh_freebsd.go | 4 + sandbox/neigh_linux.go | 138 ++ sandbox/neigh_windows.go | 4 + sandbox/options_linux.go | 61 + sandbox/route_linux.go | 197 ++ sandbox/sandbox.go | 170 ++ sandbox/sandbox_freebsd.go | 28 + sandbox/sandbox_linux_test.go | 180 ++ sandbox/sandbox_test.go | 189 ++ sandbox/sandbox_unsupported.go | 22 + sandbox/sandbox_unsupported_test.go | 20 + sandbox/test_freebsd.go | 8 + sandbox/test_linux.go | 37 + sandbox/test_windows.go | 8 + sandboxdata.go | 259 ++ sandboxdata_test.go | 141 ++ store.go | 371 +++ test/integration/README.md | 34 + test/integration/daemon-configs.bats | 104 + test/integration/daemon.cfg | 4 + test/integration/helpers.bash | 50 + types/types.go | 407 ++++ types/types_test.go | 111 + 171 files changed, 29925 insertions(+) create mode 100644 .gitignore create mode 100644 Godeps/Godeps.json create mode 100644 Godeps/Readme create mode 100644 LICENSE create mode 100644 MAINTAINERS create mode 100644 Makefile create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 api/api.go create mode 100644 api/api_test.go create mode 100644 api/types.go create mode 100644 bitseq/sequence.go create mode 100644 bitseq/sequence_test.go create mode 100644 bitseq/store.go create mode 100644 circle.yml create mode 100644 client/client.go create mode 100644 client/client_service_test.go create mode 100644 client/client_test.go create mode 100644 client/network.go create mode 100644 client/service.go create mode 100644 client/types.go create mode 100644 cmd/dnet/dnet.go create mode 100644 cmd/dnet/dnet_test.go create mode 100644 cmd/dnet/flags.go create mode 100755 cmd/dnet/libnetwork.toml create mode 100644 cmd/ovrouter/ovrouter.go create mode 100644 cmd/readme_test/readme.go create mode 100644 cmd/test/libnetwork.toml create mode 100644 cmd/test/main.go create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/libnetwork.toml create mode 100644 controller.go create mode 100644 datastore/datastore.go create mode 100644 datastore/datastore_test.go create mode 100644 datastore/mock_store.go create mode 100644 docs/Vagrantfile create mode 100644 docs/bridge.md create mode 100644 docs/design.md create mode 100644 docs/legacy.md create mode 100644 docs/overlay.md create mode 100644 docs/remote.md create mode 100644 docs/vagrant.md create mode 100644 driverapi/driverapi.go create mode 100644 driverapi/errors.go create mode 100644 drivers/bridge/bridge.go create mode 100644 drivers/bridge/bridge_test.go create mode 100644 drivers/bridge/errors.go create mode 100644 drivers/bridge/interface.go create mode 100644 drivers/bridge/interface_test.go create mode 100644 drivers/bridge/link.go create mode 100644 drivers/bridge/link_test.go create mode 100644 drivers/bridge/netlink_deprecated_linux.go create mode 100644 drivers/bridge/netlink_deprecated_linux_armppc64.go create mode 100644 drivers/bridge/netlink_deprecated_linux_notarm.go create mode 100644 drivers/bridge/netlink_deprecated_unsupported.go create mode 100644 drivers/bridge/network_test.go create mode 100644 drivers/bridge/port_mapping.go create mode 100644 drivers/bridge/port_mapping_test.go create mode 100644 drivers/bridge/resolvconf.go create mode 100644 drivers/bridge/resolvconf_test.go create mode 100644 drivers/bridge/setup.go create mode 100644 drivers/bridge/setup_bridgenetfiltering.go create mode 100644 drivers/bridge/setup_bridgenetfiltering_test.go create mode 100644 drivers/bridge/setup_device.go create mode 100644 drivers/bridge/setup_device_test.go create mode 100644 drivers/bridge/setup_firewalld.go create mode 100644 drivers/bridge/setup_fixedcidrv4.go create mode 100644 drivers/bridge/setup_fixedcidrv4_test.go create mode 100644 drivers/bridge/setup_fixedcidrv6.go create mode 100644 drivers/bridge/setup_fixedcidrv6_test.go create mode 100644 drivers/bridge/setup_ip_forwarding.go create mode 100644 drivers/bridge/setup_ip_forwarding_test.go create mode 100644 drivers/bridge/setup_ip_tables.go create mode 100644 drivers/bridge/setup_ip_tables_test.go create mode 100644 drivers/bridge/setup_ipv4.go create mode 100644 drivers/bridge/setup_ipv4_test.go create mode 100644 drivers/bridge/setup_ipv6.go create mode 100644 drivers/bridge/setup_ipv6_test.go create mode 100644 drivers/bridge/setup_verify.go create mode 100644 drivers/bridge/setup_verify_test.go create mode 100644 drivers/host/host.go create mode 100644 drivers/host/host_test.go create mode 100644 drivers/null/null.go create mode 100644 drivers/null/null_test.go create mode 100644 drivers/overlay/joinleave.go create mode 100644 drivers/overlay/ov_endpoint.go create mode 100644 drivers/overlay/ov_network.go create mode 100644 drivers/overlay/ov_serf.go create mode 100644 drivers/overlay/ov_utils.go create mode 100644 drivers/overlay/overlay.go create mode 100644 drivers/overlay/overlay_test.go create mode 100644 drivers/overlay/peerdb.go create mode 100644 drivers/remote/driver.go create mode 100644 drivers/remote/driver_test.go create mode 100644 drivers/remote/messages.go create mode 100644 drivers/windows/windows.go create mode 100644 drivers_freebsd.go create mode 100644 drivers_linux.go create mode 100644 drivers_windows.go create mode 100644 endpoint.go create mode 100644 endpoint_info.go create mode 100644 error.go create mode 100644 errors_test.go create mode 100644 etchosts/etchosts.go create mode 100644 etchosts/etchosts_test.go create mode 100644 hostdiscovery/hostdiscovery.go create mode 100644 hostdiscovery/hostdiscovery_api.go create mode 100644 hostdiscovery/hostdiscovery_disabled.go create mode 100644 hostdiscovery/hostdiscovery_test.go create mode 100644 hostdiscovery/libnetwork.toml create mode 100644 idm/idm.go create mode 100644 idm/idm_test.go create mode 100644 ipallocator/allocator.go create mode 100644 ipallocator/allocator_test.go create mode 100644 ipam/allocator.go create mode 100644 ipam/allocator_test.go create mode 100644 ipam/contract.go create mode 100644 ipam/store.go create mode 100644 iptables/firewalld.go create mode 100644 iptables/firewalld_test.go create mode 100644 iptables/iptables.go create mode 100644 iptables/iptables_test.go create mode 100644 libnetwork_internal_test.go create mode 100644 libnetwork_test.go create mode 100644 netlabel/labels.go create mode 100644 netutils/test_utils.go create mode 100644 netutils/utils.go create mode 100644 netutils/utils_test.go create mode 100644 network.go create mode 100644 options/options.go create mode 100644 options/options_test.go create mode 100644 portallocator/portallocator.go create mode 100644 portallocator/portallocator_test.go create mode 100644 portmapper/mapper.go create mode 100644 portmapper/mapper_test.go create mode 100644 portmapper/mock_proxy.go create mode 100644 portmapper/proxy.go create mode 100644 resolvconf/README.md create mode 100644 resolvconf/dns/resolvconf.go create mode 100644 resolvconf/resolvconf.go create mode 100644 resolvconf/resolvconf_test.go create mode 100644 sandbox/interface_freebsd.go create mode 100644 sandbox/interface_linux.go create mode 100644 sandbox/interface_windows.go create mode 100644 sandbox/namespace_linux.go create mode 100644 sandbox/namespace_unsupported.go create mode 100644 sandbox/namespace_windows.go create mode 100644 sandbox/neigh_freebsd.go create mode 100644 sandbox/neigh_linux.go create mode 100644 sandbox/neigh_windows.go create mode 100644 sandbox/options_linux.go create mode 100644 sandbox/route_linux.go create mode 100644 sandbox/sandbox.go create mode 100644 sandbox/sandbox_freebsd.go create mode 100644 sandbox/sandbox_linux_test.go create mode 100644 sandbox/sandbox_test.go create mode 100644 sandbox/sandbox_unsupported.go create mode 100644 sandbox/sandbox_unsupported_test.go create mode 100644 sandbox/test_freebsd.go create mode 100644 sandbox/test_linux.go create mode 100644 sandbox/test_windows.go create mode 100644 sandboxdata.go create mode 100644 sandboxdata_test.go create mode 100644 store.go create mode 100644 test/integration/README.md create mode 100644 test/integration/daemon-configs.bats create mode 100644 test/integration/daemon.cfg create mode 100644 test/integration/helpers.bash create mode 100644 types/types.go create mode 100644 types/types_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c03c9653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Coverage +*.tmp +*.coverprofile + +# IDE files +.project + +libnetwork-build.created diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 00000000..23b92956 --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,137 @@ +{ + "ImportPath": "github.com/docker/libnetwork", + "GoVersion": "go1.4.2", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/BurntSushi/toml", + "Comment": "v0.1.0-16-gf706d00", + "Rev": "f706d00e3de6abe700c994cdd545a1a4915af060" + }, + { + "ImportPath": "github.com/Sirupsen/logrus", + "Comment": "v0.6.4-12-g467d9d5", + "Rev": "467d9d55c2d2c17248441a8fc661561161f40d5e" + }, + { + "ImportPath": "github.com/armon/go-metrics", + "Rev": "eb0af217e5e9747e41dd5303755356b62d28e3ec" + }, + { + "ImportPath": "github.com/coreos/go-etcd/etcd", + "Comment": "v2.0.0-7-g73a8ef7", + "Rev": "73a8ef737e8ea002281a28b4cb92a1de121ad4c6" + }, + { + "ImportPath": "github.com/docker/docker/pkg/homedir", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/ioutils", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/mflag", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/parsers", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/plugins", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/proxy", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/reexec", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/stringid", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/docker/pkg/term", + "Comment": "v1.4.1-4106-g637023a", + "Rev": "637023a5f8d8347a0e271c09d5c9bc84fbc97693" + }, + { + "ImportPath": "github.com/docker/libcontainer/user", + "Comment": "v1.4.0-495-g3e66118", + "Rev": "3e661186ba24f259d3860f067df052c7f6904bee" + }, + { + "ImportPath": "github.com/docker/libkv", + "Rev": "60c7c881345b3c67defc7f93a8297debf041d43c" + }, + { + "ImportPath": "github.com/godbus/dbus", + "Comment": "v2-3-g4160802", + "Rev": "41608027bdce7bfa8959d653a00b954591220e67" + }, + { + "ImportPath": "github.com/gorilla/context", + "Rev": "215affda49addc4c8ef7e2534915df2c8c35c6cd" + }, + { + "ImportPath": "github.com/gorilla/mux", + "Rev": "8096f47503459bcc74d1f4c487b7e6e42e5746b5" + }, + { + "ImportPath": "github.com/hashicorp/consul/api", + "Comment": "v0.5.0rc1-66-g954aec6", + "Rev": "954aec66231b79c161a4122b023fbcad13047f79" + }, + { + "ImportPath": "github.com/hashicorp/go-msgpack/codec", + "Rev": "71c2886f5a673a35f909803f38ece5810165097b" + }, + { + "ImportPath": "github.com/hashicorp/memberlist", + "Rev": "9a1e242e454d2443df330bdd51a436d5a9058fc4" + }, + { + "ImportPath": "github.com/hashicorp/serf/serf", + "Comment": "v0.6.4", + "Rev": "7151adcef72687bf95f451a2e0ba15cb19412bf2" + }, + { + "ImportPath": "github.com/samuel/go-zookeeper/zk", + "Rev": "d0e0d8e11f318e000a8cc434616d69e329edc374" + }, + { + "ImportPath": "github.com/stretchr/objx", + "Rev": "cbeaeb16a013161a98496fad62933b1d21786672" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "dab07ac62d4905d3e48d17dc549c684ac3b7c15a" + }, + { + "ImportPath": "github.com/stretchr/testify/mock", + "Rev": "dab07ac62d4905d3e48d17dc549c684ac3b7c15a" + }, + { + "ImportPath": "github.com/vishvananda/netns", + "Rev": "493029407eeb434d0c2d44e02ea072ff2488d322" + }, + { + "ImportPath": "github.com/vishvananda/netlink", + "Rev": "4b5dce31de6d42af5bb9811c6d265472199e0fec" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 00000000..4cdaa53d --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e06d2081 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 00000000..69f1e9b8 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,5 @@ +Alessandro Boch (@aboch) +Alexandr Morozov (@LK4D4) +Arnaud Porterie (@icecrime) +Jana Radhakrishnan (@mrjana) +Madhu Venugopal (@mavenugo) diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..deb510cf --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +.PHONY: all all-local build build-local check check-code check-format run-tests check-local install-deps coveralls circle-ci +SHELL=/bin/bash +build_image=libnetwork-build +dockerargs = --privileged -v $(shell pwd):/go/src/github.com/docker/libnetwork -w /go/src/github.com/docker/libnetwork +container_env = -e "INSIDECONTAINER=-incontainer=true" +docker = docker run --rm ${dockerargs} ${container_env} ${build_image} +ciargs = -e "COVERALLS_TOKEN=$$COVERALLS_TOKEN" -e "INSIDECONTAINER=-incontainer=true" +cidocker = docker run ${ciargs} ${dockerargs} golang:1.4 + +all: ${build_image}.created + ${docker} make all-local + +all-local: check-local build-local + +${build_image}.created: + docker run --name=libnetworkbuild -v $(shell pwd):/go/src/github.com/docker/libnetwork -w /go/src/github.com/docker/libnetwork golang:1.4 make install-deps + docker commit libnetworkbuild ${build_image} + docker rm libnetworkbuild + touch ${build_image}.created + +build: ${build_image}.created + ${docker} make build-local + +build-local: + $(shell which godep) go build -tags libnetwork_discovery ./... + +check: ${build_image}.created + ${docker} make check-local + +check-code: + @echo "Checking code... " + test -z "$$(golint ./... | tee /dev/stderr)" + go vet ./... + @echo "Done checking code" + +check-format: + @echo "Checking format... " + test -z "$$(goimports -l . | grep -v Godeps/_workspace/src/ | tee /dev/stderr)" + @echo "Done checking format" + +run-tests: + @echo "Running tests... " + @echo "mode: count" > coverage.coverprofile + @for dir in $$(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d); do \ + if ls $$dir/*.go &> /dev/null; then \ + pushd . &> /dev/null ; \ + cd $$dir ; \ + $(shell which godep) go test ${INSIDECONTAINER} -test.parallel 3 -test.v -covermode=count -coverprofile=./profile.tmp ; \ + ret=$$? ;\ + if [ $$ret -ne 0 ]; then exit $$ret; fi ;\ + popd &> /dev/null; \ + if [ -f $$dir/profile.tmp ]; then \ + cat $$dir/profile.tmp | tail -n +2 >> coverage.coverprofile ; \ + rm $$dir/profile.tmp ; \ + fi ; \ + fi ; \ + done + @echo "Done running tests" + +check-local: check-format check-code run-tests + +install-deps: + apt-get update && apt-get -y install iptables + git clone https://github.com/golang/tools /go/src/golang.org/x/tools + go install golang.org/x/tools/cmd/vet + go install golang.org/x/tools/cmd/goimports + go install golang.org/x/tools/cmd/cover + go get github.com/tools/godep + go get github.com/golang/lint/golint + go get github.com/mattn/goveralls + +coveralls: + -@goveralls -service circleci -coverprofile=coverage.coverprofile -repotoken $$COVERALLS_TOKEN + +# CircleCI's Docker fails when cleaning up using the --rm flag +# The following target is a workaround for this + +circle-ci: + @${cidocker} make install-deps check-local coveralls diff --git a/README.md b/README.md new file mode 100644 index 00000000..90fcbe01 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# libnetwork - networking for containers + +[![Circle CI](https://circleci.com/gh/docker/libnetwork/tree/master.svg?style=svg)](https://circleci.com/gh/docker/libnetwork/tree/master) [![Coverage Status](https://coveralls.io/repos/docker/libnetwork/badge.svg)](https://coveralls.io/r/docker/libnetwork) [![GoDoc](https://godoc.org/github.com/docker/libnetwork?status.svg)](https://godoc.org/github.com/docker/libnetwork) + +Libnetwork provides a native Go implementation for connecting containers + +The goal of libnetwork is to deliver a robust Container Network Model that provides a consistent programming interface and the required network abstractions for applications. + +**NOTE**: libnetwork project is under heavy development and is not ready for general use. + +#### Design +Please refer to the [design](docs/design.md) for more information. + +#### Using libnetwork + +There are many networking solutions available to suit a broad range of use-cases. libnetwork uses a driver / plugin model to support all of these solutions while abstracting the complexity of the driver implementations by exposing a simple and consistent Network Model to users. + + +```go + // Create a new controller instance + controller, err := libnetwork.New() + if err != nil { + return + } + + // Select and configure the network driver + networkType := "bridge" + + driverOptions := options.Generic{} + genericOption := make(map[string]interface{}) + genericOption[netlabel.GenericData] = driverOptions + err := controller.ConfigureNetworkDriver(networkType, genericOption) + if err != nil { + return + } + + // Create a network for containers to join. + // NewNetwork accepts Variadic optional arguments that libnetwork and Drivers can make of + network, err := controller.NewNetwork(networkType, "network1") + if err != nil { + return + } + + // For each new container: allocate IP and interfaces. The returned network + // settings will be used for container infos (inspect and such), as well as + // iptables rules for port publishing. This info is contained or accessible + // from the returned endpoint. + ep, err := network.CreateEndpoint("Endpoint1") + if err != nil { + return + } + + // A container can join the endpoint by providing the container ID to the join + // api. + // Join accepts Variadic arguments which will be made use of by libnetwork and Drivers + err = ep.Join("container1", + libnetwork.JoinOptionHostname("test"), + libnetwork.JoinOptionDomainname("docker.io")) + if err != nil { + return + } + + // libnetwork client can check the endpoint's operational data via the Info() API + epInfo, err := ep.DriverInfo() + mapData, ok := epInfo[netlabel.PortMap] + if ok { + portMapping, ok := mapData.([]netutils.PortBinding) + if ok { + fmt.Printf("Current port mapping for endpoint %s: %v", ep.Name(), portMapping) + } + } + +``` +#### Current Status +Please watch this space for updates on the progress. + +Currently libnetwork is nothing more than an attempt to modularize the Docker platform's networking subsystem by moving it into libnetwork as a library. + +## Future +Please refer to [roadmap](ROADMAP.md) for more information. + +## Contributing + +Want to hack on libnetwork? [Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) apply. + +## Copyright and license +Code and documentation copyright 2015 Docker, inc. Code released under the Apache 2.0 license. Docs released under Creative commons. + diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..9cb3174e --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,20 @@ +# Roadmap + +This document defines the high-level goals of the libnetwork project. See [Project Planning](#project-planning) for information on Releases. + +## Long-term Goal + +libnetwork project will follow Docker and Linux philosophy of delivering small, highly modular and composable tools that works well independently. +libnetwork aims to satisfy that composable need for Networking in Containers. + +## Short-term Goals + +- Modularize the networking logic in Docker Engine and libcontainer in to a single, reusable library +- Replace the networking subsystem of Docker Engine, with libnetwork +- Define a flexible model that allows local and remote drivers to provide networking to containers +- Provide a stand-alone tool "dnet" for managing and testing libnetwork + +Project Planning +================ + +[Project Pages](https://github.com/docker/libnetwork/wiki) define the goals for each Milestone and identify the release-relationship to the Docker Platform. diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..2b5b5773 --- /dev/null +++ b/api/api.go @@ -0,0 +1,807 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/types" + "github.com/gorilla/mux" +) + +var ( + successResponse = responseStatus{Status: "Success", StatusCode: http.StatusOK} + createdResponse = responseStatus{Status: "Created", StatusCode: http.StatusCreated} + mismatchResponse = responseStatus{Status: "Body/URI parameter mismatch", StatusCode: http.StatusBadRequest} + badQueryResponse = responseStatus{Status: "Unsupported query", StatusCode: http.StatusBadRequest} +) + +const ( + // Resource name regex + regex = "[a-zA-Z_0-9-]+" + // Router URL variable definition + nwName = "{" + urlNwName + ":" + regex + "}" + nwID = "{" + urlNwID + ":" + regex + "}" + nwPID = "{" + urlNwPID + ":" + regex + "}" + epName = "{" + urlEpName + ":" + regex + "}" + epID = "{" + urlEpID + ":" + regex + "}" + epPID = "{" + urlEpPID + ":" + regex + "}" + cnID = "{" + urlCnID + ":" + regex + "}" + + // Though this name can be anything, in order to support default network, + // we will keep it as name + urlNwName = "name" + // Internal URL variable name, they can be anything + urlNwID = "network-id" + urlNwPID = "network-partial-id" + urlEpName = "endpoint-name" + urlEpID = "endpoint-id" + urlEpPID = "endpoint-partial-id" + urlCnID = "container-id" + + // BridgeNetworkDriver is the built-in default for Network Driver + BridgeNetworkDriver = "bridge" +) + +// NewHTTPHandler creates and initialize the HTTP handler to serve the requests for libnetwork +func NewHTTPHandler(c libnetwork.NetworkController) func(w http.ResponseWriter, req *http.Request) { + h := &httpHandler{c: c} + h.initRouter() + return h.handleRequest +} + +type responseStatus struct { + Status string + StatusCode int +} + +func (r *responseStatus) isOK() bool { + return r.StatusCode == http.StatusOK || r.StatusCode == http.StatusCreated +} + +type processor func(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) + +type httpHandler struct { + c libnetwork.NetworkController + r *mux.Router +} + +func (h *httpHandler) handleRequest(w http.ResponseWriter, req *http.Request) { + // Make sure the service is there + if h.c == nil { + http.Error(w, "NetworkController is not available", http.StatusServiceUnavailable) + return + } + + // Get handler from router and execute it + h.r.ServeHTTP(w, req) +} + +func (h *httpHandler) initRouter() { + m := map[string][]struct { + url string + qrs []string + fct processor + }{ + "GET": { + // Order matters + {"/networks", []string{"name", nwName}, procGetNetworks}, + {"/networks", []string{"partial-id", nwPID}, procGetNetworks}, + {"/networks", nil, procGetNetworks}, + {"/networks/" + nwID, nil, procGetNetwork}, + {"/networks/" + nwID + "/endpoints", []string{"name", epName}, procGetEndpoints}, + {"/networks/" + nwID + "/endpoints", []string{"partial-id", epPID}, procGetEndpoints}, + {"/networks/" + nwID + "/endpoints", nil, procGetEndpoints}, + {"/networks/" + nwID + "/endpoints/" + epID, nil, procGetEndpoint}, + {"/services", []string{"network", nwName}, procGetServices}, + {"/services", []string{"name", epName}, procGetServices}, + {"/services", []string{"partial-id", epPID}, procGetServices}, + {"/services", nil, procGetServices}, + {"/services/" + epID, nil, procGetService}, + {"/services/" + epID + "/backend", nil, procGetContainers}, + }, + "POST": { + {"/networks", nil, procCreateNetwork}, + {"/networks/" + nwID + "/endpoints", nil, procCreateEndpoint}, + {"/networks/" + nwID + "/endpoints/" + epID + "/containers", nil, procJoinEndpoint}, + {"/services", nil, procPublishService}, + {"/services/" + epID + "/backend", nil, procAttachBackend}, + }, + "DELETE": { + {"/networks/" + nwID, nil, procDeleteNetwork}, + {"/networks/" + nwID + "/endpoints/" + epID, nil, procDeleteEndpoint}, + {"/networks/" + nwID + "/endpoints/" + epID + "/containers/" + cnID, nil, procLeaveEndpoint}, + {"/services/" + epID, nil, procUnpublishService}, + {"/services/" + epID + "/backend/" + cnID, nil, procDetachBackend}, + }, + } + + h.r = mux.NewRouter() + for method, routes := range m { + for _, route := range routes { + r := h.r.Path("/{.*}" + route.url).Methods(method).HandlerFunc(makeHandler(h.c, route.fct)) + if route.qrs != nil { + r.Queries(route.qrs...) + } + + r = h.r.Path(route.url).Methods(method).HandlerFunc(makeHandler(h.c, route.fct)) + if route.qrs != nil { + r.Queries(route.qrs...) + } + } + } +} + +func makeHandler(ctrl libnetwork.NetworkController, fct processor) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + var ( + body []byte + err error + ) + if req.Body != nil { + body, err = ioutil.ReadAll(req.Body) + if err != nil { + http.Error(w, "Invalid body: "+err.Error(), http.StatusBadRequest) + return + } + } + + mvars := mux.Vars(req) + rvars := req.URL.Query() + // workaround a mux issue which filters out valid queries with empty value + for k := range rvars { + if _, ok := mvars[k]; !ok { + if rvars.Get(k) == "" { + mvars[k] = "" + } + } + } + + res, rsp := fct(ctrl, mvars, body) + if !rsp.isOK() { + http.Error(w, rsp.Status, rsp.StatusCode) + return + } + if res != nil { + writeJSON(w, rsp.StatusCode, res) + } + } +} + +/***************** + Resource Builders +******************/ + +func buildNetworkResource(nw libnetwork.Network) *networkResource { + r := &networkResource{} + if nw != nil { + r.Name = nw.Name() + r.ID = nw.ID() + r.Type = nw.Type() + epl := nw.Endpoints() + r.Endpoints = make([]*endpointResource, 0, len(epl)) + for _, e := range epl { + epr := buildEndpointResource(e) + r.Endpoints = append(r.Endpoints, epr) + } + } + return r +} + +func buildEndpointResource(ep libnetwork.Endpoint) *endpointResource { + r := &endpointResource{} + if ep != nil { + r.Name = ep.Name() + r.ID = ep.ID() + r.Network = ep.Network() + } + return r +} + +func buildContainerResource(ci libnetwork.ContainerInfo) *containerResource { + r := &containerResource{} + if ci != nil { + r.ID = ci.ID() + } + return r +} + +/**************** + Options Parsers +*****************/ + +func (nc *networkCreate) parseOptions() []libnetwork.NetworkOption { + var setFctList []libnetwork.NetworkOption + + if nc.Options != nil { + setFctList = append(setFctList, libnetwork.NetworkOptionGeneric(nc.Options)) + } + + return setFctList +} + +func (ej *endpointJoin) parseOptions() []libnetwork.EndpointOption { + var setFctList []libnetwork.EndpointOption + if ej.HostName != "" { + setFctList = append(setFctList, libnetwork.JoinOptionHostname(ej.HostName)) + } + if ej.DomainName != "" { + setFctList = append(setFctList, libnetwork.JoinOptionDomainname(ej.DomainName)) + } + if ej.HostsPath != "" { + setFctList = append(setFctList, libnetwork.JoinOptionHostsPath(ej.HostsPath)) + } + if ej.ResolvConfPath != "" { + setFctList = append(setFctList, libnetwork.JoinOptionResolvConfPath(ej.ResolvConfPath)) + } + if ej.UseDefaultSandbox { + setFctList = append(setFctList, libnetwork.JoinOptionUseDefaultSandbox()) + } + if ej.DNS != nil { + for _, d := range ej.DNS { + setFctList = append(setFctList, libnetwork.JoinOptionDNS(d)) + } + } + if ej.ExtraHosts != nil { + for _, e := range ej.ExtraHosts { + setFctList = append(setFctList, libnetwork.JoinOptionExtraHost(e.Name, e.Address)) + } + } + if ej.ParentUpdates != nil { + for _, p := range ej.ParentUpdates { + setFctList = append(setFctList, libnetwork.JoinOptionParentUpdate(p.EndpointID, p.Name, p.Address)) + } + } + return setFctList +} + +/****************** + Process functions +*******************/ + +func processCreateDefaults(c libnetwork.NetworkController, nc *networkCreate) { + if nc.NetworkType == "" { + nc.NetworkType = c.Config().Daemon.DefaultDriver + } + if nc.NetworkType == BridgeNetworkDriver { + if nc.Options == nil { + nc.Options = make(map[string]interface{}) + } + genericData, ok := nc.Options[netlabel.GenericData] + if !ok { + genericData = make(map[string]interface{}) + } + gData := genericData.(map[string]interface{}) + + if _, ok := gData["BridgeName"]; !ok { + gData["BridgeName"] = nc.Name + } + if _, ok := gData["AllowNonDefaultBridge"]; !ok { + gData["AllowNonDefaultBridge"] = "true" + } + nc.Options[netlabel.GenericData] = genericData + } +} + +/*************************** + NetworkController interface +****************************/ +func procCreateNetwork(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var create networkCreate + + err := json.Unmarshal(body, &create) + if err != nil { + return "", &responseStatus{Status: "Invalid body: " + err.Error(), StatusCode: http.StatusBadRequest} + } + processCreateDefaults(c, &create) + + nw, err := c.NewNetwork(create.NetworkType, create.Name, create.parseOptions()...) + if err != nil { + return "", convertNetworkError(err) + } + + return nw.ID(), &createdResponse +} + +func procGetNetwork(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + t, by := detectNetworkTarget(vars) + nw, errRsp := findNetwork(c, t, by) + if !errRsp.isOK() { + return nil, errRsp + } + return buildNetworkResource(nw), &successResponse +} + +func procGetNetworks(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var list []*networkResource + + // Look for query filters and validate + name, queryByName := vars[urlNwName] + shortID, queryByPid := vars[urlNwPID] + if queryByName && queryByPid { + return nil, &badQueryResponse + } + + if queryByName { + if nw, errRsp := findNetwork(c, name, byName); errRsp.isOK() { + list = append(list, buildNetworkResource(nw)) + } + } else if queryByPid { + // Return all the prefix-matching networks + l := func(nw libnetwork.Network) bool { + if strings.HasPrefix(nw.ID(), shortID) { + list = append(list, buildNetworkResource(nw)) + } + return false + } + c.WalkNetworks(l) + } else { + for _, nw := range c.Networks() { + list = append(list, buildNetworkResource(nw)) + } + } + + return list, &successResponse +} + +/****************** + Network interface +*******************/ +func procCreateEndpoint(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var ec endpointCreate + + err := json.Unmarshal(body, &ec) + if err != nil { + return "", &responseStatus{Status: "Invalid body: " + err.Error(), StatusCode: http.StatusBadRequest} + } + + nwT, nwBy := detectNetworkTarget(vars) + n, errRsp := findNetwork(c, nwT, nwBy) + if !errRsp.isOK() { + return "", errRsp + } + + var setFctList []libnetwork.EndpointOption + if ec.ExposedPorts != nil { + setFctList = append(setFctList, libnetwork.CreateOptionExposedPorts(ec.ExposedPorts)) + } + if ec.PortMapping != nil { + setFctList = append(setFctList, libnetwork.CreateOptionPortMapping(ec.PortMapping)) + } + + ep, err := n.CreateEndpoint(ec.Name, setFctList...) + if err != nil { + return "", convertNetworkError(err) + } + + return ep.ID(), &createdResponse +} + +func procGetEndpoint(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + nwT, nwBy := detectNetworkTarget(vars) + epT, epBy := detectEndpointTarget(vars) + + ep, errRsp := findEndpoint(c, nwT, epT, nwBy, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + return buildEndpointResource(ep), &successResponse +} + +func procGetEndpoints(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + // Look for query filters and validate + name, queryByName := vars[urlEpName] + shortID, queryByPid := vars[urlEpPID] + if queryByName && queryByPid { + return nil, &badQueryResponse + } + + nwT, nwBy := detectNetworkTarget(vars) + nw, errRsp := findNetwork(c, nwT, nwBy) + if !errRsp.isOK() { + return nil, errRsp + } + + var list []*endpointResource + + // If query parameter is specified, return a filtered collection + if queryByName { + if ep, errRsp := findEndpoint(c, nwT, name, nwBy, byName); errRsp.isOK() { + list = append(list, buildEndpointResource(ep)) + } + } else if queryByPid { + // Return all the prefix-matching endpoints + l := func(ep libnetwork.Endpoint) bool { + if strings.HasPrefix(ep.ID(), shortID) { + list = append(list, buildEndpointResource(ep)) + } + return false + } + nw.WalkEndpoints(l) + } else { + for _, ep := range nw.Endpoints() { + epr := buildEndpointResource(ep) + list = append(list, epr) + } + } + + return list, &successResponse +} + +func procDeleteNetwork(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + target, by := detectNetworkTarget(vars) + + nw, errRsp := findNetwork(c, target, by) + if !errRsp.isOK() { + return nil, errRsp + } + + err := nw.Delete() + if err != nil { + return nil, convertNetworkError(err) + } + + return nil, &successResponse +} + +/****************** + Endpoint interface +*******************/ +func procJoinEndpoint(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var ej endpointJoin + err := json.Unmarshal(body, &ej) + if err != nil { + return nil, &responseStatus{Status: "Invalid body: " + err.Error(), StatusCode: http.StatusBadRequest} + } + + nwT, nwBy := detectNetworkTarget(vars) + epT, epBy := detectEndpointTarget(vars) + + ep, errRsp := findEndpoint(c, nwT, epT, nwBy, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + err = ep.Join(ej.ContainerID, ej.parseOptions()...) + if err != nil { + return nil, convertNetworkError(err) + } + return ep.Info().SandboxKey(), &successResponse +} + +func procLeaveEndpoint(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + nwT, nwBy := detectNetworkTarget(vars) + epT, epBy := detectEndpointTarget(vars) + + ep, errRsp := findEndpoint(c, nwT, epT, nwBy, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + err := ep.Leave(vars[urlCnID]) + if err != nil { + return nil, convertNetworkError(err) + } + + return nil, &successResponse +} + +func procDeleteEndpoint(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + nwT, nwBy := detectNetworkTarget(vars) + epT, epBy := detectEndpointTarget(vars) + + ep, errRsp := findEndpoint(c, nwT, epT, nwBy, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + err := ep.Delete() + if err != nil { + return nil, convertNetworkError(err) + } + + return nil, &successResponse +} + +/****************** + Service interface +*******************/ +func procGetServices(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + // Look for query filters and validate + nwName, filterByNwName := vars[urlNwName] + svName, queryBySvName := vars[urlEpName] + shortID, queryBySvPID := vars[urlEpPID] + + if filterByNwName && queryBySvName || filterByNwName && queryBySvPID || queryBySvName && queryBySvPID { + return nil, &badQueryResponse + } + + var list []*endpointResource + + switch { + case filterByNwName: + // return all service present on the specified network + nw, errRsp := findNetwork(c, nwName, byName) + if !errRsp.isOK() { + return list, &successResponse + } + for _, ep := range nw.Endpoints() { + epr := buildEndpointResource(ep) + list = append(list, epr) + } + case queryBySvName: + // Look in each network for the service with the specified name + l := func(ep libnetwork.Endpoint) bool { + if ep.Name() == svName { + list = append(list, buildEndpointResource(ep)) + return true + } + return false + } + for _, nw := range c.Networks() { + nw.WalkEndpoints(l) + } + case queryBySvPID: + // Return all the prefix-matching services + l := func(ep libnetwork.Endpoint) bool { + if strings.HasPrefix(ep.ID(), shortID) { + list = append(list, buildEndpointResource(ep)) + } + return false + } + for _, nw := range c.Networks() { + nw.WalkEndpoints(l) + } + default: + for _, nw := range c.Networks() { + for _, ep := range nw.Endpoints() { + epr := buildEndpointResource(ep) + list = append(list, epr) + } + } + } + + return list, &successResponse +} + +func procGetService(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + epT, epBy := detectEndpointTarget(vars) + sv, errRsp := findService(c, epT, epBy) + if !errRsp.isOK() { + return nil, endpointToService(errRsp) + } + return buildEndpointResource(sv), &successResponse +} + +func procGetContainers(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + epT, epBy := detectEndpointTarget(vars) + sv, errRsp := findService(c, epT, epBy) + if !errRsp.isOK() { + return nil, endpointToService(errRsp) + } + var list []*containerResource + if sv.ContainerInfo() != nil { + list = append(list, buildContainerResource(sv.ContainerInfo())) + } + return list, &successResponse +} + +func procPublishService(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var sp servicePublish + + err := json.Unmarshal(body, &sp) + if err != nil { + return "", &responseStatus{Status: "Invalid body: " + err.Error(), StatusCode: http.StatusBadRequest} + } + + n, errRsp := findNetwork(c, sp.Network, byName) + if !errRsp.isOK() { + return "", errRsp + } + + var setFctList []libnetwork.EndpointOption + if sp.ExposedPorts != nil { + setFctList = append(setFctList, libnetwork.CreateOptionExposedPorts(sp.ExposedPorts)) + } + if sp.PortMapping != nil { + setFctList = append(setFctList, libnetwork.CreateOptionPortMapping(sp.PortMapping)) + } + + ep, err := n.CreateEndpoint(sp.Name, setFctList...) + if err != nil { + return "", endpointToService(convertNetworkError(err)) + } + + return ep.ID(), &createdResponse +} + +func procUnpublishService(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + epT, epBy := detectEndpointTarget(vars) + sv, errRsp := findService(c, epT, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + err := sv.Delete() + if err != nil { + return nil, endpointToService(convertNetworkError(err)) + } + return nil, &successResponse +} + +func procAttachBackend(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + var bk endpointJoin + err := json.Unmarshal(body, &bk) + if err != nil { + return nil, &responseStatus{Status: "Invalid body: " + err.Error(), StatusCode: http.StatusBadRequest} + } + + epT, epBy := detectEndpointTarget(vars) + sv, errRsp := findService(c, epT, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + err = sv.Join(bk.ContainerID, bk.parseOptions()...) + if err != nil { + return nil, convertNetworkError(err) + } + return sv.Info().SandboxKey(), &successResponse +} + +func procDetachBackend(c libnetwork.NetworkController, vars map[string]string, body []byte) (interface{}, *responseStatus) { + epT, epBy := detectEndpointTarget(vars) + sv, errRsp := findService(c, epT, epBy) + if !errRsp.isOK() { + return nil, errRsp + } + + err := sv.Leave(vars[urlCnID]) + if err != nil { + return nil, convertNetworkError(err) + } + + return nil, &successResponse +} + +/*********** + Utilities +************/ +const ( + byID = iota + byName +) + +func detectNetworkTarget(vars map[string]string) (string, int) { + if target, ok := vars[urlNwName]; ok { + return target, byName + } + if target, ok := vars[urlNwID]; ok { + return target, byID + } + // vars are populated from the URL, following cannot happen + panic("Missing URL variable parameter for network") +} + +func detectEndpointTarget(vars map[string]string) (string, int) { + if target, ok := vars[urlEpName]; ok { + return target, byName + } + if target, ok := vars[urlEpID]; ok { + return target, byID + } + // vars are populated from the URL, following cannot happen + panic("Missing URL variable parameter for endpoint") +} + +func findNetwork(c libnetwork.NetworkController, s string, by int) (libnetwork.Network, *responseStatus) { + var ( + nw libnetwork.Network + err error + ) + switch by { + case byID: + nw, err = c.NetworkByID(s) + case byName: + if s == "" { + s = c.Config().Daemon.DefaultNetwork + } + nw, err = c.NetworkByName(s) + default: + panic(fmt.Sprintf("unexpected selector for network search: %d", by)) + } + if err != nil { + if _, ok := err.(types.NotFoundError); ok { + return nil, &responseStatus{Status: "Resource not found: Network", StatusCode: http.StatusNotFound} + } + return nil, &responseStatus{Status: err.Error(), StatusCode: http.StatusBadRequest} + } + return nw, &successResponse +} + +func findEndpoint(c libnetwork.NetworkController, ns, es string, nwBy, epBy int) (libnetwork.Endpoint, *responseStatus) { + nw, errRsp := findNetwork(c, ns, nwBy) + if !errRsp.isOK() { + return nil, errRsp + } + var ( + err error + ep libnetwork.Endpoint + ) + switch epBy { + case byID: + ep, err = nw.EndpointByID(es) + case byName: + ep, err = nw.EndpointByName(es) + default: + panic(fmt.Sprintf("unexpected selector for endpoint search: %d", epBy)) + } + if err != nil { + if _, ok := err.(types.NotFoundError); ok { + return nil, &responseStatus{Status: "Resource not found: Endpoint", StatusCode: http.StatusNotFound} + } + return nil, &responseStatus{Status: err.Error(), StatusCode: http.StatusBadRequest} + } + return ep, &successResponse +} + +func findService(c libnetwork.NetworkController, svs string, svBy int) (libnetwork.Endpoint, *responseStatus) { + for _, nw := range c.Networks() { + var ( + ep libnetwork.Endpoint + err error + ) + switch svBy { + case byID: + ep, err = nw.EndpointByID(svs) + case byName: + ep, err = nw.EndpointByName(svs) + default: + panic(fmt.Sprintf("unexpected selector for service search: %d", svBy)) + } + if err == nil { + return ep, &successResponse + } else if _, ok := err.(types.NotFoundError); !ok { + return nil, convertNetworkError(err) + } + } + return nil, &responseStatus{Status: "Service not found", StatusCode: http.StatusNotFound} +} + +func endpointToService(rsp *responseStatus) *responseStatus { + rsp.Status = strings.Replace(rsp.Status, "endpoint", "service", -1) + return rsp +} + +func convertNetworkError(err error) *responseStatus { + var code int + switch err.(type) { + case types.BadRequestError: + code = http.StatusBadRequest + case types.ForbiddenError: + code = http.StatusForbidden + case types.NotFoundError: + code = http.StatusNotFound + case types.TimeoutError: + code = http.StatusRequestTimeout + case types.NotImplementedError: + code = http.StatusNotImplemented + case types.NoServiceError: + code = http.StatusServiceUnavailable + case types.InternalError: + code = http.StatusInternalServerError + default: + code = http.StatusInternalServerError + } + return &responseStatus{Status: err.Error(), StatusCode: code} +} + +func writeJSON(w http.ResponseWriter, code int, v interface{}) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + return json.NewEncoder(w).Encode(v) +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 00000000..9916747c --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,2130 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "runtime" + "testing" + + "github.com/docker/docker/pkg/reexec" + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/sandbox" + "github.com/docker/libnetwork/types" +) + +const ( + bridgeNetType = "bridge" + bridgeName = "docker0" +) + +func getEmptyGenericOption() map[string]interface{} { + genericOption := make(map[string]interface{}) + genericOption[netlabel.GenericData] = options.Generic{} + return genericOption +} + +func i2s(i interface{}) string { + s, ok := i.(string) + if !ok { + panic(fmt.Sprintf("Failed i2s for %v", i)) + } + return s +} + +func i2e(i interface{}) *endpointResource { + s, ok := i.(*endpointResource) + if !ok { + panic(fmt.Sprintf("Failed i2e for %v", i)) + } + return s +} + +func i2eL(i interface{}) []*endpointResource { + s, ok := i.([]*endpointResource) + if !ok { + panic(fmt.Sprintf("Failed i2eL for %v", i)) + } + return s +} + +func i2n(i interface{}) *networkResource { + s, ok := i.(*networkResource) + if !ok { + panic(fmt.Sprintf("Failed i2n for %v", i)) + } + return s +} + +func i2nL(i interface{}) []*networkResource { + s, ok := i.([]*networkResource) + if !ok { + panic(fmt.Sprintf("Failed i2nL for %v", i)) + } + return s +} + +func i2cL(i interface{}) []*containerResource { + s, ok := i.([]*containerResource) + if !ok { + panic(fmt.Sprintf("Failed i2cL for %v", i)) + } + return s +} + +func createTestNetwork(t *testing.T, network string) (libnetwork.NetworkController, libnetwork.Network) { + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + netOption := options.Generic{ + netlabel.GenericData: options.Generic{ + "BridgeName": network, + "AllowNonDefaultBridge": true, + }, + } + netGeneric := libnetwork.NetworkOptionGeneric(netOption) + nw, err := c.NewNetwork(bridgeNetType, network, netGeneric) + if err != nil { + t.Fatal(err) + } + + return c, nw +} + +func TestMain(m *testing.M) { + if reexec.Init() { + return + } + os.Exit(m.Run()) +} + +func TestJoinOptionParser(t *testing.T) { + hn := "host1" + dn := "docker.com" + hp := "/etc/hosts" + rc := "/etc/resolv.conf" + dnss := []string{"8.8.8.8", "172.28.34.5"} + ehs := []endpointExtraHost{endpointExtraHost{Name: "extra1", Address: "172.28.9.1"}, endpointExtraHost{Name: "extra2", Address: "172.28.9.2"}} + pus := []endpointParentUpdate{endpointParentUpdate{EndpointID: "abc123def456", Name: "serv1", Address: "172.28.30.123"}} + + ej := endpointJoin{ + HostName: hn, + DomainName: dn, + HostsPath: hp, + ResolvConfPath: rc, + DNS: dnss, + ExtraHosts: ehs, + ParentUpdates: pus, + UseDefaultSandbox: true, + } + + if len(ej.parseOptions()) != 10 { + t.Fatalf("Failed to generate all libnetwork.EndpointJoinOption methods libnetwork.EndpointJoinOption method") + } + +} + +func TestJson(t *testing.T) { + nc := networkCreate{NetworkType: bridgeNetType} + b, err := json.Marshal(nc) + if err != nil { + t.Fatal(err) + } + + var ncp networkCreate + err = json.Unmarshal(b, &ncp) + if err != nil { + t.Fatal(err) + } + + if nc.NetworkType != ncp.NetworkType { + t.Fatalf("Incorrect networkCreate after json encoding/deconding: %v", ncp) + } + + jl := endpointJoin{ContainerID: "abcdef456789"} + b, err = json.Marshal(jl) + if err != nil { + t.Fatal(err) + } + + var jld endpointJoin + err = json.Unmarshal(b, &jld) + if err != nil { + t.Fatal(err) + } + + if jl.ContainerID != jld.ContainerID { + t.Fatalf("Incorrect endpointJoin after json encoding/deconding: %v", jld) + } +} + +func TestCreateDeleteNetwork(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + badBody, err := json.Marshal("bad body") + if err != nil { + t.Fatal(err) + } + + vars := make(map[string]string) + _, errRsp := procCreateNetwork(c, nil, badBody) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected StatusBadRequest status code, got: %v", errRsp) + } + + incompleteBody, err := json.Marshal(networkCreate{}) + if err != nil { + t.Fatal(err) + } + + _, errRsp = procCreateNetwork(c, vars, incompleteBody) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected StatusBadRequest status code, got: %v", errRsp) + } + + ops := options.Generic{ + netlabel.EnableIPv6: true, + netlabel.GenericData: map[string]string{ + "BridgeName": "abc", + "AllowNonDefaultBridge": "true", + "FixedCIDRv6": "fe80::1/64", + "AddressIP": "172.28.30.254/24", + }, + } + nc := networkCreate{Name: "network_1", NetworkType: bridgeNetType, Options: ops} + goodBody, err := json.Marshal(nc) + if err != nil { + t.Fatal(err) + } + + _, errRsp = procCreateNetwork(c, vars, goodBody) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + vars[urlNwName] = "" + _, errRsp = procDeleteNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected to fail but succeeded") + } + + vars[urlNwName] = "abc" + _, errRsp = procDeleteNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected to fail but succeeded") + } + + vars[urlNwName] = "network_1" + _, errRsp = procDeleteNetwork(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } +} + +func TestGetNetworksAndEndpoints(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + ops := options.Generic{ + netlabel.GenericData: map[string]string{ + "BridgeName": "api_test_nw", + "AllowNonDefaultBridge": "true", + }, + } + + nc := networkCreate{Name: "sh", NetworkType: bridgeNetType, Options: ops} + body, err := json.Marshal(nc) + if err != nil { + t.Fatal(err) + } + + vars := make(map[string]string) + inid, errRsp := procCreateNetwork(c, vars, body) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + nid, ok := inid.(string) + if !ok { + t.FailNow() + } + + ec1 := endpointCreate{ + Name: "ep1", + ExposedPorts: []types.TransportPort{ + types.TransportPort{Proto: types.TCP, Port: uint16(5000)}, + types.TransportPort{Proto: types.UDP, Port: uint16(400)}, + types.TransportPort{Proto: types.TCP, Port: uint16(600)}, + }, + PortMapping: []types.PortBinding{ + types.PortBinding{Proto: types.TCP, Port: uint16(230), HostPort: uint16(23000)}, + types.PortBinding{Proto: types.UDP, Port: uint16(200), HostPort: uint16(22000)}, + types.PortBinding{Proto: types.TCP, Port: uint16(120), HostPort: uint16(12000)}, + }, + } + b1, err := json.Marshal(ec1) + if err != nil { + t.Fatal(err) + } + ec2 := endpointCreate{Name: "ep2"} + b2, err := json.Marshal(ec2) + if err != nil { + t.Fatal(err) + } + + vars[urlNwName] = "sh" + vars[urlEpName] = "ep1" + ieid1, errRsp := procCreateEndpoint(c, vars, b1) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + eid1 := i2s(ieid1) + vars[urlEpName] = "ep2" + ieid2, errRsp := procCreateEndpoint(c, vars, b2) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + eid2 := i2s(ieid2) + + vars[urlNwName] = "" + vars[urlEpName] = "ep1" + _, errRsp = procGetEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure but succeeded: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected to fail with http.StatusBadRequest, but got: %d", errRsp.StatusCode) + } + + vars = make(map[string]string) + vars[urlNwName] = "sh" + vars[urlEpID] = "" + _, errRsp = procGetEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure but succeeded: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected to fail with http.StatusBadRequest, but got: %d", errRsp.StatusCode) + } + + vars = make(map[string]string) + vars[urlNwID] = "" + vars[urlEpID] = eid1 + _, errRsp = procGetEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure but succeeded: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected to fail with http.StatusBadRequest, but got: %d", errRsp.StatusCode) + } + + // nw by name and ep by id + vars[urlNwName] = "sh" + i1, errRsp := procGetEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + // nw by name and ep by name + delete(vars, urlEpID) + vars[urlEpName] = "ep1" + i2, errRsp := procGetEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + // nw by id and ep by name + delete(vars, urlNwName) + vars[urlNwID] = nid + i3, errRsp := procGetEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + // nw by id and ep by id + delete(vars, urlEpName) + vars[urlEpID] = eid1 + i4, errRsp := procGetEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + id1 := i2e(i1).ID + if id1 != i2e(i2).ID || id1 != i2e(i3).ID || id1 != i2e(i4).ID { + t.Fatalf("Endpoints retireved via different query parameters differ: %v, %v, %v, %v", i1, i2, i3, i4) + } + + vars[urlNwName] = "" + _, errRsp = procGetEndpoints(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + delete(vars, urlNwName) + vars[urlNwID] = "fakeID" + _, errRsp = procGetEndpoints(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlNwID] = nid + _, errRsp = procGetEndpoints(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + vars[urlNwName] = "sh" + iepList, errRsp := procGetEndpoints(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + epList := i2eL(iepList) + if len(epList) != 2 { + t.Fatalf("Did not return the expected number (2) of endpoint resources: %d", len(epList)) + } + if "sh" != epList[0].Network || "sh" != epList[1].Network { + t.Fatalf("Did not find expected network name in endpoint resources") + } + + vars = make(map[string]string) + vars[urlNwName] = "" + _, errRsp = procGetNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Exepected failure, got: %v", errRsp) + } + vars[urlNwName] = "shhhhh" + _, errRsp = procGetNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Exepected failure, got: %v", errRsp) + } + vars[urlNwName] = "sh" + inr1, errRsp := procGetNetwork(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + nr1 := i2n(inr1) + + delete(vars, urlNwName) + vars[urlNwID] = "cacca" + _, errRsp = procGetNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + vars[urlNwID] = nid + inr2, errRsp := procGetNetwork(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("procgetNetworkByName() != procgetNetworkById(), %v vs %v", inr1, inr2) + } + nr2 := i2n(inr2) + if nr1.Name != nr2.Name || nr1.Type != nr2.Type || nr1.ID != nr2.ID || len(nr1.Endpoints) != len(nr2.Endpoints) { + t.Fatalf("Get by name and Get failure: %v", errRsp) + } + + if len(nr1.Endpoints) != 2 { + t.Fatalf("Did not find the expected number (2) of endpoint resources in the network resource: %d", len(nr1.Endpoints)) + } + for _, er := range nr1.Endpoints { + if er.ID != eid1 && er.ID != eid2 { + t.Fatalf("Did not find the expected endpoint resources in the network resource: %v", nr1.Endpoints) + } + } + + iList, errRsp := procGetNetworks(c, nil, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + netList := i2nL(iList) + if len(netList) != 1 { + t.Fatalf("Did not return the expected number of network resources") + } + if nid != netList[0].ID { + t.Fatalf("Did not find expected network %s: %v", nid, netList) + } + + _, errRsp = procDeleteNetwork(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Exepected failure, got: %v", errRsp) + } + + vars[urlEpName] = "ep1" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + delete(vars, urlEpName) + iepList, errRsp = procGetEndpoints(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + epList = i2eL(iepList) + if len(epList) != 1 { + t.Fatalf("Did not return the expected number (1) of endpoint resources: %d", len(epList)) + } + + vars[urlEpName] = "ep2" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + iepList, errRsp = procGetEndpoints(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + epList = i2eL(iepList) + if len(epList) != 0 { + t.Fatalf("Did not return the expected number (0) of endpoint resources: %d", len(epList)) + } + + _, errRsp = procDeleteNetwork(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + iList, errRsp = procGetNetworks(c, nil, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + netList = i2nL(iList) + if len(netList) != 0 { + t.Fatalf("Did not return the expected number of network resources") + } +} + +func TestProcGetServices(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + // Create 2 networks + netName1 := "production" + netOption := options.Generic{ + netlabel.GenericData: options.Generic{ + "BridgeName": netName1, + "AllowNonDefaultBridge": true, + }, + } + nw1, err := c.NewNetwork(bridgeNetType, netName1, libnetwork.NetworkOptionGeneric(netOption)) + if err != nil { + t.Fatal(err) + } + + netName2 := "work-dev" + netOption = options.Generic{ + netlabel.GenericData: options.Generic{ + "BridgeName": netName2, + "AllowNonDefaultBridge": true, + }, + } + nw2, err := c.NewNetwork(bridgeNetType, netName2, libnetwork.NetworkOptionGeneric(netOption)) + if err != nil { + t.Fatal(err) + } + + vars := make(map[string]string) + li, errRsp := procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list := i2eL(li) + if len(list) != 0 { + t.Fatalf("Unexpected services in response: %v", list) + } + + // Add a couple of services on one network and one on the other network + ep11, err := nw1.CreateEndpoint("db-prod") + if err != nil { + t.Fatal(err) + } + ep12, err := nw1.CreateEndpoint("web-prod") + if err != nil { + t.Fatal(err) + } + ep21, err := nw2.CreateEndpoint("db-dev") + if err != nil { + t.Fatal(err) + } + + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 3 { + t.Fatalf("Unexpected services in response: %v", list) + } + + // Filter by network + vars[urlNwName] = netName1 + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 2 { + t.Fatalf("Unexpected services in response: %v", list) + } + + vars[urlNwName] = netName2 + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 1 { + t.Fatalf("Unexpected services in response: %v", list) + } + + vars[urlNwName] = "unknown-network" + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 0 { + t.Fatalf("Unexpected services in response: %v", list) + } + + // Query by name + delete(vars, urlNwName) + vars[urlEpName] = "db-prod" + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 1 { + t.Fatalf("Unexpected services in response: %v", list) + } + + vars[urlEpName] = "no-service" + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 0 { + t.Fatalf("Unexpected services in response: %v", list) + } + + // Query by id or partial id + delete(vars, urlEpName) + vars[urlEpPID] = ep12.ID() + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 1 { + t.Fatalf("Unexpected services in response: %v", list) + } + if list[0].ID != ep12.ID() { + t.Fatalf("Unexpected element in response: %v", list) + } + + vars[urlEpPID] = "non-id" + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 0 { + t.Fatalf("Unexpected services in response: %v", list) + } + + delete(vars, urlEpPID) + err = ep11.Delete() + if err != nil { + t.Fatal(err) + } + err = ep12.Delete() + if err != nil { + t.Fatal(err) + } + err = ep21.Delete() + if err != nil { + t.Fatal(err) + } + + li, errRsp = procGetServices(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + list = i2eL(li) + if len(list) != 0 { + t.Fatalf("Unexpected services in response: %v", list) + } +} + +func TestProcGetService(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, nw := createTestNetwork(t, "network") + ep1, err := nw.CreateEndpoint("db") + if err != nil { + t.Fatal(err) + } + ep2, err := nw.CreateEndpoint("web") + if err != nil { + t.Fatal(err) + } + + vars := map[string]string{urlEpID: ""} + _, errRsp := procGetService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure, but suceeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d, but got: %d", http.StatusBadRequest, errRsp.StatusCode) + } + + vars[urlEpID] = "unknown-service-id" + _, errRsp = procGetService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure, but suceeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d. (%v)", http.StatusNotFound, errRsp.StatusCode, errRsp) + } + + vars[urlEpID] = ep1.ID() + si, errRsp := procGetService(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + sv := i2e(si) + if sv.ID != ep1.ID() { + t.Fatalf("Unexpected service resource returned: %v", sv) + } + + vars[urlEpID] = ep2.ID() + si, errRsp = procGetService(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + sv = i2e(si) + if sv.ID != ep2.ID() { + t.Fatalf("Unexpected service resource returned: %v", sv) + } +} + +func TestProcPublishUnpublishService(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, _ := createTestNetwork(t, "network") + vars := make(map[string]string) + + vbad, err := json.Marshal("bad service create data") + if err != nil { + t.Fatal(err) + } + _, errRsp := procPublishService(c, vars, vbad) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + b, err := json.Marshal(servicePublish{Name: ""}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procPublishService(c, vars, b) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + b, err = json.Marshal(servicePublish{Name: "db"}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procPublishService(c, vars, b) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + b, err = json.Marshal(servicePublish{Name: "db", Network: "unknown-network"}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procPublishService(c, vars, b) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d. Got: %v", http.StatusNotFound, errRsp) + } + + b, err = json.Marshal(servicePublish{Name: "", Network: "network"}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procPublishService(c, vars, b) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + b, err = json.Marshal(servicePublish{Name: "db", Network: "network"}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procPublishService(c, vars, b) + if errRsp != &createdResponse { + t.Fatalf("Unexpected failure: %v", errRsp) + } + + sp := servicePublish{ + Name: "web", + Network: "network", + ExposedPorts: []types.TransportPort{ + types.TransportPort{Proto: types.TCP, Port: uint16(6000)}, + types.TransportPort{Proto: types.UDP, Port: uint16(500)}, + types.TransportPort{Proto: types.TCP, Port: uint16(700)}, + }, + PortMapping: []types.PortBinding{ + types.PortBinding{Proto: types.TCP, Port: uint16(1230), HostPort: uint16(37000)}, + types.PortBinding{Proto: types.UDP, Port: uint16(1200), HostPort: uint16(36000)}, + types.PortBinding{Proto: types.TCP, Port: uint16(1120), HostPort: uint16(35000)}, + }, + } + b, err = json.Marshal(sp) + if err != nil { + t.Fatal(err) + } + si, errRsp := procPublishService(c, vars, b) + if errRsp != &createdResponse { + t.Fatalf("Unexpected failure: %v", errRsp) + } + sid := i2s(si) + + vars[urlEpID] = "" + _, errRsp = procUnpublishService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + vars[urlEpID] = "unknown-service-id" + _, errRsp = procUnpublishService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure but succeeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d. Got: %v", http.StatusNotFound, errRsp) + } + + vars[urlEpID] = sid + _, errRsp = procUnpublishService(c, vars, nil) + if !errRsp.isOK() { + t.Fatalf("Unexpected failure: %v", errRsp) + } + + _, errRsp = procGetService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure, but suceeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d. (%v)", http.StatusNotFound, errRsp.StatusCode, errRsp) + } +} + +func TestAttachDetachBackend(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, nw := createTestNetwork(t, "network") + ep1, err := nw.CreateEndpoint("db") + if err != nil { + t.Fatal(err) + } + + vars := make(map[string]string) + + vbad, err := json.Marshal("bad data") + if err != nil { + t.Fatal(err) + } + _, errRsp := procAttachBackend(c, vars, vbad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "endpoint" + bad, err := json.Marshal(endpointJoin{}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procAttachBackend(c, vars, bad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d. Got: %v", http.StatusNotFound, errRsp) + } + + _, errRsp = procGetContainers(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure. Got %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d. Got: %v", http.StatusNotFound, errRsp) + } + + vars[urlEpName] = "db" + _, errRsp = procAttachBackend(c, vars, bad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + cid := "abcdefghi" + jl := endpointJoin{ContainerID: cid} + jlb, err := json.Marshal(jl) + if err != nil { + t.Fatal(err) + } + + _, errRsp = procAttachBackend(c, vars, jlb) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure, got: %v", errRsp) + } + + cli, errRsp := procGetContainers(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure, got: %v", errRsp) + } + cl := i2cL(cli) + if len(cl) != 1 { + t.Fatalf("Did not find expected number of containers attached to the service: %d", len(cl)) + } + if cl[0].ID != cid { + t.Fatalf("Did not find expected container attached to the service: %v", cl[0]) + } + + _, errRsp = procUnpublishService(c, vars, nil) + if errRsp.isOK() { + t.Fatalf("Expected failure but succeeded") + } + if errRsp.StatusCode != http.StatusForbidden { + t.Fatalf("Expected %d. Got: %v", http.StatusForbidden, errRsp) + } + + vars[urlEpName] = "endpoint" + _, errRsp = procDetachBackend(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d. Got: %v", http.StatusNotFound, errRsp) + } + + vars[urlEpName] = "db" + _, errRsp = procDetachBackend(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } + + vars[urlCnID] = cid + _, errRsp = procDetachBackend(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure, got: %v", errRsp) + } + + cli, errRsp = procGetContainers(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure, got: %v", errRsp) + } + cl = i2cL(cli) + if len(cl) != 0 { + t.Fatalf("Did not find expected number of containers attached to the service: %d", len(cl)) + } + + err = ep1.Delete() + if err != nil { + t.Fatal(err) + } +} + +func TestDetectGetNetworksInvalidQueryComposition(t *testing.T) { + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + + vars := map[string]string{urlNwName: "x", urlNwPID: "y"} + _, errRsp := procGetNetworks(c, vars, nil) + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } +} + +func TestDetectGetEndpointsInvalidQueryComposition(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, _ := createTestNetwork(t, "network") + + vars := map[string]string{urlNwName: "network", urlEpName: "x", urlEpPID: "y"} + _, errRsp := procGetEndpoints(c, vars, nil) + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } +} + +func TestDetectGetServicesInvalidQueryComposition(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, _ := createTestNetwork(t, "network") + + vars := map[string]string{urlNwName: "network", urlEpName: "x", urlEpPID: "y"} + _, errRsp := procGetServices(c, vars, nil) + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d. Got: %v", http.StatusBadRequest, errRsp) + } +} + +func TestFindNetworkUtilPanic(t *testing.T) { + defer checkPanic(t) + findNetwork(nil, "", -1) +} + +func TestFindNetworkUtil(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, nw := createTestNetwork(t, "network") + nid := nw.ID() + + _, errRsp := findNetwork(c, "", byName) + if errRsp == &successResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d, but got: %d", http.StatusBadRequest, errRsp.StatusCode) + } + + n, errRsp := findNetwork(c, nid, byID) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure: %v", errRsp) + } + if n == nil { + t.Fatalf("Unexpected nil libnetwork.Network") + } + if nid != n.ID() { + t.Fatalf("Incorrect libnetwork.Network resource. It has different id: %v", n) + } + if "network" != n.Name() { + t.Fatalf("Incorrect libnetwork.Network resource. It has different name: %v", n) + } + + n, errRsp = findNetwork(c, "network", byName) + if errRsp != &successResponse { + t.Fatalf("Unexpected failure: %v", errRsp) + } + if n == nil { + t.Fatalf("Unexpected nil libnetwork.Network") + } + if nid != n.ID() { + t.Fatalf("Incorrect libnetwork.Network resource. It has different id: %v", n) + } + if "network" != n.Name() { + t.Fatalf("Incorrect libnetwork.Network resource. It has different name: %v", n) + } + + if err := n.Delete(); err != nil { + t.Fatalf("Failed to delete the network: %s", err.Error()) + } + + _, errRsp = findNetwork(c, nid, byID) + if errRsp == &successResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findNetwork(c, "network", byName) + if errRsp == &successResponse { + t.Fatalf("Expected to fail but succeeded") + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } +} + +func TestCreateDeleteEndpoints(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + nc := networkCreate{Name: "firstNet", NetworkType: bridgeNetType} + body, err := json.Marshal(nc) + if err != nil { + t.Fatal(err) + } + + vars := make(map[string]string) + i, errRsp := procCreateNetwork(c, vars, body) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + nid := i2s(i) + + vbad, err := json.Marshal("bad endppoint create data") + if err != nil { + t.Fatal(err) + } + + vars[urlNwName] = "firstNet" + _, errRsp = procCreateEndpoint(c, vars, vbad) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + + b, err := json.Marshal(endpointCreate{Name: ""}) + if err != nil { + t.Fatal(err) + } + + vars[urlNwName] = "secondNet" + _, errRsp = procCreateEndpoint(c, vars, b) + if errRsp == &createdResponse { + t.Fatalf("Expected to fail but succeeded") + } + + vars[urlNwName] = "firstNet" + _, errRsp = procCreateEndpoint(c, vars, b) + if errRsp == &successResponse { + t.Fatalf("Expected failure but succeeded: %v", errRsp) + } + + b, err = json.Marshal(endpointCreate{Name: "firstEp"}) + if err != nil { + t.Fatal(err) + } + + i, errRsp = procCreateEndpoint(c, vars, b) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + eid := i2s(i) + + _, errRsp = findEndpoint(c, "myNet", "firstEp", byName, byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure but succeeded: %v", errRsp) + } + + ep0, errRsp := findEndpoint(c, nid, "firstEp", byID, byName) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep1, errRsp := findEndpoint(c, "firstNet", "firstEp", byName, byName) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep2, errRsp := findEndpoint(c, nid, eid, byID, byID) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep3, errRsp := findEndpoint(c, "firstNet", eid, byName, byID) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + if ep0.ID() != ep1.ID() || ep0.ID() != ep2.ID() || ep0.ID() != ep3.ID() { + t.Fatalf("Diffenrent queries returned different endpoints: \nep0: %v\nep1: %v\nep2: %v\nep3: %v", ep0, ep1, ep2, ep3) + } + + vars = make(map[string]string) + vars[urlNwName] = "" + vars[urlEpName] = "ep1" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlNwName] = "firstNet" + vars[urlEpName] = "" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "ep2" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "firstEp" + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + _, errRsp = findEndpoint(c, "firstNet", "firstEp", byName, byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } +} + +func TestJoinLeave(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + err = c.ConfigureNetworkDriver(bridgeNetType, nil) + if err != nil { + t.Fatal(err) + } + + nb, err := json.Marshal(networkCreate{Name: "network", NetworkType: bridgeNetType}) + if err != nil { + t.Fatal(err) + } + vars := make(map[string]string) + _, errRsp := procCreateNetwork(c, vars, nb) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + eb, err := json.Marshal(endpointCreate{Name: "endpoint"}) + if err != nil { + t.Fatal(err) + } + vars[urlNwName] = "network" + _, errRsp = procCreateEndpoint(c, vars, eb) + if errRsp != &createdResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + vbad, err := json.Marshal("bad data") + if err != nil { + t.Fatal(err) + } + _, errRsp = procJoinEndpoint(c, vars, vbad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "endpoint" + bad, err := json.Marshal(endpointJoin{}) + if err != nil { + t.Fatal(err) + } + _, errRsp = procJoinEndpoint(c, vars, bad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + cid := "abcdefghi" + jl := endpointJoin{ContainerID: cid} + jlb, err := json.Marshal(jl) + if err != nil { + t.Fatal(err) + } + + vars = make(map[string]string) + vars[urlNwName] = "" + vars[urlEpName] = "" + _, errRsp = procJoinEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlNwName] = "network" + vars[urlEpName] = "" + _, errRsp = procJoinEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "epoint" + _, errRsp = procJoinEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlEpName] = "endpoint" + key, errRsp := procJoinEndpoint(c, vars, jlb) + if errRsp != &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + keyStr := i2s(key) + if keyStr == "" { + t.Fatalf("Empty sandbox key") + } + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlNwName] = "network2" + _, errRsp = procLeaveEndpoint(c, vars, vbad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + _, errRsp = procLeaveEndpoint(c, vars, bad) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + vars = make(map[string]string) + vars[urlNwName] = "" + vars[urlEpName] = "" + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + vars[urlNwName] = "network" + vars[urlEpName] = "" + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + vars[urlEpName] = "2epoint" + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + vars[urlEpName] = "epoint" + vars[urlCnID] = "who" + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + delete(vars, urlCnID) + vars[urlEpName] = "endpoint" + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + vars[urlCnID] = cid + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + _, errRsp = procLeaveEndpoint(c, vars, jlb) + if errRsp == &successResponse { + t.Fatalf("Expected failure, got: %v", errRsp) + } + + _, errRsp = procDeleteEndpoint(c, vars, nil) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } +} + +func TestFindEndpointUtilPanic(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + defer checkPanic(t) + c, nw := createTestNetwork(t, "network") + nid := nw.ID() + findEndpoint(c, nid, "", byID, -1) +} + +func TestFindServiceUtilPanic(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + defer checkPanic(t) + c, _ := createTestNetwork(t, "network") + findService(c, "random_service", -1) +} + +func TestFindEndpointUtil(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, nw := createTestNetwork(t, "network") + nid := nw.ID() + + ep, err := nw.CreateEndpoint("secondEp", nil) + if err != nil { + t.Fatal(err) + } + eid := ep.ID() + + _, errRsp := findEndpoint(c, nid, "", byID, byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusBadRequest { + t.Fatalf("Expected %d, but got: %d", http.StatusBadRequest, errRsp.StatusCode) + } + + ep0, errRsp := findEndpoint(c, nid, "secondEp", byID, byName) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep1, errRsp := findEndpoint(c, "network", "secondEp", byName, byName) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep2, errRsp := findEndpoint(c, nid, eid, byID, byID) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep3, errRsp := findEndpoint(c, "network", eid, byName, byID) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep4, errRsp := findService(c, "secondEp", byName) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + ep5, errRsp := findService(c, eid, byID) + if errRsp != &successResponse { + t.Fatalf("Unexepected failure: %v", errRsp) + } + + if ep0 != ep1 || ep0 != ep2 || ep0 != ep3 || ep0 != ep4 || ep0 != ep5 { + t.Fatalf("Diffenrent queries returned different endpoints") + } + + ep.Delete() + + _, errRsp = findEndpoint(c, nid, "secondEp", byID, byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findEndpoint(c, "network", "secondEp", byName, byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findEndpoint(c, nid, eid, byID, byID) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findEndpoint(c, "network", eid, byName, byID) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findService(c, "secondEp", byName) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } + + _, errRsp = findService(c, eid, byID) + if errRsp == &successResponse { + t.Fatalf("Expected failure, but got: %v", errRsp) + } + if errRsp.StatusCode != http.StatusNotFound { + t.Fatalf("Expected %d, but got: %d", http.StatusNotFound, errRsp.StatusCode) + } +} + +func TestEndpointToService(t *testing.T) { + r := &responseStatus{Status: "this is one endpoint", StatusCode: http.StatusOK} + r = endpointToService(r) + if r.Status != "this is one service" { + t.Fatalf("endpointToService returned unexpected status string: %s", r.Status) + } + + r = &responseStatus{Status: "this is one network", StatusCode: http.StatusOK} + r = endpointToService(r) + if r.Status != "this is one network" { + t.Fatalf("endpointToService returned unexpected status string: %s", r.Status) + } +} + +func checkPanic(t *testing.T) { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + } else { + t.Fatalf("Expected to panic, but suceeded") + } +} + +func TestDetectNetworkTargetPanic(t *testing.T) { + defer checkPanic(t) + vars := make(map[string]string) + detectNetworkTarget(vars) +} + +func TestDetectEndpointTargetPanic(t *testing.T) { + defer checkPanic(t) + vars := make(map[string]string) + detectEndpointTarget(vars) +} + +func TestResponseStatus(t *testing.T) { + list := []int{ + http.StatusBadGateway, + http.StatusBadRequest, + http.StatusConflict, + http.StatusContinue, + http.StatusExpectationFailed, + http.StatusForbidden, + http.StatusFound, + http.StatusGatewayTimeout, + http.StatusGone, + http.StatusHTTPVersionNotSupported, + http.StatusInternalServerError, + http.StatusLengthRequired, + http.StatusMethodNotAllowed, + http.StatusMovedPermanently, + http.StatusMultipleChoices, + http.StatusNoContent, + http.StatusNonAuthoritativeInfo, + http.StatusNotAcceptable, + http.StatusNotFound, + http.StatusNotModified, + http.StatusPartialContent, + http.StatusPaymentRequired, + http.StatusPreconditionFailed, + http.StatusProxyAuthRequired, + http.StatusRequestEntityTooLarge, + http.StatusRequestTimeout, + http.StatusRequestURITooLong, + http.StatusRequestedRangeNotSatisfiable, + http.StatusResetContent, + http.StatusServiceUnavailable, + http.StatusSwitchingProtocols, + http.StatusTemporaryRedirect, + http.StatusUnauthorized, + http.StatusUnsupportedMediaType, + http.StatusUseProxy, + } + for _, c := range list { + r := responseStatus{StatusCode: c} + if r.isOK() { + t.Fatalf("isOK() returned true for code% d", c) + } + } + + r := responseStatus{StatusCode: http.StatusOK} + if !r.isOK() { + t.Fatalf("isOK() failed") + } + + r = responseStatus{StatusCode: http.StatusCreated} + if !r.isOK() { + t.Fatalf("isOK() failed") + } +} + +// Local structs for end to end testing of api.go +type localReader struct { + data []byte + beBad bool +} + +func newLocalReader(data []byte) *localReader { + lr := &localReader{data: make([]byte, len(data))} + copy(lr.data, data) + return lr +} + +func (l *localReader) Read(p []byte) (n int, err error) { + if l.beBad { + return 0, errors.New("I am a bad reader") + } + if p == nil { + return -1, fmt.Errorf("nil buffer passed") + } + if l.data == nil || len(l.data) == 0 { + return 0, io.EOF + } + copy(p[:], l.data[:]) + return len(l.data), io.EOF +} + +type localResponseWriter struct { + body []byte + statusCode int +} + +func newWriter() *localResponseWriter { + return &localResponseWriter{} +} + +func (f *localResponseWriter) Header() http.Header { + return make(map[string][]string, 0) +} + +func (f *localResponseWriter) Write(data []byte) (int, error) { + if data == nil { + return -1, fmt.Errorf("nil data passed") + } + + f.body = make([]byte, len(data)) + copy(f.body, data) + + return len(f.body), nil +} + +func (f *localResponseWriter) WriteHeader(c int) { + f.statusCode = c +} + +func TestwriteJSON(t *testing.T) { + testCode := 55 + testData, err := json.Marshal("test data") + if err != nil { + t.Fatal(err) + } + + rsp := newWriter() + writeJSON(rsp, testCode, testData) + if rsp.statusCode != testCode { + t.Fatalf("writeJSON() failed to set the status code. Expected %d. Got %d", testCode, rsp.statusCode) + } + if !bytes.Equal(testData, rsp.body) { + t.Fatalf("writeJSON() failed to set the body. Expected %s. Got %s", testData, rsp.body) + } + +} + +func TestHttpHandlerUninit(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + + h := &httpHandler{c: c} + h.initRouter() + if h.r == nil { + t.Fatalf("initRouter() did not initialize the router") + } + + rsp := newWriter() + req, err := http.NewRequest("GET", "/v1.19/networks", nil) + if err != nil { + t.Fatal(err) + } + + handleRequest := NewHTTPHandler(nil) + handleRequest(rsp, req) + if rsp.statusCode != http.StatusServiceUnavailable { + t.Fatalf("Expected (%d). Got (%d): %s", http.StatusServiceUnavailable, rsp.statusCode, rsp.body) + } + + handleRequest = NewHTTPHandler(c) + + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Expected (%d). Got: (%d): %s", http.StatusOK, rsp.statusCode, rsp.body) + } + + var list []*networkResource + err = json.Unmarshal(rsp.body, &list) + if err != nil { + t.Fatal(err) + } + if len(list) != 0 { + t.Fatalf("Expected empty list. Got %v", list) + } + + n, err := c.NewNetwork(bridgeNetType, "didietro", nil) + if err != nil { + t.Fatal(err) + } + nwr := buildNetworkResource(n) + expected, err := json.Marshal([]*networkResource{nwr}) + if err != nil { + t.Fatal(err) + } + + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + if len(rsp.body) == 0 { + t.Fatalf("Empty list of networks") + } + if bytes.Equal(rsp.body, expected) { + t.Fatalf("Incorrect list of networks in response's body") + } +} + +func TestHttpHandlerBadBody(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + rsp := newWriter() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + handleRequest := NewHTTPHandler(c) + + req, err := http.NewRequest("POST", "/v1.19/networks", &localReader{beBad: true}) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusBadRequest { + t.Fatalf("Unexpected status code. Expected (%d). Got (%d): %s.", http.StatusBadRequest, rsp.statusCode, string(rsp.body)) + } + + body := []byte{} + lr := newLocalReader(body) + req, err = http.NewRequest("POST", "/v1.19/networks", lr) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusBadRequest { + t.Fatalf("Unexpected status code. Expected (%d). Got (%d): %s.", http.StatusBadRequest, rsp.statusCode, string(rsp.body)) + } +} + +func TestEndToEnd(t *testing.T) { + defer sandbox.SetupTestOSContext(t)() + + rsp := newWriter() + + c, err := libnetwork.New() + if err != nil { + t.Fatal(err) + } + handleRequest := NewHTTPHandler(c) + + ops := options.Generic{ + netlabel.EnableIPv6: true, + netlabel.GenericData: map[string]string{ + "BridgeName": "cdef", + "FixedCIDRv6": "fe80:2000::1/64", + "EnableIPv6": "true", + "Mtu": "1460", + "EnableIPTables": "true", + "AddressIP": "172.28.30.254/16", + "EnableUserlandProxy": "true", + "AllowNonDefaultBridge": "true", + }, + } + + // Create network + nc := networkCreate{Name: "network-fiftyfive", NetworkType: bridgeNetType, Options: ops} + body, err := json.Marshal(nc) + if err != nil { + t.Fatal(err) + } + lr := newLocalReader(body) + req, err := http.NewRequest("POST", "/v1.19/networks", lr) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusCreated { + t.Fatalf("Unexpectded status code. Expected (%d). Got (%d): %s.", http.StatusCreated, rsp.statusCode, string(rsp.body)) + } + if len(rsp.body) == 0 { + t.Fatalf("Empty response body") + } + + var nid string + err = json.Unmarshal(rsp.body, &nid) + if err != nil { + t.Fatal(err) + } + + // Query networks collection + req, err = http.NewRequest("GET", "/v1.19/networks", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Expected StatusOK. Got (%d): %s", rsp.statusCode, rsp.body) + } + + b0 := make([]byte, len(rsp.body)) + copy(b0, rsp.body) + + req, err = http.NewRequest("GET", "/v1.19/networks?name=network-fiftyfive", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Expected StatusOK. Got (%d): %s", rsp.statusCode, rsp.body) + } + + if !bytes.Equal(b0, rsp.body) { + t.Fatalf("Expected same body from GET /networks and GET /networks?name= when only network exist.") + } + + // Query network by name + req, err = http.NewRequest("GET", "/v1.19/networks?name=culo", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Expected StatusOK. Got (%d): %s", rsp.statusCode, rsp.body) + } + + var list []*networkResource + err = json.Unmarshal(rsp.body, &list) + if err != nil { + t.Fatal(err) + } + if len(list) != 0 { + t.Fatalf("Expected empty list. Got %v", list) + } + + req, err = http.NewRequest("GET", "/v1.19/networks?name=network-fiftyfive", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + err = json.Unmarshal(rsp.body, &list) + if err != nil { + t.Fatal(err) + } + if len(list) == 0 { + t.Fatalf("Expected non empty list") + } + if list[0].Name != "network-fiftyfive" || nid != list[0].ID { + t.Fatalf("Incongruent resource found: %v", list[0]) + } + + // Query network by partial id + chars := []byte(nid) + partial := string(chars[0 : len(chars)/2]) + req, err = http.NewRequest("GET", "/v1.19/networks?partial-id="+partial, nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + err = json.Unmarshal(rsp.body, &list) + if err != nil { + t.Fatal(err) + } + if len(list) == 0 { + t.Fatalf("Expected non empty list") + } + if list[0].Name != "network-fiftyfive" || nid != list[0].ID { + t.Fatalf("Incongruent resource found: %v", list[0]) + } + + // Get network by id + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid, nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + var nwr networkResource + err = json.Unmarshal(rsp.body, &nwr) + if err != nil { + t.Fatal(err) + } + if nwr.Name != "network-fiftyfive" || nid != nwr.ID { + t.Fatalf("Incongruent resource found: %v", nwr) + } + + // Create endpoint + eb, err := json.Marshal(endpointCreate{Name: "ep-TwentyTwo"}) + if err != nil { + t.Fatal(err) + } + + lr = newLocalReader(eb) + req, err = http.NewRequest("POST", "/v1.19/networks/"+nid+"/endpoints", lr) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusCreated { + t.Fatalf("Unexpectded status code. Expected (%d). Got (%d): %s.", http.StatusCreated, rsp.statusCode, string(rsp.body)) + } + if len(rsp.body) == 0 { + t.Fatalf("Empty response body") + } + + var eid string + err = json.Unmarshal(rsp.body, &eid) + if err != nil { + t.Fatal(err) + } + + // Query endpoint(s) + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid+"/endpoints", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Expected StatusOK. Got (%d): %s", rsp.statusCode, rsp.body) + } + + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid+"/endpoints?name=bla", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + var epList []*endpointResource + err = json.Unmarshal(rsp.body, &epList) + if err != nil { + t.Fatal(err) + } + if len(epList) != 0 { + t.Fatalf("Expected empty list. Got %v", epList) + } + + // Query endpoint by name + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid+"/endpoints?name=ep-TwentyTwo", nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + err = json.Unmarshal(rsp.body, &epList) + if err != nil { + t.Fatal(err) + } + if len(epList) == 0 { + t.Fatalf("Empty response body") + } + if epList[0].Name != "ep-TwentyTwo" || eid != epList[0].ID { + t.Fatalf("Incongruent resource found: %v", epList[0]) + } + + // Query endpoint by partial id + chars = []byte(eid) + partial = string(chars[0 : len(chars)/2]) + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid+"/endpoints?partial-id="+partial, nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + err = json.Unmarshal(rsp.body, &epList) + if err != nil { + t.Fatal(err) + } + if len(epList) == 0 { + t.Fatalf("Empty response body") + } + if epList[0].Name != "ep-TwentyTwo" || eid != epList[0].ID { + t.Fatalf("Incongruent resource found: %v", epList[0]) + } + + // Get endpoint by id + req, err = http.NewRequest("GET", "/v1.19/networks/"+nid+"/endpoints/"+eid, nil) + if err != nil { + t.Fatal(err) + } + handleRequest(rsp, req) + if rsp.statusCode != http.StatusOK { + t.Fatalf("Unexpectded failure: (%d): %s", rsp.statusCode, rsp.body) + } + + var epr endpointResource + err = json.Unmarshal(rsp.body, &epr) + if err != nil { + t.Fatal(err) + } + if epr.Name != "ep-TwentyTwo" || epr.ID != eid { + t.Fatalf("Incongruent resource found: %v", epr) + } +} + +type bre struct{} + +func (b *bre) Error() string { + return "I am a bad request error" +} +func (b *bre) BadRequest() {} + +type nfe struct{} + +func (n *nfe) Error() string { + return "I am a not found error" +} +func (n *nfe) NotFound() {} + +type forb struct{} + +func (f *forb) Error() string { + return "I am a bad request error" +} +func (f *forb) Forbidden() {} + +type notimpl struct{} + +func (nip *notimpl) Error() string { + return "I am a not implemented error" +} +func (nip *notimpl) NotImplemented() {} + +type inter struct{} + +func (it *inter) Error() string { + return "I am a internal error" +} +func (it *inter) Internal() {} + +type tout struct{} + +func (to *tout) Error() string { + return "I am a timeout error" +} +func (to *tout) Timeout() {} + +type noserv struct{} + +func (nos *noserv) Error() string { + return "I am a no service error" +} +func (nos *noserv) NoService() {} + +type notclassified struct{} + +func (noc *notclassified) Error() string { + return "I am a non classified error" +} + +func TestErrorConversion(t *testing.T) { + if convertNetworkError(new(bre)).StatusCode != http.StatusBadRequest { + t.Fatalf("Failed to recognize BadRequest error") + } + + if convertNetworkError(new(nfe)).StatusCode != http.StatusNotFound { + t.Fatalf("Failed to recognize NotFound error") + } + + if convertNetworkError(new(forb)).StatusCode != http.StatusForbidden { + t.Fatalf("Failed to recognize Forbidden error") + } + + if convertNetworkError(new(notimpl)).StatusCode != http.StatusNotImplemented { + t.Fatalf("Failed to recognize NotImplemented error") + } + + if convertNetworkError(new(inter)).StatusCode != http.StatusInternalServerError { + t.Fatalf("Failed to recognize Internal error") + } + + if convertNetworkError(new(tout)).StatusCode != http.StatusRequestTimeout { + t.Fatalf("Failed to recognize Timeout error") + } + + if convertNetworkError(new(noserv)).StatusCode != http.StatusServiceUnavailable { + t.Fatalf("Failed to recognize No Service error") + } + + if convertNetworkError(new(notclassified)).StatusCode != http.StatusInternalServerError { + t.Fatalf("Failed to recognize not classified error as Internal error") + } +} diff --git a/api/types.go b/api/types.go new file mode 100644 index 00000000..72f20db2 --- /dev/null +++ b/api/types.go @@ -0,0 +1,81 @@ +package api + +import "github.com/docker/libnetwork/types" + +/*********** + Resources +************/ + +// networkResource is the body of the "get network" http response message +type networkResource struct { + Name string `json:"name"` + ID string `json:"id"` + Type string `json:"type"` + Endpoints []*endpointResource `json:"endpoints"` +} + +// endpointResource is the body of the "get endpoint" http response message +type endpointResource struct { + Name string `json:"name"` + ID string `json:"id"` + Network string `json:"network"` +} + +// containerResource is the body of "get service backend" response message +type containerResource struct { + ID string `json:"id"` + // will add more fields once labels change is in +} + +/*********** + Body types + ************/ + +// networkCreate is the expected body of the "create network" http request message +type networkCreate struct { + Name string `json:"name"` + NetworkType string `json:"network_type"` + Options map[string]interface{} `json:"options"` +} + +// endpointCreate represents the body of the "create endpoint" http request message +type endpointCreate struct { + Name string `json:"name"` + ExposedPorts []types.TransportPort `json:"exposed_ports"` + PortMapping []types.PortBinding `json:"port_mapping"` +} + +// endpointJoin represents the expected body of the "join endpoint" or "leave endpoint" http request messages +type endpointJoin struct { + ContainerID string `json:"container_id"` + HostName string `json:"host_name"` + DomainName string `json:"domain_name"` + HostsPath string `json:"hosts_path"` + ResolvConfPath string `json:"resolv_conf_path"` + DNS []string `json:"dns"` + ExtraHosts []endpointExtraHost `json:"extra_hosts"` + ParentUpdates []endpointParentUpdate `json:"parent_updates"` + UseDefaultSandbox bool `json:"use_default_sandbox"` +} + +// servicePublish represents the body of the "publish service" http request message +type servicePublish struct { + Name string `json:"name"` + Network string `json:"network_name"` + ExposedPorts []types.TransportPort `json:"exposed_ports"` + PortMapping []types.PortBinding `json:"port_mapping"` +} + +// EndpointExtraHost represents the extra host object +type endpointExtraHost struct { + Name string `json:"name"` + Address string `json:"address"` +} + +// EndpointParentUpdate is the object carrying the information about the +// endpoint parent that needs to be updated +type endpointParentUpdate struct { + EndpointID string `json:"endpoint_id"` + Name string `json:"name"` + Address string `json:"address"` +} diff --git a/bitseq/sequence.go b/bitseq/sequence.go new file mode 100644 index 00000000..d9c9ad52 --- /dev/null +++ b/bitseq/sequence.go @@ -0,0 +1,454 @@ +// Package bitseq provides a structure and utilities for representing long bitmask +// as sequence of run-lenght encoded blocks. It operates direclty on the encoded +// representation, it does not decode/encode. +package bitseq + +import ( + "fmt" + "sync" + + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/netutils" +) + +// Block Sequence constants +// If needed we can think of making these configurable +const ( + blockLen = 32 + blockBytes = blockLen / 8 + blockMAX = 1<%s", s.Block, s.Count, nextBlock) +} + +// GetAvailableBit returns the position of the first unset bit in the bitmask represented by this sequence +func (s *Sequence) GetAvailableBit() (bytePos, bitPos int) { + if s.Block == blockMAX || s.Count == 0 { + return -1, -1 + } + bits := 0 + bitSel := uint32(blockFirstBit) + for bitSel > 0 && s.Block&bitSel != 0 { + bitSel >>= 1 + bits++ + } + return bits / 8, bits % 8 +} + +// GetCopy returns a copy of the linked list rooted at this node +func (s *Sequence) GetCopy() *Sequence { + n := &Sequence{Block: s.Block, Count: s.Count} + pn := n + ps := s.Next + for ps != nil { + pn.Next = &Sequence{Block: ps.Block, Count: ps.Count} + pn = pn.Next + ps = ps.Next + } + return n +} + +// Equal checks if this sequence is equal to the passed one +func (s *Sequence) Equal(o *Sequence) bool { + this := s + other := o + for this != nil { + if other == nil { + return false + } + if this.Block != other.Block || this.Count != other.Count { + return false + } + this = this.Next + other = other.Next + } + // Check if other is longer than this + if other != nil { + return false + } + return true +} + +// ToByteArray converts the sequence into a byte array +// TODO (aboch): manage network/host order stuff +func (s *Sequence) ToByteArray() ([]byte, error) { + var bb []byte + + p := s + for p != nil { + bb = append(bb, netutils.U32ToA(p.Block)...) + bb = append(bb, netutils.U32ToA(p.Count)...) + p = p.Next + } + + return bb, nil +} + +// FromByteArray construct the sequence from the byte array +// TODO (aboch): manage network/host order stuff +func (s *Sequence) FromByteArray(data []byte) error { + l := len(data) + if l%8 != 0 { + return fmt.Errorf("cannot deserialize byte sequence of lenght %d (%v)", l, data) + } + + p := s + i := 0 + for { + p.Block = netutils.ATo32(data[i : i+4]) + p.Count = netutils.ATo32(data[i+4 : i+8]) + i += 8 + if i == l { + break + } + p.Next = &Sequence{} + p = p.Next + } + + return nil +} + +// GetFirstAvailable returns the byte and bit position of the first unset bit +func (h *Handle) GetFirstAvailable() (int, int, error) { + h.Lock() + defer h.Unlock() + return GetFirstAvailable(h.head) +} + +// CheckIfAvailable checks if the bit correspondent to the specified ordinal is unset +// If the ordinal is beyond the Sequence limits, a negative response is returned +func (h *Handle) CheckIfAvailable(ordinal int) (int, int, error) { + h.Lock() + defer h.Unlock() + return CheckIfAvailable(h.head, ordinal) +} + +// PushReservation pushes the bit reservation inside the bitmask. +func (h *Handle) PushReservation(bytePos, bitPos int, release bool) error { + // Create a copy of the current handler + h.Lock() + nh := &Handle{ + app: h.app, + id: h.id, + store: h.store, + dbIndex: h.dbIndex, + head: h.head.GetCopy(), + dbExists: h.dbExists, + } + h.Unlock() + + nh.head = PushReservation(bytePos, bitPos, nh.head, release) + + err := nh.writeToStore() + if err == nil { + // Commit went through, save locally + h.Lock() + h.head = nh.head + if release { + h.unselected++ + } else { + h.unselected-- + } + // Can't use SetIndex() since we're locked. + h.dbIndex = nh.Index() + h.dbExists = true + h.Unlock() + } + + return err +} + +// Destroy removes from the datastore the data belonging to this handle +func (h *Handle) Destroy() { + h.deleteFromStore() +} + +// ToByteArray converts this handle's data into a byte array +func (h *Handle) ToByteArray() ([]byte, error) { + ba := make([]byte, 8) + + h.Lock() + defer h.Unlock() + copy(ba[0:4], netutils.U32ToA(h.bits)) + copy(ba[4:8], netutils.U32ToA(h.unselected)) + bm, err := h.head.ToByteArray() + if err != nil { + return nil, fmt.Errorf("failed to serialize head: %s", err.Error()) + } + ba = append(ba, bm...) + + return ba, nil +} + +// FromByteArray reads his handle's data from a byte array +func (h *Handle) FromByteArray(ba []byte) error { + if ba == nil { + return fmt.Errorf("nil byte array") + } + + nh := &Sequence{} + err := nh.FromByteArray(ba[8:]) + if err != nil { + return fmt.Errorf("failed to deserialize head: %s", err.Error()) + } + + h.Lock() + h.head = nh + h.bits = netutils.ATo32(ba[0:4]) + h.unselected = netutils.ATo32(ba[4:8]) + h.Unlock() + + return nil +} + +// Bits returns the length of the bit sequence +func (h *Handle) Bits() uint32 { + return h.bits +} + +// Unselected returns the number of bits which are not selected +func (h *Handle) Unselected() uint32 { + h.Lock() + defer h.Unlock() + return h.unselected +} + +// GetFirstAvailable looks for the first unset bit in passed mask +func GetFirstAvailable(head *Sequence) (int, int, error) { + byteIndex := 0 + current := head + for current != nil { + if current.Block != blockMAX { + bytePos, bitPos := current.GetAvailableBit() + return byteIndex + bytePos, bitPos, nil + } + byteIndex += int(current.Count * blockBytes) + current = current.Next + } + return -1, -1, fmt.Errorf("no bit available") +} + +// CheckIfAvailable checks if the bit correspondent to the specified ordinal is unset +// If the ordinal is beyond the Sequence limits, a negative response is returned +func CheckIfAvailable(head *Sequence, ordinal int) (int, int, error) { + bytePos := ordinal / 8 + bitPos := ordinal % 8 + + // Find the Sequence containing this byte + current, _, _, inBlockBytePos := findSequence(head, bytePos) + + if current != nil { + // Check whether the bit corresponding to the ordinal address is unset + bitSel := uint32(blockFirstBit >> uint(inBlockBytePos*8+bitPos)) + if current.Block&bitSel == 0 { + return bytePos, bitPos, nil + } + } + + return -1, -1, fmt.Errorf("requested bit is not available") +} + +// Given the byte position and the sequences list head, return the pointer to the +// sequence containing the byte (current), the pointer to the previous sequence, +// the number of blocks preceding the block containing the byte inside the current sequence. +// If bytePos is outside of the list, function will return (nil, nil, 0, -1) +func findSequence(head *Sequence, bytePos int) (*Sequence, *Sequence, uint32, int) { + // Find the Sequence containing this byte + previous := head + current := head + n := bytePos + for current.Next != nil && n >= int(current.Count*blockBytes) { // Nil check for less than 32 addresses masks + n -= int(current.Count * blockBytes) + previous = current + current = current.Next + } + + // If byte is outside of the list, let caller know + if n >= int(current.Count*blockBytes) { + return nil, nil, 0, -1 + } + + // Find the byte position inside the block and the number of blocks + // preceding the block containing the byte inside this sequence + precBlocks := uint32(n / blockBytes) + inBlockBytePos := bytePos % blockBytes + + return current, previous, precBlocks, inBlockBytePos +} + +// PushReservation pushes the bit reservation inside the bitmask. +// Given byte and bit positions, identify the sequence (current) which holds the block containing the affected bit. +// Create a new block with the modified bit according to the operation (allocate/release). +// Create a new Sequence containing the new Block and insert it in the proper position. +// Remove current sequence if empty. +// Check if new Sequence can be merged with neighbour (previous/Next) sequences. +// +// +// Identify "current" Sequence containing block: +// [prev seq] [current seq] [Next seq] +// +// Based on block position, resulting list of sequences can be any of three forms: +// +// Block position Resulting list of sequences +// A) Block is first in current: [prev seq] [new] [modified current seq] [Next seq] +// B) Block is last in current: [prev seq] [modified current seq] [new] [Next seq] +// C) Block is in the middle of current: [prev seq] [curr pre] [new] [curr post] [Next seq] +func PushReservation(bytePos, bitPos int, head *Sequence, release bool) *Sequence { + // Store list's head + newHead := head + + // Find the Sequence containing this byte + current, previous, precBlocks, inBlockBytePos := findSequence(head, bytePos) + if current == nil { + return newHead + } + + // Construct updated block + bitSel := uint32(blockFirstBit >> uint(inBlockBytePos*8+bitPos)) + newBlock := current.Block + if release { + newBlock &^= bitSel + } else { + newBlock |= bitSel + } + + // Quit if it was a redundant request + if current.Block == newBlock { + return newHead + } + + // Current Sequence inevitably looses one block, upadate Count + current.Count-- + + // Create new sequence + newSequence := &Sequence{Block: newBlock, Count: 1} + + // Insert the new sequence in the list based on block position + if precBlocks == 0 { // First in sequence (A) + newSequence.Next = current + if current == head { + newHead = newSequence + previous = newHead + } else { + previous.Next = newSequence + } + removeCurrentIfEmpty(&newHead, newSequence, current) + mergeSequences(previous) + } else if precBlocks == current.Count-2 { // Last in sequence (B) + newSequence.Next = current.Next + current.Next = newSequence + mergeSequences(current) + } else { // In between the sequence (C) + currPre := &Sequence{Block: current.Block, Count: precBlocks, Next: newSequence} + currPost := current + currPost.Count -= precBlocks + newSequence.Next = currPost + if currPost == head { + newHead = currPre + } else { + previous.Next = currPre + } + // No merging or empty current possible here + } + + return newHead +} + +// Removes the current sequence from the list if empty, adjusting the head pointer if needed +func removeCurrentIfEmpty(head **Sequence, previous, current *Sequence) { + if current.Count == 0 { + if current == *head { + *head = current.Next + } else { + previous.Next = current.Next + current = current.Next + } + } +} + +// Given a pointer to a Sequence, it checks if it can be merged with any following sequences +// It stops when no more merging is possible. +// TODO: Optimization: only attempt merge from start to end sequence, no need to scan till the end of the list +func mergeSequences(seq *Sequence) { + if seq != nil { + // Merge all what possible from seq + for seq.Next != nil && seq.Block == seq.Next.Block { + seq.Count += seq.Next.Count + seq.Next = seq.Next.Next + } + // Move to Next + mergeSequences(seq.Next) + } +} + +func getNumBlocks(numBits uint32) uint32 { + numBlocks := numBits / blockLen + if numBits%blockLen != 0 { + numBlocks++ + } + return numBlocks +} diff --git a/bitseq/sequence_test.go b/bitseq/sequence_test.go new file mode 100644 index 00000000..54e505f7 --- /dev/null +++ b/bitseq/sequence_test.go @@ -0,0 +1,427 @@ +package bitseq + +import ( + "testing" +) + +func TestSequenceGetAvailableBit(t *testing.T) { + input := []struct { + head *Sequence + bytePos int + bitPos int + }{ + {&Sequence{Block: 0x0, Count: 0}, -1, -1}, + {&Sequence{Block: 0x0, Count: 1}, 0, 0}, + {&Sequence{Block: 0x0, Count: 100}, 0, 0}, + + {&Sequence{Block: 0x80000000, Count: 0}, -1, -1}, + {&Sequence{Block: 0x80000000, Count: 1}, 0, 1}, + {&Sequence{Block: 0x80000000, Count: 100}, 0, 1}, + + {&Sequence{Block: 0xFF000000, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFF000000, Count: 1}, 1, 0}, + {&Sequence{Block: 0xFF000000, Count: 100}, 1, 0}, + + {&Sequence{Block: 0xFF800000, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFF800000, Count: 1}, 1, 1}, + {&Sequence{Block: 0xFF800000, Count: 100}, 1, 1}, + + {&Sequence{Block: 0xFFC0FF00, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFC0FF00, Count: 1}, 1, 2}, + {&Sequence{Block: 0xFFC0FF00, Count: 100}, 1, 2}, + + {&Sequence{Block: 0xFFE0FF00, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFE0FF00, Count: 1}, 1, 3}, + {&Sequence{Block: 0xFFE0FF00, Count: 100}, 1, 3}, + + {&Sequence{Block: 0xFFFEFF00, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFFEFF00, Count: 1}, 1, 7}, + {&Sequence{Block: 0xFFFEFF00, Count: 100}, 1, 7}, + + {&Sequence{Block: 0xFFFFC0FF, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFFFC0FF, Count: 1}, 2, 2}, + {&Sequence{Block: 0xFFFFC0FF, Count: 100}, 2, 2}, + + {&Sequence{Block: 0xFFFFFF00, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFFFFF00, Count: 1}, 3, 0}, + {&Sequence{Block: 0xFFFFFF00, Count: 100}, 3, 0}, + + {&Sequence{Block: 0xFFFFFFFE, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFFFFFFE, Count: 1}, 3, 7}, + {&Sequence{Block: 0xFFFFFFFE, Count: 100}, 3, 7}, + + {&Sequence{Block: 0xFFFFFFFF, Count: 0}, -1, -1}, + {&Sequence{Block: 0xFFFFFFFF, Count: 1}, -1, -1}, + {&Sequence{Block: 0xFFFFFFFF, Count: 100}, -1, -1}, + } + + for n, i := range input { + b, bb := i.head.GetAvailableBit() + if b != i.bytePos || bb != i.bitPos { + t.Fatalf("Error in Sequence.getAvailableBit() (%d).\nExp: (%d, %d)\nGot: (%d, %d),", n, i.bytePos, i.bitPos, b, bb) + } + } +} + +func TestSequenceEqual(t *testing.T) { + input := []struct { + first *Sequence + second *Sequence + areEqual bool + }{ + {&Sequence{Block: 0x0, Count: 8, Next: nil}, &Sequence{Block: 0x0, Count: 8}, true}, + {&Sequence{Block: 0x0, Count: 0, Next: nil}, &Sequence{Block: 0x0, Count: 0}, true}, + {&Sequence{Block: 0x0, Count: 2, Next: nil}, &Sequence{Block: 0x0, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}, false}, + {&Sequence{Block: 0x0, Count: 2, Next: &Sequence{Block: 0x1, Count: 1}}, &Sequence{Block: 0x0, Count: 2}, false}, + + {&Sequence{Block: 0x12345678, Count: 8, Next: nil}, &Sequence{Block: 0x12345678, Count: 8}, true}, + {&Sequence{Block: 0x12345678, Count: 8, Next: nil}, &Sequence{Block: 0x12345678, Count: 9}, false}, + {&Sequence{Block: 0x12345678, Count: 1, Next: &Sequence{Block: 0XFFFFFFFF, Count: 1}}, &Sequence{Block: 0x12345678, Count: 1}, false}, + {&Sequence{Block: 0x12345678, Count: 1}, &Sequence{Block: 0x12345678, Count: 1, Next: &Sequence{Block: 0XFFFFFFFF, Count: 1}}, false}, + } + + for n, i := range input { + if i.areEqual != i.first.Equal(i.second) { + t.Fatalf("Error in Sequence.Equal() (%d).\nExp: %t\nGot: %t,", n, i.areEqual, !i.areEqual) + } + } +} + +func TestSequenceCopy(t *testing.T) { + s := &Sequence{ + Block: 0x0, + Count: 8, + Next: &Sequence{ + Block: 0x0, + Count: 8, + Next: &Sequence{ + Block: 0x0, + Count: 0, + Next: &Sequence{ + Block: 0x0, + Count: 0, + Next: &Sequence{ + Block: 0x0, + Count: 2, + Next: &Sequence{ + Block: 0x0, + Count: 1, + Next: &Sequence{ + Block: 0x0, + Count: 1, + Next: &Sequence{ + Block: 0x0, + Count: 2, + Next: &Sequence{ + Block: 0x1, + Count: 1, + Next: &Sequence{ + Block: 0x0, + Count: 2, + Next: nil, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + n := s.GetCopy() + if !s.Equal(n) { + t.Fatalf("copy of s failed") + } + if n == s { + t.Fatalf("not true copy of s") + } +} + +func TestGetFirstAvailable(t *testing.T) { + input := []struct { + mask *Sequence + bytePos int + bitPos int + }{ + {&Sequence{Block: 0xffffffff, Count: 2048}, -1, -1}, + {&Sequence{Block: 0x0, Count: 8}, 0, 0}, + {&Sequence{Block: 0x80000000, Count: 8}, 0, 1}, + {&Sequence{Block: 0xC0000000, Count: 8}, 0, 2}, + {&Sequence{Block: 0xE0000000, Count: 8}, 0, 3}, + {&Sequence{Block: 0xF0000000, Count: 8}, 0, 4}, + {&Sequence{Block: 0xF8000000, Count: 8}, 0, 5}, + {&Sequence{Block: 0xFC000000, Count: 8}, 0, 6}, + {&Sequence{Block: 0xFE000000, Count: 8}, 0, 7}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x00000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 0}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 2}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xE0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 3}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 4}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF8000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 5}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFC000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 6}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFE000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 7}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 0}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF800000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFC00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 2}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFE00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 3}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 4}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF80000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 5}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFC0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 6}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFE0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 7}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 7, 7}, + + {&Sequence{Block: 0xffffffff, Count: 2, Next: &Sequence{Block: 0x0, Count: 6}}, 8, 0}, + } + + for n, i := range input { + bytePos, bitPos, _ := GetFirstAvailable(i.mask) + if bytePos != i.bytePos || bitPos != i.bitPos { + t.Fatalf("Error in (%d) getFirstAvailable(). Expected (%d, %d). Got (%d, %d)", n, i.bytePos, i.bitPos, bytePos, bitPos) + } + } +} + +func TestFindSequence(t *testing.T) { + input := []struct { + head *Sequence + bytePos int + precBlocks uint32 + inBlockBytePos int + }{ + {&Sequence{Block: 0xffffffff, Count: 0}, 0, 0, -1}, + {&Sequence{Block: 0xffffffff, Count: 0}, 31, 0, -1}, + {&Sequence{Block: 0xffffffff, Count: 0}, 100, 0, -1}, + + {&Sequence{Block: 0x0, Count: 1}, 0, 0, 0}, + {&Sequence{Block: 0x0, Count: 1}, 1, 0, 1}, + {&Sequence{Block: 0x0, Count: 1}, 31, 0, -1}, + {&Sequence{Block: 0x0, Count: 1}, 60, 0, -1}, + + {&Sequence{Block: 0xffffffff, Count: 10}, 0, 0, 0}, + {&Sequence{Block: 0xffffffff, Count: 10}, 3, 0, 3}, + {&Sequence{Block: 0xffffffff, Count: 10}, 4, 1, 0}, + {&Sequence{Block: 0xffffffff, Count: 10}, 7, 1, 3}, + {&Sequence{Block: 0xffffffff, Count: 10}, 8, 2, 0}, + {&Sequence{Block: 0xffffffff, Count: 10}, 39, 9, 3}, + + {&Sequence{Block: 0xffffffff, Count: 10, Next: &Sequence{Block: 0xcc000000, Count: 10}}, 79, 9, 3}, + {&Sequence{Block: 0xffffffff, Count: 10, Next: &Sequence{Block: 0xcc000000, Count: 10}}, 80, 0, -1}, + } + + for n, i := range input { + _, _, precBlocks, inBlockBytePos := findSequence(i.head, i.bytePos) + if precBlocks != i.precBlocks || inBlockBytePos != i.inBlockBytePos { + t.Fatalf("Error in (%d) findSequence(). Expected (%d, %d). Got (%d, %d)", n, i.precBlocks, i.inBlockBytePos, precBlocks, inBlockBytePos) + } + } +} + +func TestCheckIfAvailable(t *testing.T) { + input := []struct { + head *Sequence + ordinal int + bytePos int + bitPos int + }{ + {&Sequence{Block: 0xffffffff, Count: 0}, 0, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 0}, 31, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 0}, 100, -1, -1}, + + {&Sequence{Block: 0x0, Count: 1}, 0, 0, 0}, + {&Sequence{Block: 0x0, Count: 1}, 1, 0, 1}, + {&Sequence{Block: 0x0, Count: 1}, 31, 3, 7}, + {&Sequence{Block: 0x0, Count: 1}, 60, -1, -1}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x800000ff, Count: 1}}, 31, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x800000ff, Count: 1}}, 32, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x800000ff, Count: 1}}, 33, 4, 1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1}}, 33, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1}}, 34, 4, 2}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 55, 6, 7}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 56, -1, -1}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 63, -1, -1}, + + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 64, 8, 0}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 95, 11, 7}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC00000ff, Count: 1, Next: &Sequence{Block: 0x0, Count: 1}}}, 96, -1, -1}, + } + + for n, i := range input { + bytePos, bitPos, _ := CheckIfAvailable(i.head, i.ordinal) + if bytePos != i.bytePos || bitPos != i.bitPos { + t.Fatalf("Error in (%d) checkIfAvailable(ord:%d). Expected (%d, %d). Got (%d, %d)", n, i.ordinal, i.bytePos, i.bitPos, bytePos, bitPos) + } + } +} + +func TestMergeSequences(t *testing.T) { + input := []struct { + original *Sequence + merged *Sequence + }{ + {&Sequence{Block: 0xFE000000, Count: 8, Next: &Sequence{Block: 0xFE000000, Count: 2}}, &Sequence{Block: 0xFE000000, Count: 10}}, + {&Sequence{Block: 0xFFFFFFFF, Count: 8, Next: &Sequence{Block: 0xFFFFFFFF, Count: 1}}, &Sequence{Block: 0xFFFFFFFF, Count: 9}}, + {&Sequence{Block: 0xFFFFFFFF, Count: 1, Next: &Sequence{Block: 0xFFFFFFFF, Count: 8}}, &Sequence{Block: 0xFFFFFFFF, Count: 9}}, + + {&Sequence{Block: 0xFFFFFFF0, Count: 8, Next: &Sequence{Block: 0xFFFFFFF0, Count: 1}}, &Sequence{Block: 0xFFFFFFF0, Count: 9}}, + {&Sequence{Block: 0xFFFFFFF0, Count: 1, Next: &Sequence{Block: 0xFFFFFFF0, Count: 8}}, &Sequence{Block: 0xFFFFFFF0, Count: 9}}, + + {&Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xFE, Count: 1, Next: &Sequence{Block: 0xFE, Count: 5}}}, &Sequence{Block: 0xFE, Count: 14}}, + {&Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xFE, Count: 1, Next: &Sequence{Block: 0xFE, Count: 5, Next: &Sequence{Block: 0xFF, Count: 1}}}}, + &Sequence{Block: 0xFE, Count: 14, Next: &Sequence{Block: 0xFF, Count: 1}}}, + + // No merge + {&Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xF8, Count: 1, Next: &Sequence{Block: 0xFE, Count: 5}}}, + &Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xF8, Count: 1, Next: &Sequence{Block: 0xFE, Count: 5}}}}, + + // No merge from head: // Merge function tries to merge from passed head. If it can't merge with Next, it does not reattempt with Next as head + {&Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xFF, Count: 1, Next: &Sequence{Block: 0xFF, Count: 5}}}, + &Sequence{Block: 0xFE, Count: 8, Next: &Sequence{Block: 0xFF, Count: 6}}}, + } + + for n, i := range input { + mergeSequences(i.original) + for !i.merged.Equal(i.original) { + t.Fatalf("Error in (%d) mergeSequences().\nExp: %s\nGot: %s,", n, i.merged, i.original) + } + } +} + +func TestPushReservation(t *testing.T) { + input := []struct { + mask *Sequence + bytePos int + bitPos int + newMask *Sequence + }{ + // Create first Sequence and fill in 8 addresses starting from address 0 + {&Sequence{Block: 0x0, Count: 8, Next: nil}, 0, 0, &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 7, Next: nil}}}, + {&Sequence{Block: 0x80000000, Count: 8}, 0, 1, &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0x80000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xC0000000, Count: 8}, 0, 2, &Sequence{Block: 0xE0000000, Count: 1, Next: &Sequence{Block: 0xC0000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xE0000000, Count: 8}, 0, 3, &Sequence{Block: 0xF0000000, Count: 1, Next: &Sequence{Block: 0xE0000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xF0000000, Count: 8}, 0, 4, &Sequence{Block: 0xF8000000, Count: 1, Next: &Sequence{Block: 0xF0000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xF8000000, Count: 8}, 0, 5, &Sequence{Block: 0xFC000000, Count: 1, Next: &Sequence{Block: 0xF8000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xFC000000, Count: 8}, 0, 6, &Sequence{Block: 0xFE000000, Count: 1, Next: &Sequence{Block: 0xFC000000, Count: 7, Next: nil}}}, + {&Sequence{Block: 0xFE000000, Count: 8}, 0, 7, &Sequence{Block: 0xFF000000, Count: 1, Next: &Sequence{Block: 0xFE000000, Count: 7, Next: nil}}}, + + {&Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 7}}, 0, 1, &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 7, Next: nil}}}, + + // Create second Sequence and fill in 8 addresses starting from address 32 + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x00000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6, Next: nil}}}, 4, 0, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 1, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 2, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xE0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xE0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 3, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF0000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 4, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF8000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xF8000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 5, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFC000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFC000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 6, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFE000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFE000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 4, 7, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + // fill in 8 addresses starting from address 40 + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF000000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 0, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF800000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFF800000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 1, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFC00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFC00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 2, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFE00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFE00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 3, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF00000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 4, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF80000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFF80000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 5, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFC0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFC0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 6, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFE0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFE0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}, 5, 7, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xFFFF0000, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 6}}}}, + + // Insert new Sequence + {&Sequence{Block: 0xffffffff, Count: 2, Next: &Sequence{Block: 0x0, Count: 6}}, 8, 0, + &Sequence{Block: 0xffffffff, Count: 2, Next: &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 5}}}}, + {&Sequence{Block: 0xffffffff, Count: 2, Next: &Sequence{Block: 0x80000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 5}}}, 8, 1, + &Sequence{Block: 0xffffffff, Count: 2, Next: &Sequence{Block: 0xC0000000, Count: 1, Next: &Sequence{Block: 0x0, Count: 5}}}}, + + // Merge affected with Next + {&Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 2, Next: &Sequence{Block: 0xffffffff, Count: 1}}}, 31, 7, + &Sequence{Block: 0xffffffff, Count: 8, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 1}}}}, + {&Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xfffffffc, Count: 1, Next: &Sequence{Block: 0xfffffffe, Count: 6}}}, 7, 6, + &Sequence{Block: 0xffffffff, Count: 1, Next: &Sequence{Block: 0xfffffffe, Count: 7}}}, + + // Merge affected with Next and Next.Next + {&Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 1}}}, 31, 7, + &Sequence{Block: 0xffffffff, Count: 9}}, + {&Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 1}}, 31, 7, + &Sequence{Block: 0xffffffff, Count: 8}}, + + // Merge affected with previous and Next + {&Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 1}}}, 31, 7, + &Sequence{Block: 0xffffffff, Count: 9}}, + + // Redundant push: No change + {&Sequence{Block: 0xffff0000, Count: 1}, 0, 0, &Sequence{Block: 0xffff0000, Count: 1}}, + {&Sequence{Block: 0xffff0000, Count: 7}, 25, 7, &Sequence{Block: 0xffff0000, Count: 7}}, + {&Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 1}}}, 7, 7, + &Sequence{Block: 0xffffffff, Count: 7, Next: &Sequence{Block: 0xfffffffe, Count: 1, Next: &Sequence{Block: 0xffffffff, Count: 1}}}}, + } + + for n, i := range input { + mask := PushReservation(i.bytePos, i.bitPos, i.mask, false) + if !mask.Equal(i.newMask) { + t.Fatalf("Error in (%d) pushReservation():\n%s + (%d,%d):\nExp: %s\nGot: %s,", n, i.mask, i.bytePos, i.bitPos, i.newMask, mask) + } + } +} + +func TestSerializeDeserialize(t *testing.T) { + s := &Sequence{ + Block: 0xffffffff, + Count: 1, + Next: &Sequence{ + Block: 0xFF000000, + Count: 1, + Next: &Sequence{ + Block: 0xffffffff, + Count: 6, + Next: &Sequence{ + Block: 0xffffffff, + Count: 1, + Next: &Sequence{ + Block: 0xFF800000, + Count: 1, + Next: &Sequence{ + Block: 0xffffffff, + Count: 6, + }, + }, + }, + }, + }, + } + + data, err := s.ToByteArray() + if err != nil { + t.Fatal(err) + } + + r := &Sequence{} + err = r.FromByteArray(data) + if err != nil { + t.Fatal(err) + } + + if !s.Equal(r) { + t.Fatalf("Sequences are different: \n%v\n%v", s, r) + } +} diff --git a/bitseq/store.go b/bitseq/store.go new file mode 100644 index 00000000..b5b3f231 --- /dev/null +++ b/bitseq/store.go @@ -0,0 +1,138 @@ +package bitseq + +import ( + "encoding/json" + "fmt" + + log "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/types" +) + +// Key provides the Key to be used in KV Store +func (h *Handle) Key() []string { + h.Lock() + defer h.Unlock() + return []string{h.app, h.id} +} + +// KeyPrefix returns the immediate parent key that can be used for tree walk +func (h *Handle) KeyPrefix() []string { + h.Lock() + defer h.Unlock() + return []string{h.app} +} + +// Value marshals the data to be stored in the KV store +func (h *Handle) Value() []byte { + b, err := h.ToByteArray() + if err != nil { + log.Warnf("Failed to serialize Handle: %v", err) + b = []byte{} + } + jv, err := json.Marshal(b) + if err != nil { + log.Warnf("Failed to json encode bitseq handler byte array: %v", err) + return []byte{} + } + return jv +} + +// SetValue unmarshals the data from the KV store +func (h *Handle) SetValue(value []byte) error { + var b []byte + if err := json.Unmarshal(value, &b); err != nil { + return err + } + + return h.FromByteArray(b) +} + +// Index returns the latest DB Index as seen by this object +func (h *Handle) Index() uint64 { + h.Lock() + defer h.Unlock() + return h.dbIndex +} + +// SetIndex method allows the datastore to store the latest DB Index into this object +func (h *Handle) SetIndex(index uint64) { + h.Lock() + h.dbIndex = index + h.dbExists = true + h.Unlock() +} + +// Exists method is true if this object has been stored in the DB. +func (h *Handle) Exists() bool { + h.Lock() + defer h.Unlock() + return h.dbExists +} + +func (h *Handle) watchForChanges() error { + h.Lock() + store := h.store + h.Unlock() + + if store == nil { + return nil + } + + kvpChan, err := store.KVStore().Watch(datastore.Key(h.Key()...), nil) + if err != nil { + return err + } + go func() { + for { + select { + case kvPair := <-kvpChan: + // Only process remote update + if kvPair != nil && (kvPair.LastIndex != h.Index()) { + err := h.fromDsValue(kvPair.Value) + if err != nil { + log.Warnf("Failed to reconstruct bitseq handle from ds watch: %s", err.Error()) + } else { + h.SetIndex(kvPair.LastIndex) + } + } + } + } + }() + return nil +} + +func (h *Handle) fromDsValue(value []byte) error { + var ba []byte + if err := json.Unmarshal(value, &ba); err != nil { + return fmt.Errorf("failed to decode json: %s", err.Error()) + } + if err := h.FromByteArray(ba); err != nil { + return fmt.Errorf("failed to decode handle: %s", err.Error()) + } + return nil +} + +func (h *Handle) writeToStore() error { + h.Lock() + store := h.store + h.Unlock() + if store == nil { + return nil + } + err := store.PutObjectAtomic(h) + if err == datastore.ErrKeyModified { + return types.RetryErrorf("failed to perform atomic write (%v). retry might fix the error", err) + } + return err +} + +func (h *Handle) deleteFromStore() error { + h.Lock() + store := h.store + h.Unlock() + if store == nil { + return nil + } + return store.DeleteObjectAtomic(h) +} diff --git a/circle.yml b/circle.yml new file mode 100644 index 00000000..d02f6a92 --- /dev/null +++ b/circle.yml @@ -0,0 +1,12 @@ +machine: + services: + - docker + +dependencies: + override: + - echo "Nothing to install" + +test: + override: + - make circle-ci + diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..c5713b01 --- /dev/null +++ b/client/client.go @@ -0,0 +1,115 @@ +package client + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "reflect" + "strings" + + flag "github.com/docker/docker/pkg/mflag" +) + +// CallFunc provides environment specific call utility to invoke backend functions from UI +type CallFunc func(string, string, interface{}, map[string][]string) (io.ReadCloser, http.Header, int, error) + +// NetworkCli is the UI object for network subcmds +type NetworkCli struct { + out io.Writer + err io.Writer + call CallFunc +} + +// NewNetworkCli is a convenient function to create a NetworkCli object +func NewNetworkCli(out, err io.Writer, call CallFunc) *NetworkCli { + return &NetworkCli{ + out: out, + err: err, + call: call, + } +} + +// getMethod is Borrowed from Docker UI which uses reflection to identify the UI Handler +func (cli *NetworkCli) getMethod(args ...string) (func(string, ...string) error, bool) { + camelArgs := make([]string, len(args)) + for i, s := range args { + if len(s) == 0 { + return nil, false + } + camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) + } + methodName := "Cmd" + strings.Join(camelArgs, "") + method := reflect.ValueOf(cli).MethodByName(methodName) + if !method.IsValid() { + return nil, false + } + return method.Interface().(func(string, ...string) error), true +} + +// Cmd is borrowed from Docker UI and acts as the entry point for network UI commands. +// network UI commands are designed to be invoked from multiple parent chains +func (cli *NetworkCli) Cmd(chain string, args ...string) error { + if len(args) > 2 { + method, exists := cli.getMethod(args[:3]...) + if exists { + return method(chain+" "+args[0]+" "+args[1], args[3:]...) + } + } + if len(args) > 1 { + method, exists := cli.getMethod(args[:2]...) + if exists { + return method(chain+" "+args[0], args[2:]...) + } + } + if len(args) > 0 { + method, exists := cli.getMethod(args[0]) + if !exists { + return fmt.Errorf("%s: '%s' is not a %s command. See '%s --help'.\n", chain, args[0], chain, chain) + } + return method(chain, args[1:]...) + } + flag.Usage() + return nil +} + +// Subcmd is borrowed from Docker UI and performs the same function of configuring the subCmds +func (cli *NetworkCli) Subcmd(chain, name, signature, description string, exitOnError bool) *flag.FlagSet { + var errorHandling flag.ErrorHandling + if exitOnError { + errorHandling = flag.ExitOnError + } else { + errorHandling = flag.ContinueOnError + } + flags := flag.NewFlagSet(name, errorHandling) + flags.Usage = func() { + flags.ShortUsage() + flags.PrintDefaults() + } + flags.ShortUsage = func() { + options := "" + if signature != "" { + signature = " " + signature + } + if flags.FlagCountUndeprecated() > 0 { + options = " [OPTIONS]" + } + fmt.Fprintf(cli.out, "\nUsage: %s %s%s%s\n\n%s\n\n", chain, name, options, signature, description) + flags.SetOutput(cli.out) + } + return flags +} + +func readBody(stream io.ReadCloser, hdr http.Header, statusCode int, err error) ([]byte, int, error) { + if stream != nil { + defer stream.Close() + } + if err != nil { + return nil, statusCode, err + } + body, err := ioutil.ReadAll(stream) + if err != nil { + return nil, -1, err + } + return body, statusCode, nil +} diff --git a/client/client_service_test.go b/client/client_service_test.go new file mode 100644 index 00000000..fb10fd9f --- /dev/null +++ b/client/client_service_test.go @@ -0,0 +1,122 @@ +package client + +import ( + "bytes" + "testing" + + _ "github.com/docker/libnetwork/netutils" +) + +func TestClientServiceInvalidCommand(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "invalid") + if err == nil { + t.Fatalf("Passing invalid commands must fail") + } +} + +func TestClientServiceCreate(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "publish", mockServiceName+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceRm(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "unpublish", mockServiceName+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceLs(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "ls") + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceInfo(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "info", mockServiceName+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceInfoById(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "info", mockServiceID+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceJoin(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "attach", mockContainerID, mockServiceName+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +func TestClientServiceLeave(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "service", "detach", mockContainerID, mockServiceName+"."+mockNwName) + if err != nil { + t.Fatal(err) + } +} + +// Docker Flag processing in flag.go uses os.Exit() frequently, even for --help +// TODO : Handle the --help test-case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateHelp(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create", "--help") + if err != nil { + t.Fatalf(err.Error()) + } +} +*/ + +// Docker flag processing in flag.go uses os.Exit(1) for incorrect parameter case. +// TODO : Handle the missing argument case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateMissingArgument(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create") + if err != nil { + t.Fatal(err.Error()) + } +} +*/ diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..06b8b2a6 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,217 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "testing" + + _ "github.com/docker/libnetwork/netutils" +) + +// nopCloser is used to provide a dummy CallFunc for Cmd() +type nopCloser struct { + io.Reader +} + +func (nopCloser) Close() error { return nil } + +func TestMain(m *testing.M) { + setupMockHTTPCallback() + os.Exit(m.Run()) +} + +var callbackFunc func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) +var mockNwJSON, mockNwListJSON, mockServiceJSON, mockServiceListJSON []byte +var mockNwName = "test" +var mockNwID = "2a3456789" +var mockServiceName = "testSrv" +var mockServiceID = "2a3456789" +var mockContainerID = "2a3456789" + +func setupMockHTTPCallback() { + var list []networkResource + nw := networkResource{Name: mockNwName, ID: mockNwID} + mockNwJSON, _ = json.Marshal(nw) + list = append(list, nw) + mockNwListJSON, _ = json.Marshal(list) + + var srvList []serviceResource + ep := serviceResource{Name: mockServiceName, ID: mockServiceID, Network: mockNwName} + mockServiceJSON, _ = json.Marshal(ep) + srvList = append(srvList, ep) + mockServiceListJSON, _ = json.Marshal(srvList) + + dummyHTTPHdr := http.Header{} + + callbackFunc = func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { + var rsp string + switch method { + case "GET": + if strings.Contains(path, fmt.Sprintf("networks?name=%s", mockNwName)) { + rsp = string(mockNwListJSON) + } else if strings.Contains(path, "networks?name=") { + rsp = "[]" + } else if strings.Contains(path, fmt.Sprintf("networks?partial-id=%s", mockNwID)) { + rsp = string(mockNwListJSON) + } else if strings.Contains(path, "networks?partial-id=") { + rsp = "[]" + } else if strings.HasSuffix(path, "networks") { + rsp = string(mockNwListJSON) + } else if strings.HasSuffix(path, "networks/"+mockNwID) { + rsp = string(mockNwJSON) + } else if strings.Contains(path, fmt.Sprintf("services?name=%s", mockServiceName)) { + rsp = string(mockServiceListJSON) + } else if strings.Contains(path, "services?name=") { + rsp = "[]" + } else if strings.Contains(path, fmt.Sprintf("services?partial-id=%s", mockServiceID)) { + rsp = string(mockServiceListJSON) + } else if strings.Contains(path, "services?partial-id=") { + rsp = "[]" + } else if strings.HasSuffix(path, "services") { + rsp = string(mockServiceListJSON) + } else if strings.HasSuffix(path, "services/"+mockServiceID) { + rsp = string(mockServiceJSON) + } else if strings.Contains(path, "containers") { + return nopCloser{bytes.NewBufferString("")}, dummyHTTPHdr, 400, fmt.Errorf("Bad Request") + } + case "POST": + var data []byte + if strings.HasSuffix(path, "networks") { + data, _ = json.Marshal(mockNwID) + } else if strings.HasSuffix(path, "services") { + data, _ = json.Marshal(mockServiceID) + } else if strings.HasSuffix(path, "backend") { + data, _ = json.Marshal(mockContainerID) + } + rsp = string(data) + case "PUT": + case "DELETE": + rsp = "" + } + return nopCloser{bytes.NewBufferString(rsp)}, dummyHTTPHdr, 200, nil + } +} + +func TestClientDummyCommand(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "dummy") + if err == nil { + t.Fatalf("Incorrect Command must fail") + } +} + +func TestClientNetworkInvalidCommand(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "invalid") + if err == nil { + t.Fatalf("Passing invalid commands must fail") + } +} + +func TestClientNetworkCreate(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create", mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkCreateWithDriver(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create", "-f=dummy", mockNwName) + if err == nil { + t.Fatalf("Passing incorrect flags to the create command must fail") + } + + err = cli.Cmd("docker", "network", "create", "-d=dummy", mockNwName) + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestClientNetworkRm(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "rm", mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkLs(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "ls") + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkInfo(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "info", mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkInfoById(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "info", mockNwID) + if err != nil { + t.Fatal(err.Error()) + } +} + +// Docker Flag processing in flag.go uses os.Exit() frequently, even for --help +// TODO : Handle the --help test-case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateHelp(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create", "--help") + if err != nil { + t.Fatalf(err.Error()) + } +} +*/ + +// Docker flag processing in flag.go uses os.Exit(1) for incorrect parameter case. +// TODO : Handle the missing argument case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateMissingArgument(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create") + if err != nil { + t.Fatal(err.Error()) + } +} +*/ diff --git a/client/network.go b/client/network.go new file mode 100644 index 00000000..a244ad5f --- /dev/null +++ b/client/network.go @@ -0,0 +1,231 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "text/tabwriter" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringid" +) + +type command struct { + name string + description string +} + +var ( + networkCommands = []command{ + {"create", "Create a network"}, + {"rm", "Remove a network"}, + {"ls", "List all networks"}, + {"info", "Display information of a network"}, + } +) + +// CmdNetwork handles the root Network UI +func (cli *NetworkCli) CmdNetwork(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "network", "COMMAND [OPTIONS] [arg...]", networkUsage(chain), false) + cmd.Require(flag.Min, 1) + err := cmd.ParseFlags(args, true) + if err == nil { + cmd.Usage() + return fmt.Errorf("invalid command : %v", args) + } + return err +} + +// CmdNetworkCreate handles Network Create UI +func (cli *NetworkCli) CmdNetworkCreate(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "create", "NETWORK-NAME", "Creates a new network with a name specified by the user", false) + flDriver := cmd.String([]string{"d", "-driver"}, "", "Driver to manage the Network") + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + // Construct network create request body + ops := make(map[string]interface{}) + nc := networkCreate{Name: cmd.Arg(0), NetworkType: *flDriver, Options: ops} + obj, _, err := readBody(cli.call("POST", "/networks", nc, nil)) + if err != nil { + return err + } + var replyID string + err = json.Unmarshal(obj, &replyID) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "%s\n", replyID) + return nil +} + +// CmdNetworkRm handles Network Delete UI +func (cli *NetworkCli) CmdNetworkRm(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "rm", "NETWORK", "Deletes a network", false) + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + id, err := lookupNetworkID(cli, cmd.Arg(0)) + if err != nil { + return err + } + _, _, err = readBody(cli.call("DELETE", "/networks/"+id, nil, nil)) + if err != nil { + return err + } + return nil +} + +// CmdNetworkLs handles Network List UI +func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "ls", "", "Lists all the networks created by the user", false) + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") + noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Do not truncate the output") + nLatest := cmd.Bool([]string{"l", "-latest"}, false, "Show the latest network created") + last := cmd.Int([]string{"n"}, -1, "Show n last created networks") + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + obj, _, err := readBody(cli.call("GET", "/networks", nil, nil)) + if err != nil { + return err + } + if *last == -1 && *nLatest { + *last = 1 + } + + var networkResources []networkResource + err = json.Unmarshal(obj, &networkResources) + if err != nil { + return err + } + + wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + + // unless quiet (-q) is specified, print field titles + if !*quiet { + fmt.Fprintln(wr, "NETWORK ID\tNAME\tTYPE") + } + + for _, networkResource := range networkResources { + ID := networkResource.ID + netName := networkResource.Name + if !*noTrunc { + ID = stringid.TruncateID(ID) + } + if *quiet { + fmt.Fprintln(wr, ID) + continue + } + netType := networkResource.Type + fmt.Fprintf(wr, "%s\t%s\t%s\t", + ID, + netName, + netType) + fmt.Fprint(wr, "\n") + } + wr.Flush() + return nil +} + +// CmdNetworkInfo handles Network Info UI +func (cli *NetworkCli) CmdNetworkInfo(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "info", "NETWORK", "Displays detailed information on a network", false) + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + id, err := lookupNetworkID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("GET", "/networks/"+id, nil, nil)) + if err != nil { + return err + } + networkResource := &networkResource{} + if err := json.NewDecoder(bytes.NewReader(obj)).Decode(networkResource); err != nil { + return err + } + fmt.Fprintf(cli.out, "Network Id: %s\n", networkResource.ID) + fmt.Fprintf(cli.out, "Name: %s\n", networkResource.Name) + fmt.Fprintf(cli.out, "Type: %s\n", networkResource.Type) + if networkResource.Services != nil { + for _, serviceResource := range networkResource.Services { + fmt.Fprintf(cli.out, " Service Id: %s\n", serviceResource.ID) + fmt.Fprintf(cli.out, "\tName: %s\n", serviceResource.Name) + } + } + + return nil +} + +// Helper function to predict if a string is a name or id or partial-id +// This provides a best-effort mechanism to identify a id with the help of GET Filter APIs +// Being a UI, its most likely that name will be used by the user, which is used to lookup +// the corresponding ID. If ID is not found, this function will assume that the passed string +// is an ID by itself. + +func lookupNetworkID(cli *NetworkCli, nameID string) (string, error) { + obj, statusCode, err := readBody(cli.call("GET", "/networks?name="+nameID, nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("name query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) + } + + var list []*networkResource + err = json.Unmarshal(obj, &list) + if err != nil { + return "", err + } + if len(list) > 0 { + // name query filter will always return a single-element collection + return list[0].ID, nil + } + + // Check for Partial-id + obj, statusCode, err = readBody(cli.call("GET", "/networks?partial-id="+nameID, nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("partial-id match query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) + } + + err = json.Unmarshal(obj, &list) + if err != nil { + return "", err + } + if len(list) == 0 { + return "", fmt.Errorf("resource not found %s", nameID) + } + if len(list) > 1 { + return "", fmt.Errorf("multiple Networks matching the partial identifier (%s). Please use full identifier", nameID) + } + return list[0].ID, nil +} + +func networkUsage(chain string) string { + help := "Commands:\n" + + for _, cmd := range networkCommands { + help += fmt.Sprintf(" %-25.25s%s\n", cmd.name, cmd.description) + } + + help += fmt.Sprintf("\nRun '%s network COMMAND --help' for more information on a command.", chain) + return help +} diff --git a/client/service.go b/client/service.go new file mode 100644 index 00000000..35e040f8 --- /dev/null +++ b/client/service.go @@ -0,0 +1,362 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "text/tabwriter" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/stringid" +) + +var ( + serviceCommands = []command{ + {"publish", "Publish a service"}, + {"unpublish", "Remove a service"}, + {"attach", "Attach a backend (container) to the service"}, + {"detach", "Detach the backend from the service"}, + {"ls", "Lists all services"}, + {"info", "Display information about a service"}, + } +) + +func lookupServiceID(cli *NetworkCli, nwName, svNameID string) (string, error) { + // Sanity Check + obj, _, err := readBody(cli.call("GET", fmt.Sprintf("/networks?name=%s", nwName), nil, nil)) + if err != nil { + return "", err + } + var nwList []networkResource + if err = json.Unmarshal(obj, &nwList); err != nil { + return "", err + } + if len(nwList) == 0 { + return "", fmt.Errorf("Network %s does not exist", nwName) + } + + if nwName == "" { + obj, _, err := readBody(cli.call("GET", "/networks/"+nwList[0].ID, nil, nil)) + if err != nil { + return "", err + } + networkResource := &networkResource{} + if err := json.NewDecoder(bytes.NewReader(obj)).Decode(networkResource); err != nil { + return "", err + } + nwName = networkResource.Name + } + + // Query service by name + obj, statusCode, err := readBody(cli.call("GET", fmt.Sprintf("/services?name=%s", svNameID), nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("name query failed for %s due to: (%d) %s", svNameID, statusCode, string(obj)) + } + + var list []*serviceResource + if err = json.Unmarshal(obj, &list); err != nil { + return "", err + } + for _, sr := range list { + if sr.Network == nwName { + return sr.ID, nil + } + } + + // Query service by Partial-id (this covers full id as well) + obj, statusCode, err = readBody(cli.call("GET", fmt.Sprintf("/services?partial-id=%s", svNameID), nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("partial-id match query failed for %s due to: (%d) %s", svNameID, statusCode, string(obj)) + } + + if err = json.Unmarshal(obj, &list); err != nil { + return "", err + } + for _, sr := range list { + if sr.Network == nwName { + return sr.ID, nil + } + } + + return "", fmt.Errorf("Service %s not found on network %s", svNameID, nwName) +} + +func lookupContainerID(cli *NetworkCli, cnNameID string) (string, error) { + // Container is a Docker resource, ask docker about it. + // In case of connecton error, we assume we are running in dnet and return whatever was passed to us + obj, _, err := readBody(cli.call("GET", fmt.Sprintf("/containers/%s/json", cnNameID), nil, nil)) + if err != nil { + // We are probably running outside of docker + return cnNameID, nil + } + + var x map[string]interface{} + err = json.Unmarshal(obj, &x) + if err != nil { + return "", err + } + if iid, ok := x["Id"]; ok { + if id, ok := iid.(string); ok { + return id, nil + } + return "", fmt.Errorf("Unexpected data type for container ID in json response") + } + return "", fmt.Errorf("Cannot find container ID in json response") +} + +// CmdService handles the service UI +func (cli *NetworkCli) CmdService(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "service", "COMMAND [OPTIONS] [arg...]", serviceUsage(chain), false) + cmd.Require(flag.Min, 1) + err := cmd.ParseFlags(args, true) + if err == nil { + cmd.Usage() + return fmt.Errorf("Invalid command : %v", args) + } + return err +} + +// Parse service name for "SERVICE[.NETWORK]" format +func parseServiceName(name string) (string, string) { + s := strings.Split(name, ".") + var sName, nName string + if len(s) > 1 { + nName = s[len(s)-1] + sName = strings.Join(s[:len(s)-1], ".") + } else { + sName = s[0] + } + return sName, nName +} + +// CmdServicePublish handles service create UI +func (cli *NetworkCli) CmdServicePublish(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "publish", "SERVICE[.NETWORK]", "Publish a new service on a network", false) + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + sn, nn := parseServiceName(cmd.Arg(0)) + sc := serviceCreate{Name: sn, Network: nn} + obj, _, err := readBody(cli.call("POST", "/services", sc, nil)) + if err != nil { + return err + } + + var replyID string + err = json.Unmarshal(obj, &replyID) + if err != nil { + return err + } + + fmt.Fprintf(cli.out, "%s\n", replyID) + return nil +} + +// CmdServiceUnpublish handles service delete UI +func (cli *NetworkCli) CmdServiceUnpublish(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "unpublish", "SERVICE[.NETWORK]", "Removes a service", false) + cmd.Require(flag.Exact, 1) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + sn, nn := parseServiceName(cmd.Arg(0)) + serviceID, err := lookupServiceID(cli, nn, sn) + if err != nil { + return err + } + + _, _, err = readBody(cli.call("DELETE", "/services/"+serviceID, nil, nil)) + + return err +} + +// CmdServiceLs handles service list UI +func (cli *NetworkCli) CmdServiceLs(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "ls", "SERVICE", "Lists all the services on a network", false) + flNetwork := cmd.String([]string{"net", "-network"}, "", "Only show the services that are published on the specified network") + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") + noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Do not truncate the output") + + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + var obj []byte + if *flNetwork == "" { + obj, _, err = readBody(cli.call("GET", "/services", nil, nil)) + } else { + obj, _, err = readBody(cli.call("GET", "/services?network="+*flNetwork, nil, nil)) + } + if err != nil { + return err + } + + var serviceResources []serviceResource + err = json.Unmarshal(obj, &serviceResources) + if err != nil { + fmt.Println(err) + return err + } + + wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + // unless quiet (-q) is specified, print field titles + if !*quiet { + fmt.Fprintln(wr, "SERVICE ID\tNAME\tNETWORK\tCONTAINER") + } + + for _, sr := range serviceResources { + ID := sr.ID + bkID, err := getBackendID(cli, ID) + if err != nil { + return err + } + if !*noTrunc { + ID = stringid.TruncateID(ID) + bkID = stringid.TruncateID(bkID) + } + if !*quiet { + fmt.Fprintf(wr, "%s\t%s\t%s\t%s\n", ID, sr.Name, sr.Network, bkID) + } else { + fmt.Fprintln(wr, ID) + } + } + wr.Flush() + + return nil +} + +func getBackendID(cli *NetworkCli, servID string) (string, error) { + var ( + obj []byte + err error + bk string + ) + + if obj, _, err = readBody(cli.call("GET", "/services/"+servID+"/backend", nil, nil)); err == nil { + var bkl []backendResource + if err := json.NewDecoder(bytes.NewReader(obj)).Decode(&bkl); err == nil { + if len(bkl) > 0 { + bk = bkl[0].ID + } + } else { + // Only print a message, don't make the caller cli fail for this + fmt.Fprintf(cli.out, "Failed to retrieve backend list for service %s (%v)", servID, err) + } + } + + return bk, err +} + +// CmdServiceInfo handles service info UI +func (cli *NetworkCli) CmdServiceInfo(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "info", "SERVICE[.NETWORK]", "Displays detailed information about a service", false) + cmd.Require(flag.Min, 1) + + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + sn, nn := parseServiceName(cmd.Arg(0)) + serviceID, err := lookupServiceID(cli, nn, sn) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("GET", "/services/"+serviceID, nil, nil)) + if err != nil { + return err + } + + sr := &serviceResource{} + if err := json.NewDecoder(bytes.NewReader(obj)).Decode(sr); err != nil { + return err + } + + fmt.Fprintf(cli.out, "Service Id: %s\n", sr.ID) + fmt.Fprintf(cli.out, "\tName: %s\n", sr.Name) + fmt.Fprintf(cli.out, "\tNetwork: %s\n", sr.Network) + + return nil +} + +// CmdServiceAttach handles service attach UI +func (cli *NetworkCli) CmdServiceAttach(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "attach", "CONTAINER SERVICE[.NETWORK]", "Sets a container as a service backend", false) + cmd.Require(flag.Min, 2) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + containerID, err := lookupContainerID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + sn, nn := parseServiceName(cmd.Arg(1)) + serviceID, err := lookupServiceID(cli, nn, sn) + if err != nil { + return err + } + + nc := serviceAttach{ContainerID: containerID} + + _, _, err = readBody(cli.call("POST", "/services/"+serviceID+"/backend", nc, nil)) + + return err +} + +// CmdServiceDetach handles service detach UI +func (cli *NetworkCli) CmdServiceDetach(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "detach", "CONTAINER SERVICE", "Removes a container from service backend", false) + cmd.Require(flag.Min, 2) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + sn, nn := parseServiceName(cmd.Arg(1)) + containerID, err := lookupContainerID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + serviceID, err := lookupServiceID(cli, nn, sn) + if err != nil { + return err + } + + _, _, err = readBody(cli.call("DELETE", "/services/"+serviceID+"/backend/"+containerID, nil, nil)) + if err != nil { + return err + } + return nil +} + +func serviceUsage(chain string) string { + help := "Commands:\n" + + for _, cmd := range serviceCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd.name, cmd.description) + } + + help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain) + return help +} diff --git a/client/types.go b/client/types.go new file mode 100644 index 00000000..e14460ab --- /dev/null +++ b/client/types.go @@ -0,0 +1,73 @@ +package client + +import "github.com/docker/libnetwork/types" + +/*********** + Resources +************/ + +// networkResource is the body of the "get network" http response message +type networkResource struct { + Name string `json:"name"` + ID string `json:"id"` + Type string `json:"type"` + Services []*serviceResource `json:"services"` +} + +// serviceResource is the body of the "get service" http response message +type serviceResource struct { + Name string `json:"name"` + ID string `json:"id"` + Network string `json:"network"` +} + +// backendResource is the body of "get service backend" response message +type backendResource struct { + ID string `json:"id"` +} + +/*********** + Body types + ************/ + +// networkCreate is the expected body of the "create network" http request message +type networkCreate struct { + Name string `json:"name"` + NetworkType string `json:"network_type"` + Options map[string]interface{} `json:"options"` +} + +// serviceCreate represents the body of the "publish service" http request message +type serviceCreate struct { + Name string `json:"name"` + Network string `json:"network_name"` + ExposedPorts []types.TransportPort `json:"exposed_ports"` + PortMapping []types.PortBinding `json:"port_mapping"` +} + +// serviceAttach represents the expected body of the "attach/detach backend to/from service" http request messages +type serviceAttach struct { + ContainerID string `json:"container_id"` + HostName string `json:"host_name"` + DomainName string `json:"domain_name"` + HostsPath string `json:"hosts_path"` + ResolvConfPath string `json:"resolv_conf_path"` + DNS []string `json:"dns"` + ExtraHosts []serviceExtraHost `json:"extra_hosts"` + ParentUpdates []serviceParentUpdate `json:"parent_updates"` + UseDefaultSandbox bool `json:"use_default_sandbox"` +} + +// serviceExtraHost represents the extra host object +type serviceExtraHost struct { + Name string `json:"name"` + Address string `json:"address"` +} + +// EndpointParentUpdate is the object carrying the information about the +// endpoint parent that needs to be updated +type serviceParentUpdate struct { + EndpointID string `json:"service_id"` + Name string `json:"name"` + Address string `json:"address"` +} diff --git a/cmd/dnet/dnet.go b/cmd/dnet/dnet.go new file mode 100644 index 00000000..c0d497d4 --- /dev/null +++ b/cmd/dnet/dnet.go @@ -0,0 +1,288 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/pkg/reexec" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/term" + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/api" + "github.com/docker/libnetwork/client" + "github.com/docker/libnetwork/config" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/gorilla/mux" +) + +const ( + // DefaultHTTPHost is used if only port is provided to -H flag e.g. docker -d -H tcp://:8080 + DefaultHTTPHost = "127.0.0.1" + // DefaultHTTPPort is the default http port used by dnet + DefaultHTTPPort = 2385 + // DefaultUnixSocket exported + DefaultUnixSocket = "/var/run/dnet.sock" + cfgFileEnv = "LIBNETWORK_CFG" + defaultCfgFile = "/etc/default/libnetwork.toml" +) + +func main() { + if reexec.Init() { + return + } + + _, stdout, stderr := term.StdStreams() + logrus.SetOutput(stderr) + + err := dnetCommand(stdout, stderr) + if err != nil { + os.Exit(1) + } +} + +func parseConfig(cfgFile string) (*config.Config, error) { + if strings.Trim(cfgFile, " ") == "" { + cfgFile = os.Getenv(cfgFileEnv) + if strings.Trim(cfgFile, " ") == "" { + cfgFile = defaultCfgFile + } + } + return config.ParseConfig(cfgFile) +} + +func processConfig(cfg *config.Config) []config.Option { + options := []config.Option{} + if cfg == nil { + return options + } + dn := "bridge" + if strings.TrimSpace(cfg.Daemon.DefaultNetwork) != "" { + dn = cfg.Daemon.DefaultNetwork + } + options = append(options, config.OptionDefaultNetwork(dn)) + + dd := "bridge" + if strings.TrimSpace(cfg.Daemon.DefaultDriver) != "" { + dd = cfg.Daemon.DefaultDriver + } + options = append(options, config.OptionDefaultDriver(dd)) + + if cfg.Daemon.Labels != nil { + options = append(options, config.OptionLabels(cfg.Daemon.Labels)) + } + if strings.TrimSpace(cfg.Datastore.Client.Provider) != "" { + options = append(options, config.OptionKVProvider(cfg.Datastore.Client.Provider)) + } + if strings.TrimSpace(cfg.Datastore.Client.Address) != "" { + options = append(options, config.OptionKVProviderURL(cfg.Datastore.Client.Address)) + } + return options +} + +func dnetCommand(stdout, stderr io.Writer) error { + flag.Parse() + + if *flHelp { + flag.Usage() + return nil + } + + if *flLogLevel != "" { + lvl, err := logrus.ParseLevel(*flLogLevel) + if err != nil { + fmt.Fprintf(stderr, "Unable to parse logging level: %s\n", *flLogLevel) + return err + } + logrus.SetLevel(lvl) + } else { + logrus.SetLevel(logrus.InfoLevel) + } + + if *flDebug { + logrus.SetLevel(logrus.DebugLevel) + } + + if *flHost == "" { + defaultHost := os.Getenv("DNET_HOST") + if defaultHost == "" { + // TODO : Add UDS support + defaultHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultHTTPPort) + } + *flHost = defaultHost + } + + dc, err := newDnetConnection(*flHost) + if err != nil { + if *flDaemon { + logrus.Error(err) + } else { + fmt.Fprint(stderr, err) + } + return err + } + + if *flDaemon { + err := dc.dnetDaemon() + if err != nil { + logrus.Errorf("dnet Daemon exited with an error : %v", err) + } + return err + } + + cli := client.NewNetworkCli(stdout, stderr, dc.httpCall) + if err := cli.Cmd("dnet", flag.Args()...); err != nil { + fmt.Fprintln(stderr, err) + return err + } + return nil +} + +func createDefaultNetwork(c libnetwork.NetworkController) { + nw := c.Config().Daemon.DefaultNetwork + d := c.Config().Daemon.DefaultDriver + createOptions := []libnetwork.NetworkOption{} + genericOption := options.Generic{} + + if nw != "" && d != "" { + // Bridge driver is special due to legacy reasons + if d == "bridge" { + genericOption[netlabel.GenericData] = map[string]interface{}{ + "BridgeName": nw, + "AllowNonDefaultBridge": "true", + } + networkOption := libnetwork.NetworkOptionGeneric(genericOption) + createOptions = append(createOptions, networkOption) + } + _, err := c.NewNetwork(d, nw, createOptions...) + if err != nil { + logrus.Errorf("Error creating default network : %s : %v", nw, err) + } + } +} + +type dnetConnection struct { + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string +} + +func (d *dnetConnection) dnetDaemon() error { + cfg, err := parseConfig(*flCfgFile) + var cOptions []config.Option + if err == nil { + cOptions = processConfig(cfg) + } + controller, err := libnetwork.New(cOptions...) + if err != nil { + fmt.Println("Error starting dnetDaemon :", err) + return err + } + createDefaultNetwork(controller) + httpHandler := api.NewHTTPHandler(controller) + r := mux.NewRouter().StrictSlash(false) + post := r.PathPrefix("/{.*}/networks").Subrouter() + post.Methods("GET", "PUT", "POST", "DELETE").HandlerFunc(httpHandler) + post = r.PathPrefix("/networks").Subrouter() + post.Methods("GET", "PUT", "POST", "DELETE").HandlerFunc(httpHandler) + post = r.PathPrefix("/{.*}/services").Subrouter() + post.Methods("GET", "PUT", "POST", "DELETE").HandlerFunc(httpHandler) + post = r.PathPrefix("/services").Subrouter() + post.Methods("GET", "PUT", "POST", "DELETE").HandlerFunc(httpHandler) + return http.ListenAndServe(d.addr, r) +} + +func newDnetConnection(val string) (*dnetConnection, error) { + url, err := parsers.ParseHost(DefaultHTTPHost, DefaultUnixSocket, val) + if err != nil { + return nil, err + } + protoAddrParts := strings.SplitN(url, "://", 2) + if len(protoAddrParts) != 2 { + return nil, fmt.Errorf("bad format, expected tcp://ADDR") + } + if strings.ToLower(protoAddrParts[0]) != "tcp" { + return nil, fmt.Errorf("dnet currently only supports tcp transport") + } + + return &dnetConnection{protoAddrParts[0], protoAddrParts[1]}, nil +} + +func (d *dnetConnection) httpCall(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { + var in io.Reader + in, err := encodeData(data) + if err != nil { + return nil, nil, -1, err + } + + req, err := http.NewRequest(method, fmt.Sprintf("%s", path), in) + if err != nil { + return nil, nil, -1, err + } + + setupRequestHeaders(method, data, req, headers) + + req.URL.Host = d.addr + req.URL.Scheme = "http" + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + statusCode := -1 + if resp != nil { + statusCode = resp.StatusCode + } + if err != nil { + return nil, nil, statusCode, fmt.Errorf("error when trying to connect: %v", err) + } + + if statusCode < 200 || statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, statusCode, err + } + return nil, nil, statusCode, fmt.Errorf("error : %s", bytes.TrimSpace(body)) + } + + return resp.Body, resp.Header, statusCode, nil +} + +func setupRequestHeaders(method string, data interface{}, req *http.Request, headers map[string][]string) { + if data != nil { + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + expectedPayload := (method == "POST" || method == "PUT") + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} diff --git a/cmd/dnet/dnet_test.go b/cmd/dnet/dnet_test.go new file mode 100644 index 00000000..b8466f1a --- /dev/null +++ b/cmd/dnet/dnet_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/docker/libnetwork/netutils" +) + +const dnetCommandName = "dnet" + +var origStdOut = os.Stdout + +func TestDnetDaemonCustom(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4567 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d", fmt.Sprintf("-H=:%d", customPort)} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", fmt.Sprintf("-H=:%d", customPort), "-D", "network", "ls"} + executeDnetCommand(t, args, true) + } +} + +func TestDnetDaemonInvalidCustom(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4668 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", fmt.Sprintf("-H=:%d", customPort)} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", "-H=:6669", "-D", "network", "ls"} + executeDnetCommand(t, args, false) + } +} + +func TestDnetDaemonInvalidParams(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + args := []string{dnetCommandName, "-d=false", "-H=tcp:/127.0.0.1:8080"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=unix://var/run/dnet.sock"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=", "-l=invalid"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=", "-l=error", "invalid"} + executeDnetCommand(t, args, false) +} + +func TestDnetDefaultsWithFlags(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", "-H=", "-l=error"} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", "network", "create", "-d=null", "test"} + executeDnetCommand(t, args, true) + + args = []string{dnetCommandName, "-d=false", "-D", "network", "ls"} + executeDnetCommand(t, args, true) + } +} + +func TestDnetMain(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4568 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", "-h=false", fmt.Sprintf("-H=:%d", customPort)} + os.Args = args + main() + doneChan <- true + }() + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(2 * time.Second): + } +} + +func executeDnetCommand(t *testing.T, args []string, shouldSucced bool) { + _, w, _ := os.Pipe() + os.Stdout = w + + os.Args = args + err := dnetCommand(ioutil.Discard, ioutil.Discard) + if shouldSucced && err != nil { + os.Stdout = origStdOut + t.Fatalf("cli [%v] must succeed, but failed with an error : %v", args, err) + } else if !shouldSucced && err == nil { + os.Stdout = origStdOut + t.Fatalf("cli [%v] must fail, but succeeded with an error : %v", args, err) + } + os.Stdout = origStdOut +} diff --git a/cmd/dnet/flags.go b/cmd/dnet/flags.go new file mode 100644 index 00000000..27dfbd19 --- /dev/null +++ b/cmd/dnet/flags.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +type command struct { + name string + description string +} + +type byName []command + +var ( + flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode") + flHost = flag.String([]string{"H", "-host"}, "", "Daemon socket to connect to") + flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level") + flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode") + flCfgFile = flag.String([]string{"c", "-cfg-file"}, "/etc/default/libnetwork.toml", "Configuration file") + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") + + dnetCommands = []command{ + {"network", "Network management commands"}, + {"service", "Service management commands"}, + } +) + +func init() { + flag.Usage = func() { + fmt.Fprint(os.Stdout, "Usage: dnet [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for container networking.\n\nOptions:\n") + + flag.CommandLine.SetOutput(os.Stdout) + flag.PrintDefaults() + + help := "\nCommands:\n" + + for _, cmd := range dnetCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd.name, cmd.description) + } + + help += "\nRun 'dnet COMMAND --help' for more information on a command." + fmt.Fprintf(os.Stdout, "%s\n", help) + } +} + +func printUsage() { + fmt.Println("Usage: dnet COMMAND [arg...]") +} diff --git a/cmd/dnet/libnetwork.toml b/cmd/dnet/libnetwork.toml new file mode 100755 index 00000000..4e22516d --- /dev/null +++ b/cmd/dnet/libnetwork.toml @@ -0,0 +1,12 @@ +title = "LibNetwork Configuration file" + +[daemon] + debug = false +[cluster] + discovery = "token://22aa23948f4f6b31230687689636959e" + Address = "1.1.1.1" +[datastore] + embedded = false +[datastore.client] + provider = "consul" + Address = "localhost:8500" diff --git a/cmd/ovrouter/ovrouter.go b/cmd/ovrouter/ovrouter.go new file mode 100644 index 00000000..a75b5506 --- /dev/null +++ b/cmd/ovrouter/ovrouter.go @@ -0,0 +1,149 @@ +package main + +import ( + "fmt" + "net" + "os" + "os/signal" + + "github.com/docker/docker/pkg/reexec" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/drivers/overlay" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/types" + "github.com/vishvananda/netlink" +) + +type router struct { + d driverapi.Driver +} + +type endpoint struct { + addr net.IPNet + mac net.HardwareAddr + name string + id int +} + +func (r *router) RegisterDriver(name string, driver driverapi.Driver, c driverapi.Capability) error { + r.d = driver + return nil +} + +func (ep *endpoint) Interfaces() []driverapi.InterfaceInfo { + return nil +} + +func (ep *endpoint) AddInterface(ID int, mac net.HardwareAddr, ipv4 net.IPNet, ipv6 net.IPNet) error { + ep.id = ID + ep.addr = ipv4 + ep.mac = mac + return nil +} + +func (ep *endpoint) InterfaceNames() []driverapi.InterfaceNameInfo { + return []driverapi.InterfaceNameInfo{ep} + +} + +func (ep *endpoint) SetNames(srcName, dstPrefix string) error { + ep.name = srcName + return nil +} + +func (ep *endpoint) ID() int { + return ep.id +} + +func (ep *endpoint) SetGateway(net.IP) error { + return nil +} + +func (ep *endpoint) SetGatewayIPv6(net.IP) error { + return nil +} + +func (ep *endpoint) AddStaticRoute(destination *net.IPNet, routeType int, + nextHop net.IP, interfaceID int) error { + return nil +} + +func (ep *endpoint) SetHostsPath(string) error { + return nil +} + +func (ep *endpoint) SetResolvConfPath(string) error { + return nil +} + +func main() { + if reexec.Init() { + return + } + + r := &router{} + if err := overlay.Init(r); err != nil { + fmt.Printf("Failed to initialize overlay driver: %v\n", err) + os.Exit(1) + } + + opt := make(map[string]interface{}) + if len(os.Args) > 1 { + opt[netlabel.OverlayBindInterface] = os.Args[1] + } + if len(os.Args) > 2 { + opt[netlabel.OverlayNeighborIP] = os.Args[2] + } + if len(os.Args) > 3 { + opt[netlabel.KVProvider] = os.Args[3] + } + if len(os.Args) > 4 { + opt[netlabel.KVProviderURL] = os.Args[4] + } + + r.d.Config(opt) + + if err := r.d.CreateNetwork(types.UUID("testnetwork"), + map[string]interface{}{}); err != nil { + fmt.Printf("Failed to create network in the driver: %v\n", err) + os.Exit(1) + } + + ep := &endpoint{} + if err := r.d.CreateEndpoint(types.UUID("testnetwork"), types.UUID("testep"), + ep, map[string]interface{}{}); err != nil { + fmt.Printf("Failed to create endpoint in the driver: %v\n", err) + os.Exit(1) + } + + if err := r.d.Join(types.UUID("testnetwork"), types.UUID("testep"), + "", ep, map[string]interface{}{}); err != nil { + fmt.Printf("Failed to join an endpoint in the driver: %v\n", err) + os.Exit(1) + } + + link, err := netlink.LinkByName(ep.name) + if err != nil { + fmt.Printf("Failed to find the container interface with name %s: %v\n", + ep.name, err) + os.Exit(1) + } + + ipAddr := &netlink.Addr{IPNet: &ep.addr, Label: ""} + if err := netlink.AddrAdd(link, ipAddr); err != nil { + fmt.Printf("Failed to add address to the interface: %v\n", err) + os.Exit(1) + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, os.Kill) + + for { + select { + case <-sigCh: + r.d.Leave(types.UUID("testnetwork"), types.UUID("testep")) + overlay.Fini(r.d) + os.Exit(0) + } + } +} diff --git a/cmd/readme_test/readme.go b/cmd/readme_test/readme.go new file mode 100644 index 00000000..669b517f --- /dev/null +++ b/cmd/readme_test/readme.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/options" + "github.com/docker/libnetwork/types" +) + +func main() { + // Create a new controller instance + controller, err := libnetwork.New() + if err != nil { + return + } + + // Select and configure the network driver + networkType := "bridge" + + driverOptions := options.Generic{} + genericOption := make(map[string]interface{}) + genericOption[netlabel.GenericData] = driverOptions + err = controller.ConfigureNetworkDriver(networkType, genericOption) + if err != nil { + return + } + + // Create a network for containers to join. + // NewNetwork accepts Variadic optional arguments that libnetwork and Drivers can make of + network, err := controller.NewNetwork(networkType, "network1") + if err != nil { + return + } + + // For each new container: allocate IP and interfaces. The returned network + // settings will be used for container infos (inspect and such), as well as + // iptables rules for port publishing. This info is contained or accessible + // from the returned endpoint. + ep, err := network.CreateEndpoint("Endpoint1") + if err != nil { + return + } + + // A container can join the endpoint by providing the container ID to the join + // api. + // Join accepts Variadic arguments which will be made use of by libnetwork and Drivers + err = ep.Join("container1", + libnetwork.JoinOptionHostname("test"), + libnetwork.JoinOptionDomainname("docker.io")) + if err != nil { + return + } + + // libnetwork client can check the endpoint's operational data via the Info() API + epInfo, err := ep.DriverInfo() + mapData, ok := epInfo[netlabel.PortMap] + if ok { + portMapping, ok := mapData.([]types.PortBinding) + if ok { + fmt.Printf("Current port mapping for endpoint %s: %v", ep.Name(), portMapping) + } + } +} diff --git a/cmd/test/libnetwork.toml b/cmd/test/libnetwork.toml new file mode 100644 index 00000000..4e22516d --- /dev/null +++ b/cmd/test/libnetwork.toml @@ -0,0 +1,12 @@ +title = "LibNetwork Configuration file" + +[daemon] + debug = false +[cluster] + discovery = "token://22aa23948f4f6b31230687689636959e" + Address = "1.1.1.1" +[datastore] + embedded = false +[datastore.client] + provider = "consul" + Address = "localhost:8500" diff --git a/cmd/test/main.go b/cmd/test/main.go new file mode 100644 index 00000000..957a0cda --- /dev/null +++ b/cmd/test/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "net" + "time" + + log "github.com/Sirupsen/logrus" + + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/options" +) + +func main() { + log.SetLevel(log.DebugLevel) + controller, err := libnetwork.New() + if err != nil { + log.Fatal(err) + } + + netType := "null" + ip, net, _ := net.ParseCIDR("192.168.100.1/24") + net.IP = ip + options := options.Generic{"AddressIPv4": net} + + err = controller.ConfigureNetworkDriver(netType, options) + for i := 0; i < 10; i++ { + netw, err := controller.NewNetwork(netType, fmt.Sprintf("Gordon-%d", i)) + if err != nil { + if _, ok := err.(libnetwork.NetworkNameError); !ok { + log.Fatal(err) + } + } else { + fmt.Println("Network Created Successfully :", netw) + } + netw, _ = controller.NetworkByName(fmt.Sprintf("Gordon-%d", i)) + _, err = netw.CreateEndpoint(fmt.Sprintf("Gordon-Ep-%d", i), nil) + if err != nil { + log.Fatalf("Error creating endpoint 1 %v", err) + } + + _, err = netw.CreateEndpoint(fmt.Sprintf("Gordon-Ep2-%d", i), nil) + if err != nil { + log.Fatalf("Error creating endpoint 2 %v", err) + } + + time.Sleep(2 * time.Second) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..bb93d981 --- /dev/null +++ b/config/config.go @@ -0,0 +1,116 @@ +package config + +import ( + "strings" + + "github.com/BurntSushi/toml" + log "github.com/Sirupsen/logrus" + "github.com/docker/libnetwork/netlabel" +) + +// Config encapsulates configurations of various Libnetwork components +type Config struct { + Daemon DaemonCfg + Cluster ClusterCfg + Datastore DatastoreCfg +} + +// DaemonCfg represents libnetwork core configuration +type DaemonCfg struct { + Debug bool + DefaultNetwork string + DefaultDriver string + Labels []string +} + +// ClusterCfg represents cluster configuration +type ClusterCfg struct { + Discovery string + Address string + Heartbeat uint64 +} + +// DatastoreCfg represents Datastore configuration. +type DatastoreCfg struct { + Embedded bool + Client DatastoreClientCfg +} + +// DatastoreClientCfg represents Datastore Client-only mode configuration +type DatastoreClientCfg struct { + Provider string + Address string +} + +// ParseConfig parses the libnetwork configuration file +func ParseConfig(tomlCfgFile string) (*Config, error) { + var cfg Config + if _, err := toml.DecodeFile(tomlCfgFile, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +// Option is a option setter function type used to pass varios configurations +// to the controller +type Option func(c *Config) + +// OptionDefaultNetwork function returns an option setter for a default network +func OptionDefaultNetwork(dn string) Option { + return func(c *Config) { + log.Infof("Option DefaultNetwork: %s", dn) + c.Daemon.DefaultNetwork = strings.TrimSpace(dn) + } +} + +// OptionDefaultDriver function returns an option setter for default driver +func OptionDefaultDriver(dd string) Option { + return func(c *Config) { + log.Infof("Option DefaultDriver: %s", dd) + c.Daemon.DefaultDriver = strings.TrimSpace(dd) + } +} + +// OptionLabels function returns an option setter for labels +func OptionLabels(labels []string) Option { + return func(c *Config) { + for _, label := range labels { + if strings.HasPrefix(label, netlabel.Prefix) { + c.Daemon.Labels = append(c.Daemon.Labels, label) + } + } + } +} + +// OptionKVProvider function returns an option setter for kvstore provider +func OptionKVProvider(provider string) Option { + return func(c *Config) { + log.Infof("Option OptionKVProvider: %s", provider) + c.Datastore.Client.Provider = strings.TrimSpace(provider) + } +} + +// OptionKVProviderURL function returns an option setter for kvstore url +func OptionKVProviderURL(url string) Option { + return func(c *Config) { + log.Infof("Option OptionKVProviderURL: %s", url) + c.Datastore.Client.Address = strings.TrimSpace(url) + } +} + +// ProcessOptions processes options and stores it in config +func (c *Config) ProcessOptions(options ...Option) { + for _, opt := range options { + if opt != nil { + opt(c) + } + } +} + +// IsValidName validates configuration objects supported by libnetwork +func IsValidName(name string) bool { + if name == "" || strings.Contains(name, ".") { + return false + } + return true +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..cc8a911c --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "strings" + "testing" + + "github.com/docker/libnetwork/netlabel" + _ "github.com/docker/libnetwork/netutils" +) + +func TestInvalidConfig(t *testing.T) { + _, err := ParseConfig("invalid.toml") + if err == nil { + t.Fatal("Invalid Configuration file must fail") + } +} + +func TestConfig(t *testing.T) { + _, err := ParseConfig("libnetwork.toml") + if err != nil { + t.Fatal("Error parsing a valid configuration file :", err) + } +} + +func TestOptionsLabels(t *testing.T) { + c := &Config{} + l := []string{ + "com.docker.network.key1=value1", + "com.docker.storage.key1=value1", + "com.docker.network.driver.key1=value1", + "com.docker.network.driver.key2=value2", + } + f := OptionLabels(l) + f(c) + if len(c.Daemon.Labels) != 3 { + t.Fatalf("Expecting 3 labels, seen %d", len(c.Daemon.Labels)) + } + for _, l := range c.Daemon.Labels { + if !strings.HasPrefix(l, netlabel.Prefix) { + t.Fatalf("config must accept only libnetwork labels. Not : %s", l) + } + } +} + +func TestValidName(t *testing.T) { + if !IsValidName("test") { + t.Fatal("Name validation fails for a name that must be accepted") + } + if IsValidName("") { + t.Fatal("Name validation succeeds for a case when it is expected to fail") + } + if IsValidName("name.with.dots") { + t.Fatal("Name validation succeeds for a case when it is expected to fail") + } +} diff --git a/config/libnetwork.toml b/config/libnetwork.toml new file mode 100644 index 00000000..93a2ff47 --- /dev/null +++ b/config/libnetwork.toml @@ -0,0 +1,12 @@ +title = "LibNetwork Configuration file" + +[daemon] + debug = false +[cluster] + discovery = "token://swarm-discovery-token" + Address = "Cluster-wide reachable Host IP" +[datastore] + embedded = false +[datastore.client] + provider = "consul" + Address = "localhost:8500" diff --git a/controller.go b/controller.go new file mode 100644 index 00000000..02a9f7eb --- /dev/null +++ b/controller.go @@ -0,0 +1,399 @@ +/* +Package libnetwork provides the basic functionality and extension points to +create network namespaces and allocate interfaces for containers to use. + + // Create a new controller instance + controller, _err := libnetwork.New(nil) + + // Select and configure the network driver + networkType := "bridge" + + driverOptions := options.Generic{} + genericOption := make(map[string]interface{}) + genericOption[netlabel.GenericData] = driverOptions + err := controller.ConfigureNetworkDriver(networkType, genericOption) + if err != nil { + return + } + + // Create a network for containers to join. + // NewNetwork accepts Variadic optional arguments that libnetwork and Drivers can make of + network, err := controller.NewNetwork(networkType, "network1") + if err != nil { + return + } + + // For each new container: allocate IP and interfaces. The returned network + // settings will be used for container infos (inspect and such), as well as + // iptables rules for port publishing. This info is contained or accessible + // from the returned endpoint. + ep, err := network.CreateEndpoint("Endpoint1") + if err != nil { + return + } + + // A container can join the endpoint by providing the container ID to the join + // api. + // Join accepts Variadic arguments which will be made use of by libnetwork and Drivers + err = ep.Join("container1", + libnetwork.JoinOptionHostname("test"), + libnetwork.JoinOptionDomainname("docker.io")) + if err != nil { + return + } +*/ +package libnetwork + +import ( + "fmt" + "net" + "strings" + "sync" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/plugins" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/libnetwork/config" + "github.com/docker/libnetwork/datastore" + "github.com/docker/libnetwork/driverapi" + "github.com/docker/libnetwork/hostdiscovery" + "github.com/docker/libnetwork/netlabel" + "github.com/docker/libnetwork/sandbox" + "github.com/docker/libnetwork/types" +) + +// NetworkController provides the interface for controller instance which manages +// networks. +type NetworkController interface { + // ConfigureNetworkDriver applies the passed options to the driver instance for the specified network type + ConfigureNetworkDriver(networkType string, options map[string]interface{}) error + + // Config method returns the bootup configuration for the controller + Config() config.Config + + // Create a new network. The options parameter carries network specific options. + // Labels support will be added in the near future. + NewNetwork(networkType, name string, options ...NetworkOption) (Network, error) + + // Networks returns the list of Network(s) managed by this controller. + Networks() []Network + + // WalkNetworks uses the provided function to walk the Network(s) managed by this controller. + WalkNetworks(walker NetworkWalker) + + // NetworkByName returns the Network which has the passed name. If not found, the error ErrNoSuchNetwork is returned. + NetworkByName(name string) (Network, error) + + // NetworkByID returns the Network which has the passed id. If not found, the error ErrNoSuchNetwork is returned. + NetworkByID(id string) (Network, error) + + // LeaveAll accepts a container id and attempts to leave all endpoints that the container has joined + LeaveAll(id string) error + + // GC triggers immediate garbage collection of resources which are garbage collected. + GC() +} + +// NetworkWalker is a client provided function which will be used to walk the Networks. +// When the function returns true, the walk will stop. +type NetworkWalker func(nw Network) bool + +type driverData struct { + driver driverapi.Driver + capability driverapi.Capability +} + +type driverTable map[string]*driverData +type networkTable map[types.UUID]*network +type endpointTable map[types.UUID]*endpoint +type sandboxTable map[string]*sandboxData + +type controller struct { + networks networkTable + drivers driverTable + sandboxes sandboxTable + cfg *config.Config + store datastore.DataStore + sync.Mutex +} + +// New creates a new instance of network controller. +func New(cfgOptions ...config.Option) (NetworkController, error) { + var cfg *config.Config + if len(cfgOptions) > 0 { + cfg = &config.Config{} + cfg.ProcessOptions(cfgOptions...) + } + c := &controller{ + cfg: cfg, + networks: networkTable{}, + sandboxes: sandboxTable{}, + drivers: driverTable{}} + if err := initDrivers(c); err != nil { + return nil, err + } + + if cfg != nil { + if err := c.initDataStore(); err != nil { + // Failing to initalize datastore is a bad situation to be in. + // But it cannot fail creating the Controller + log.Debugf("Failed to Initialize Datastore due to %v. Operating in non-clustered mode", err) + } + if err := c.initDiscovery(); err != nil { + // Failing to initalize discovery is a bad situation to be in. + // But it cannot fail creating the Controller + log.Debugf("Failed to Initialize Discovery : %v", err) + } + } + + return c, nil +} + +func (c *controller) validateHostDiscoveryConfig() bool { + if c.cfg == nil || c.cfg.Cluster.Discovery == "" || c.cfg.Cluster.Address == "" { + return false + } + return true +} + +func (c *controller) initDiscovery() error { + if c.cfg == nil { + return fmt.Errorf("discovery initialization requires a valid configuration") + } + + hostDiscovery := hostdiscovery.NewHostDiscovery() + return hostDiscovery.StartDiscovery(&c.cfg.Cluster, c.hostJoinCallback, c.hostLeaveCallback) +} + +func (c *controller) hostJoinCallback(hosts []net.IP) { +} + +func (c *controller) hostLeaveCallback(hosts []net.IP) { +} + +func (c *controller) Config() config.Config { + c.Lock() + defer c.Unlock() + if c.cfg == nil { + return config.Config{} + } + return *c.cfg +} + +func (c *controller) ConfigureNetworkDriver(networkType string, options map[string]interface{}) error { + c.Lock() + dd, ok := c.drivers[networkType] + c.Unlock() + if !ok { + return NetworkTypeError(networkType) + } + return dd.driver.Config(options) +} + +func (c *controller) RegisterDriver(networkType string, driver driverapi.Driver, capability driverapi.Capability) error { + c.Lock() + if !config.IsValidName(networkType) { + c.Unlock() + return ErrInvalidName(networkType) + } + if _, ok := c.drivers[networkType]; ok { + c.Unlock() + return driverapi.ErrActiveRegistration(networkType) + } + c.drivers[networkType] = &driverData{driver, capability} + + if c.cfg == nil { + c.Unlock() + return nil + } + + opt := make(map[string]interface{}) + for _, label := range c.cfg.Daemon.Labels { + if strings.HasPrefix(label, netlabel.DriverPrefix+"."+networkType) { + opt[netlabel.Key(label)] = netlabel.Value(label) + } + } + + if capability.Scope == driverapi.GlobalScope && c.validateDatastoreConfig() { + opt[netlabel.KVProvider] = c.cfg.Datastore.Client.Provider + opt[netlabel.KVProviderURL] = c.cfg.Datastore.Client.Address + } + + c.Unlock() + + if len(opt) != 0 { + if err := driver.Config(opt); err != nil { + return err + } + } + + return nil +} + +// NewNetwork creates a new network of the specified network type. The options +// are network specific and modeled in a generic way. +func (c *controller) NewNetwork(networkType, name string, options ...NetworkOption) (Network, error) { + if !config.IsValidName(name) { + return nil, ErrInvalidName(name) + } + // Check if a network already exists with the specified network name + c.Lock() + for _, n := range c.networks { + if n.name == name { + c.Unlock() + return nil, NetworkNameError(name) + } + } + c.Unlock() + + // Construct the network object + network := &network{ + name: name, + networkType: networkType, + id: types.UUID(stringid.GenerateRandomID()), + ctrlr: c, + endpoints: endpointTable{}, + } + + network.processOptions(options...) + + if err := c.addNetwork(network); err != nil { + return nil, err + } + + if err := c.updateNetworkToStore(network); err != nil { + log.Warnf("couldnt create network %s: %v", network.name, err) + if e := network.Delete(); e != nil { + log.Warnf("couldnt cleanup network %s: %v", network.name, err) + } + return nil, err + } + + return network, nil +} + +func (c *controller) addNetwork(n *network) error { + + c.Lock() + // Check if a driver for the specified network type is available + dd, ok := c.drivers[n.networkType] + c.Unlock() + + if !ok { + var err error + dd, err = c.loadDriver(n.networkType) + if err != nil { + return err + } + } + + n.Lock() + n.svcRecords = svcMap{} + n.driver = dd.driver + d := n.driver + n.Unlock() + + // Create the network + if err := d.CreateNetwork(n.id, n.generic); err != nil { + return err + } + if err := n.watchEndpoints(); err != nil { + return err + } + c.Lock() + c.networks[n.id] = n + c.Unlock() + + return nil +} + +func (c *controller) Networks() []Network { + c.Lock() + defer c.Unlock() + + list := make([]Network, 0, len(c.networks)) + for _, n := range c.networks { + list = append(list, n) + } + + return list +} + +func (c *controller) WalkNetworks(walker NetworkWalker) { + for _, n := range c.Networks() { + if walker(n) { + return + } + } +} + +func (c *controller) NetworkByName(name string) (Network, error) { + if name == "" { + return nil, ErrInvalidName(name) + } + var n Network + + s := func(current Network) bool { + if current.Name() == name { + n = current + return true + } + return false + } + + c.WalkNetworks(s) + + if n == nil { + return nil, ErrNoSuchNetwork(name) + } + + return n, nil +} + +func (c *controller) NetworkByID(id string) (Network, error) { + if id == "" { + return nil, ErrInvalidID(id) + } + c.Lock() + defer c.Unlock() + if n, ok := c.networks[types.UUID(id)]; ok { + return n, nil + } + return nil, ErrNoSuchNetwork(id) +} + +func (c *controller) loadDriver(networkType string) (*driverData, error) { + // Plugins pkg performs lazy loading of plugins that acts as remote drivers. + // As per the design, this Get call will result in remote driver discovery if there is a corresponding plugin available. + _, err := plugins.Get(networkType, driverapi.NetworkPluginEndpointType) + if err != nil { + if err == plugins.ErrNotFound { + return nil, types.NotFoundErrorf(err.Error()) + } + return nil, err + } + c.Lock() + defer c.Unlock() + dd, ok := c.drivers[networkType] + if !ok { + return nil, ErrInvalidNetworkDriver(networkType) + } + return dd, nil +} + +func (c *controller) isDriverGlobalScoped(networkType string) (bool, error) { + c.Lock() + dd, ok := c.drivers[networkType] + c.Unlock() + if !ok { + return false, types.NotFoundErrorf("driver not found for %s", networkType) + } + if dd.capability.Scope == driverapi.GlobalScope { + return true, nil + } + return false, nil +} + +func (c *controller) GC() { + sandbox.GC() +} diff --git a/datastore/datastore.go b/datastore/datastore.go new file mode 100644 index 00000000..8a195aa9 --- /dev/null +++ b/datastore/datastore.go @@ -0,0 +1,194 @@ +package datastore + +import ( + "reflect" + "strings" + + "github.com/docker/libkv" + "github.com/docker/libkv/store" + "github.com/docker/libnetwork/config" + "github.com/docker/libnetwork/types" +) + +//DataStore exported +type DataStore interface { + // GetObject gets data from datastore and unmarshals to the specified object + GetObject(key string, o KV) error + // PutObject adds a new Record based on an object into the datastore + PutObject(kvObject KV) error + // PutObjectAtomic provides an atomic add and update operation for a Record + PutObjectAtomic(kvObject KV) error + // DeleteObject deletes a record + DeleteObject(kvObject KV) error + // DeleteObjectAtomic performs an atomic delete operation + DeleteObjectAtomic(kvObject KV) error + // DeleteTree deletes a record + DeleteTree(kvObject KV) error + // KVStore returns access to the KV Store + KVStore() store.Store +} + +// ErrKeyModified is raised for an atomic update when the update is working on a stale state +var ( + ErrKeyModified = store.ErrKeyModified + ErrKeyNotFound = store.ErrKeyNotFound +) + +type datastore struct { + store store.Store +} + +//KV Key Value interface used by objects to be part of the DataStore +type KV interface { + // Key method lets an object to provide the Key to be used in KV Store + Key() []string + // KeyPrefix method lets an object to return immediate parent key that can be used for tree walk + KeyPrefix() []string + // Value method lets an object to marshal its content to be stored in the KV store + Value() []byte + // SetValue is used by the datastore to set the object's value when loaded from the data store. + SetValue([]byte) error + // Index method returns the latest DB Index as seen by the object + Index() uint64 + // SetIndex method allows the datastore to store the latest DB Index into the object + SetIndex(uint64) + // True if the object exists in the datastore, false if it hasn't been stored yet. + // When SetIndex() is called, the object has been stored. + Exists() bool +} + +const ( + // NetworkKeyPrefix is the prefix for network key in the kv store + NetworkKeyPrefix = "network" + // EndpointKeyPrefix is the prefix for endpoint key in the kv store + EndpointKeyPrefix = "endpoint" +) + +var rootChain = []string{"docker", "libnetwork"} + +//Key provides convenient method to create a Key +func Key(key ...string) string { + keychain := append(rootChain, key...) + str := strings.Join(keychain, "/") + return str + "/" +} + +//ParseKey provides convenient method to unpack the key to complement the Key function +func ParseKey(key string) ([]string, error) { + chain := strings.Split(strings.Trim(key, "/"), "/") + + // The key must atleast be equal to the rootChain in order to be considered as valid + if len(chain) <= len(rootChain) || !reflect.DeepEqual(chain[0:len(rootChain)], rootChain) { + return nil, types.BadRequestErrorf("invalid Key : %s", key) + } + return chain[len(rootChain):], nil +} + +// newClient used to connect to KV Store +func newClient(kv string, addrs string) (DataStore, error) { + store, err := libkv.NewStore(store.Backend(kv), []string{addrs}, &store.Config{}) + if err != nil { + return nil, err + } + ds := &datastore{store: store} + return ds, nil +} + +// NewDataStore creates a new instance of LibKV data store +func NewDataStore(cfg *config.DatastoreCfg) (DataStore, error) { + if cfg == nil { + return nil, types.BadRequestErrorf("invalid configuration passed to datastore") + } + // TODO : cfg.Embedded case + return newClient(cfg.Client.Provider, cfg.Client.Address) +} + +// NewCustomDataStore can be used by clients to plugin cusom datatore that adhers to store.Store +func NewCustomDataStore(customStore store.Store) DataStore { + return &datastore{store: customStore} +} + +func (ds *datastore) KVStore() store.Store { + return ds.store +} + +// PutObjectAtomic adds a new Record based on an object into the datastore +func (ds *datastore) PutObjectAtomic(kvObject KV) error { + if kvObject == nil { + return types.BadRequestErrorf("invalid KV Object : nil") + } + kvObjValue := kvObject.Value() + + if kvObjValue == nil { + return types.BadRequestErrorf("invalid KV Object with a nil Value for key %s", Key(kvObject.Key()...)) + } + + var previous *store.KVPair + if kvObject.Exists() { + previous = &store.KVPair{Key: Key(kvObject.Key()...), LastIndex: kvObject.Index()} + } else { + previous = nil + } + _, pair, err := ds.store.AtomicPut(Key(kvObject.Key()...), kvObjValue, previous, nil) + if err != nil { + return err + } + + kvObject.SetIndex(pair.LastIndex) + return nil +} + +// PutObject adds a new Record based on an object into the datastore +func (ds *datastore) PutObject(kvObject KV) error { + if kvObject == nil { + return types.BadRequestErrorf("invalid KV Object : nil") + } + return ds.putObjectWithKey(kvObject, kvObject.Key()...) +} + +func (ds *datastore) putObjectWithKey(kvObject KV, key ...string) error { + kvObjValue := kvObject.Value() + + if kvObjValue == nil { + return types.BadRequestErrorf("invalid KV Object with a nil Value for key %s", Key(kvObject.Key()...)) + } + return ds.store.Put(Key(key...), kvObjValue, nil) +} + +// GetObject returns a record matching the key +func (ds *datastore) GetObject(key string, o KV) error { + kvPair, err := ds.store.Get(key) + if err != nil { + return err + } + err = o.SetValue(kvPair.Value) + if err != nil { + return err + } + + // Make sure the object has a correct view of the DB index in case we need to modify it + // and update the DB. + o.SetIndex(kvPair.LastIndex) + return nil +} + +// DeleteObject unconditionally deletes a record from the store +func (ds *datastore) DeleteObject(kvObject KV) error { + return ds.store.Delete(Key(kvObject.Key()...)) +} + +// DeleteObjectAtomic performs atomic delete on a record +func (ds *datastore) DeleteObjectAtomic(kvObject KV) error { + if kvObject == nil { + return types.BadRequestErrorf("invalid KV Object : nil") + } + + previous := &store.KVPair{Key: Key(kvObject.Key()...), LastIndex: kvObject.Index()} + _, err := ds.store.AtomicDelete(Key(kvObject.Key()...), previous) + return err +} + +// DeleteTree unconditionally deletes a record from the store +func (ds *datastore) DeleteTree(kvObject KV) error { + return ds.store.DeleteTree(Key(kvObject.KeyPrefix()...)) +} diff --git a/datastore/datastore_test.go b/datastore/datastore_test.go new file mode 100644 index 00000000..397842db --- /dev/null +++ b/datastore/datastore_test.go @@ -0,0 +1,241 @@ +package datastore + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/docker/libnetwork/config" + _ "github.com/docker/libnetwork/netutils" + "github.com/docker/libnetwork/options" + "github.com/stretchr/testify/assert" +) + +var dummyKey = "dummy" + +// NewCustomDataStore can be used by other Tests in order to use custom datastore +func NewTestDataStore() DataStore { + return &datastore{store: NewMockStore()} +} + +func TestKey(t *testing.T) { + eKey := []string{"hello", "world"} + sKey := Key(eKey...) + if sKey != "docker/libnetwork/hello/world/" { + t.Fatalf("unexpected key : %s", sKey) + } +} + +func TestParseKey(t *testing.T) { + keySlice, err := ParseKey("/docker/libnetwork/hello/world/") + if err != nil { + t.Fatal(err) + } + eKey := []string{"hello", "world"} + if len(keySlice) < 2 || !reflect.DeepEqual(eKey, keySlice) { + t.Fatalf("unexpected unkey : %s", keySlice) + } +} + +func TestInvalidDataStore(t *testing.T) { + config := &config.DatastoreCfg{} + config.Embedded = false + config.Client.Provider = "invalid" + config.Client.Address = "localhost:8500" + _, err := NewDataStore(config) + if err == nil { + t.Fatal("Invalid Datastore connection configuration must result in a failure") + } +} + +func TestKVObjectFlatKey(t *testing.T) { + store := NewTestDataStore() + expected := dummyKVObject("1000", true) + err := store.PutObject(expected) + if err != nil { + t.Fatal(err) + } + keychain := []string{dummyKey, "1000"} + data, err := store.KVStore().Get(Key(keychain...)) + if err != nil { + t.Fatal(err) + } + var n dummyObject + json.Unmarshal(data.Value, &n) + if n.Name != expected.Name { + t.Fatalf("Dummy object doesn't match the expected object") + } +} + +func TestAtomicKVObjectFlatKey(t *testing.T) { + store := NewTestDataStore() + expected := dummyKVObject("1111", true) + assert.False(t, expected.Exists()) + err := store.PutObjectAtomic(expected) + if err != nil { + t.Fatal(err) + } + assert.True(t, expected.Exists()) + + // PutObjectAtomic automatically sets the Index again. Hence the following must pass. + + err = store.PutObjectAtomic(expected) + if err != nil { + t.Fatal("Atomic update should succeed.") + } + + // Get the latest index and try PutObjectAtomic again for the same Key + // This must succeed as well + data, err := store.KVStore().Get(Key(expected.Key()...)) + if err != nil { + t.Fatal(err) + } + n := dummyObject{} + json.Unmarshal(data.Value, &n) + n.ID = "1111" + n.SetIndex(data.LastIndex) + n.ReturnValue = true + err = store.PutObjectAtomic(&n) + if err != nil { + t.Fatal(err) + } + + // Get the Object using GetObject, then set again. + newObj := dummyObject{} + err = store.GetObject(Key(expected.Key()...), &newObj) + assert.True(t, newObj.Exists()) + err = store.PutObjectAtomic(&n) + if err != nil { + t.Fatal(err) + } + +} + +// dummy data used to test the datastore +type dummyObject struct { + Name string `kv:"leaf"` + NetworkType string `kv:"leaf"` + EnableIPv6 bool `kv:"leaf"` + Rec *recStruct `kv:"recursive"` + Dict map[string]*recStruct `kv:"iterative"` + Generic options.Generic `kv:"iterative"` + ID string + DBIndex uint64 + DBExists bool + ReturnValue bool +} + +func (n *dummyObject) Key() []string { + return []string{dummyKey, n.ID} +} + +func (n *dummyObject) KeyPrefix() []string { + return []string{dummyKey} +} + +func (n *dummyObject) Value() []byte { + if !n.ReturnValue { + return nil + } + + b, err := json.Marshal(n) + if err != nil { + return nil + } + return b +} + +func (n *dummyObject) SetValue(value []byte) error { + return json.Unmarshal(value, n) +} + +func (n *dummyObject) Index() uint64 { + return n.DBIndex +} + +func (n *dummyObject) SetIndex(index uint64) { + n.DBIndex = index + n.DBExists = true +} + +func (n *dummyObject) Exists() bool { + return n.DBExists +} + +func (n *dummyObject) MarshalJSON() ([]byte, error) { + netMap := make(map[string]interface{}) + netMap["name"] = n.Name + netMap["networkType"] = n.NetworkType + netMap["enableIPv6"] = n.EnableIPv6 + netMap["generic"] = n.Generic + return json.Marshal(netMap) +} + +func (n *dummyObject) UnmarshalJSON(b []byte) (err error) { + var netMap map[string]interface{} + if err := json.Unmarshal(b, &netMap); err != nil { + return err + } + n.Name = netMap["name"].(string) + n.NetworkType = netMap["networkType"].(string) + n.EnableIPv6 = netMap["enableIPv6"].(bool) + n.Generic = netMap["generic"].(map[string]interface{}) + return nil +} + +// dummy structure to test "recursive" cases +type recStruct struct { + Name string `kv:"leaf"` + Field1 int `kv:"leaf"` + Dict map[string]string `kv:"iterative"` + DBIndex uint64 + DBExists bool +} + +func (r *recStruct) Key() []string { + return []string{"recStruct"} +} +func (r *recStruct) Value() []byte { + b, err := json.Marshal(r) + if err != nil { + return nil + } + return b +} + +func (r *recStruct) SetValue(value []byte) error { + return json.Unmarshal(value, r) +} + +func (r *recStruct) Index() uint64 { + return r.DBIndex +} + +func (r *recStruct) SetIndex(index uint64) { + r.DBIndex = index + r.DBExists = true +} + +func (r *recStruct) Exists() bool { + return r.DBExists +} + +func dummyKVObject(id string, retValue bool) *dummyObject { + cDict := make(map[string]string) + cDict["foo"] = "bar" + cDict["hello"] = "world" + n := dummyObject{ + Name: "testNw", + NetworkType: "bridge", + EnableIPv6: true, + Rec: &recStruct{"gen", 5, cDict, 0, false}, + ID: id, + DBIndex: 0, + ReturnValue: retValue, + DBExists: false} + generic := make(map[string]interface{}) + generic["label1"] = &recStruct{"value1", 1, cDict, 0, false} + generic["label2"] = "subnet=10.1.1.0/16" + n.Generic = generic + return &n +} diff --git a/datastore/mock_store.go b/datastore/mock_store.go new file mode 100644 index 00000000..0817339b --- /dev/null +++ b/datastore/mock_store.go @@ -0,0 +1,129 @@ +package datastore + +import ( + "errors" + + "github.com/docker/libkv/store" + "github.com/docker/libnetwork/types" +) + +var ( + // ErrNotImplmented exported + ErrNotImplmented = errors.New("Functionality not implemented") +) + +// MockData exported +type MockData struct { + Data []byte + Index uint64 +} + +// MockStore exported +type MockStore struct { + db map[string]*MockData +} + +// NewMockStore creates a Map backed Datastore that is useful for mocking +func NewMockStore() *MockStore { + db := make(map[string]*MockData) + return &MockStore{db} +} + +// Get the value at "key", returns the last modified index +// to use in conjunction to CAS calls +func (s *MockStore) Get(key string) (*store.KVPair, error) { + mData := s.db[key] + if mData == nil { + return nil, nil + } + return &store.KVPair{Value: mData.Data, LastIndex: mData.Index}, nil + +} + +// Put a value at "key" +func (s *MockStore) Put(key string, value []byte, options *store.WriteOptions) error { + mData := s.db[key] + if mData == nil { + mData = &MockData{value, 0} + } + mData.Index = mData.Index + 1 + s.db[key] = mData + return nil +} + +// Delete a value at "key" +func (s *MockStore) Delete(key string) error { + delete(s.db, key) + return nil +} + +// Exists checks that the key exists inside the store +func (s *MockStore) Exists(key string) (bool, error) { + _, ok := s.db[key] + return ok, nil +} + +// List gets a range of values at "directory" +func (s *MockStore) List(prefix string) ([]*store.KVPair, error) { + return nil, ErrNotImplmented +} + +// DeleteTree deletes a range of values at "directory" +func (s *MockStore) DeleteTree(prefix string) error { + delete(s.db, prefix) + return nil +} + +// Watch a single key for modifications +func (s *MockStore) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) { + return nil, ErrNotImplmented +} + +// WatchTree triggers a watch on a range of values at "directory" +func (s *MockStore) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) { + return nil, ErrNotImplmented +} + +// NewLock exposed +func (s *MockStore) NewLock(key string, options *store.LockOptions) (store.Locker, error) { + return nil, ErrNotImplmented +} + +// AtomicPut put a value at "key" if the key has not been +// modified in the meantime, throws an error if this is the case +func (s *MockStore) AtomicPut(key string, newValue []byte, previous *store.KVPair, options *store.WriteOptions) (bool, *store.KVPair, error) { + mData := s.db[key] + + if previous == nil { + if mData != nil { + return false, nil, types.BadRequestErrorf("atomic put failed because key exists") + } // Else OK. + } else { + if mData == nil { + return false, nil, types.BadRequestErrorf("atomic put failed because key exists") + } + if mData != nil && mData.Index != previous.LastIndex { + return false, nil, types.BadRequestErrorf("atomic put failed due to mismatched Index") + } // Else OK. + } + err := s.Put(key, newValue, nil) + if err != nil { + return false, nil, err + } + return true, &store.KVPair{Key: key, Value: newValue, LastIndex: s.db[key].Index}, nil +} + +// AtomicDelete deletes a value at "key" if the key has not +// been modified in the meantime, throws an error if this is the case +func (s *MockStore) AtomicDelete(key string, previous *store.KVPair) (bool, error) { + mData := s.db[key] + if mData != nil && mData.Index != previous.LastIndex { + return false, types.BadRequestErrorf("atomic delete failed due to mismatched Index") + } + return true, s.Delete(key) +} + +// Close closes the client connection +func (s *MockStore) Close() { + return +} diff --git a/docs/Vagrantfile b/docs/Vagrantfile new file mode 100644 index 00000000..8f1f8e1c --- /dev/null +++ b/docs/Vagrantfile @@ -0,0 +1,57 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +$consul=<