mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-19 07:48:08 +00:00
Merge pull request #1028 from balena-io/seperate-volumes
Use instantiations of compose objects throughout application-manager
This commit is contained in:
89
package-lock.json
generated
89
package-lock.json
generated
@ -1222,7 +1222,7 @@
|
|||||||
"fetch-ponyfill": "^4.0.0",
|
"fetch-ponyfill": "^4.0.0",
|
||||||
"fetch-readablestream": "^0.2.0",
|
"fetch-readablestream": "^0.2.0",
|
||||||
"lodash": "^4.6.1",
|
"lodash": "^4.6.1",
|
||||||
"node-web-streams": "github:resin-io-modules/node-web-streams#emit-errors",
|
"node-web-streams": "github:resin-io-modules/node-web-streams#46f98300b69090bde3f6b4983877ccfe283a892c",
|
||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
"qs": "^6.3.0",
|
"qs": "^6.3.0",
|
||||||
"rindle": "^1.3.1"
|
"rindle": "^1.3.1"
|
||||||
@ -1358,7 +1358,7 @@
|
|||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"resin-cli-form": "^1.4.1",
|
"resin-cli-form": "^1.4.1",
|
||||||
"resin-cli-visuals": "^1.2.8",
|
"resin-cli-visuals": "^1.2.8",
|
||||||
"resin-discoverable-services": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#find-on-all-interfaces",
|
"resin-discoverable-services": "git+https://github.com/resin-io-modules/resin-discoverable-services.git#afca9e4700ec5ef82aa897f14bd5a46f06518061",
|
||||||
"resin-semver": "^1.4.0",
|
"resin-semver": "^1.4.0",
|
||||||
"revalidator": "^0.3.1",
|
"revalidator": "^0.3.1",
|
||||||
"rindle": "^1.3.0",
|
"rindle": "^1.3.0",
|
||||||
@ -1609,7 +1609,7 @@
|
|||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dns-equal": "^1.0.0",
|
"dns-equal": "^1.0.0",
|
||||||
"dns-txt": "^2.0.2",
|
"dns-txt": "^2.0.2",
|
||||||
"multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces",
|
"multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc",
|
||||||
"multicast-dns-service-types": "^1.1.0"
|
"multicast-dns-service-types": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1811,7 +1811,7 @@
|
|||||||
},
|
},
|
||||||
"cacache": {
|
"cacache": {
|
||||||
"version": "10.0.4",
|
"version": "10.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
|
"resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
|
||||||
"integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
|
"integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -2499,7 +2499,7 @@
|
|||||||
},
|
},
|
||||||
"dbus-native": {
|
"dbus-native": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/dbus-native/-/dbus-native-0.2.5.tgz",
|
"resolved": "http://registry.npmjs.org/dbus-native/-/dbus-native-0.2.5.tgz",
|
||||||
"integrity": "sha512-ocxMKCV7QdiNhzhFSeEMhj258OGtvpANSb3oWGiotmI5h1ZIse0TMPcSLiXSpqvbYvQz2Y5RsYPMNYLWhg9eBw==",
|
"integrity": "sha512-ocxMKCV7QdiNhzhFSeEMhj258OGtvpANSb3oWGiotmI5h1ZIse0TMPcSLiXSpqvbYvQz2Y5RsYPMNYLWhg9eBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -2655,7 +2655,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globby": {
|
"globby": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
|
"resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
|
||||||
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
|
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -3025,7 +3025,7 @@
|
|||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "0.10.31",
|
"version": "0.10.31",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
|
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@ -4049,8 +4049,7 @@
|
|||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -4210,7 +4209,6 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@ -4229,7 +4227,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@ -4323,7 +4320,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@ -4409,8 +4405,7 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -4446,7 +4441,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@ -4510,14 +4504,12 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -6638,9 +6630,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"livepush": {
|
"livepush": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/livepush/-/livepush-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/livepush/-/livepush-2.0.2.tgz",
|
||||||
"integrity": "sha512-0UWr6T/AR4NpkcdStfOs1Ii3K2yBoX5Ipo25b56Xfuj/ytyNgByd+UUk2SB0uZEHj/QONwgbhmE64mE3oYFoOw==",
|
"integrity": "sha512-esIy95BpYr5EUXtJhntu6lrRZEF7rhSebmbQnnSrouQAFIvIhk7USOEky2VOXs49ot9/KG5grxtWoiTyUlg8bg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bluebird": "^3.5.1",
|
"bluebird": "^3.5.1",
|
||||||
@ -7454,7 +7446,7 @@
|
|||||||
},
|
},
|
||||||
"next-tick": {
|
"next-tick": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
"resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||||
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
|
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@ -7843,7 +7835,7 @@
|
|||||||
},
|
},
|
||||||
"os-homedir": {
|
"os-homedir": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
"resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||||
},
|
},
|
||||||
"os-locale": {
|
"os-locale": {
|
||||||
@ -7893,7 +7885,7 @@
|
|||||||
},
|
},
|
||||||
"os-tmpdir": {
|
"os-tmpdir": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
"resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||||
},
|
},
|
||||||
"osenv": {
|
"osenv": {
|
||||||
@ -8048,7 +8040,7 @@
|
|||||||
},
|
},
|
||||||
"path-is-absolute": {
|
"path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
},
|
},
|
||||||
"path-is-inside": {
|
"path-is-inside": {
|
||||||
@ -8678,7 +8670,7 @@
|
|||||||
},
|
},
|
||||||
"require-npm4-to-publish": {
|
"require-npm4-to-publish": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "http://registry.npmjs.org/require-npm4-to-publish/-/require-npm4-to-publish-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-npm4-to-publish/-/require-npm4-to-publish-1.0.0.tgz",
|
||||||
"integrity": "sha1-5Z7D5ikQFT3Fu90MpA20IrLE2ec=",
|
"integrity": "sha1-5Z7D5ikQFT3Fu90MpA20IrLE2ec=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -8785,7 +8777,7 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bluebird": "^3.0.0",
|
"bluebird": "^3.0.0",
|
||||||
"bonjour": "git+https://github.com/resin-io/bonjour.git#fixed-mdns",
|
"bonjour": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c",
|
||||||
"ip": "^1.1.4",
|
"ip": "^1.1.4",
|
||||||
"lodash": "^4.17.4"
|
"lodash": "^4.17.4"
|
||||||
}
|
}
|
||||||
@ -9444,7 +9436,7 @@
|
|||||||
},
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
|
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
|
||||||
"integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
|
"integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@ -9669,7 +9661,7 @@
|
|||||||
},
|
},
|
||||||
"stream-browserify": {
|
"stream-browserify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
"resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
||||||
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
|
"integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -9765,7 +9757,7 @@
|
|||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
@ -10084,7 +10076,7 @@
|
|||||||
},
|
},
|
||||||
"thunky": {
|
"thunky": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz",
|
||||||
"integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=",
|
"integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@ -10262,9 +10254,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"version": "8.2.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz",
|
||||||
"integrity": "sha512-m8XQwUurkbYqXrKqr3WHCW310utRNvV5OnRVeISeea7LoCWVcdfeB/Ntl8JYWFh+WRoUAdBgESrzKochQt7sMw==",
|
"integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"arg": "^4.1.0",
|
"arg": "^4.1.0",
|
||||||
@ -10797,8 +10789,7 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@ -10841,8 +10832,7 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@ -10853,8 +10843,7 @@
|
|||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -10971,8 +10960,7 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@ -10984,7 +10972,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -11014,7 +11001,6 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@ -11033,7 +11019,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@ -11127,7 +11112,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@ -11213,8 +11197,7 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -11250,7 +11233,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@ -11270,7 +11252,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -11314,14 +11295,12 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"test": "npm run lint && npm run test:build && JUNIT_REPORT_PATH=report.xml istanbul cover _mocha && npm run coverage",
|
"test": "npm run lint && npm run test:build && JUNIT_REPORT_PATH=report.xml istanbul cover _mocha && npm run coverage",
|
||||||
"test:build": "npm run typescript:test-build && npm run coffeescript:test && npm run testitems:copy && npm run migrations:copy-test && npm run packagejson:copy",
|
"test:build": "npm run typescript:test-build && npm run coffeescript:test && npm run testitems:copy && npm run migrations:copy-test && npm run packagejson:copy",
|
||||||
"coverage": "istanbul report text && istanbul report html",
|
"coverage": "istanbul report text && istanbul report html",
|
||||||
"test:fast": "npm run test:build && mocha",
|
"test:fast": "mocha --opts test/fast-mocha.opts",
|
||||||
"test:debug": "npm run test:build && mocha --inspect-brk",
|
"test:debug": "npm run test:build && mocha --inspect-brk",
|
||||||
"prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"{src,test,typings}/**/*.ts\"",
|
"prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"{src,test,typings}/**/*.ts\"",
|
||||||
"typescript:test-build": "tsc --project tsconfig.json",
|
"typescript:test-build": "tsc --project tsconfig.json",
|
||||||
@ -85,7 +85,7 @@
|
|||||||
"json-mask": "^0.3.8",
|
"json-mask": "^0.3.8",
|
||||||
"knex": "~0.15.2",
|
"knex": "~0.15.2",
|
||||||
"lint-staged": "^8.1.0",
|
"lint-staged": "^8.1.0",
|
||||||
"livepush": "^2.0.1",
|
"livepush": "^2.0.2",
|
||||||
"lockfile": "^1.0.1",
|
"lockfile": "^1.0.1",
|
||||||
"lodash": "^4.17.5",
|
"lodash": "^4.17.5",
|
||||||
"log-timestamp": "^0.1.2",
|
"log-timestamp": "^0.1.2",
|
||||||
@ -112,6 +112,7 @@
|
|||||||
"terser": "^3.14.1",
|
"terser": "^3.14.1",
|
||||||
"tmp": "^0.1.0",
|
"tmp": "^0.1.0",
|
||||||
"ts-loader": "^5.3.0",
|
"ts-loader": "^5.3.0",
|
||||||
|
"ts-node": "^8.3.0",
|
||||||
"typed-error": "^2.0.0",
|
"typed-error": "^2.0.0",
|
||||||
"typescript": "^3.5.1",
|
"typescript": "^3.5.1",
|
||||||
"webpack": "^4.25.0",
|
"webpack": "^4.25.0",
|
||||||
|
@ -7,6 +7,7 @@ fs = Promise.promisifyAll(require('fs'))
|
|||||||
path = require 'path'
|
path = require 'path'
|
||||||
|
|
||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
|
{ log } = require './lib/supervisor-console'
|
||||||
|
|
||||||
{ DockerUtils: Docker } = require './lib/docker-utils'
|
{ DockerUtils: Docker } = require './lib/docker-utils'
|
||||||
{ LocalModeManager } = require './local-mode'
|
{ LocalModeManager } = require './local-mode'
|
||||||
@ -20,7 +21,8 @@ updateLock = require './lib/update-lock'
|
|||||||
{ Images } = require './compose/images'
|
{ Images } = require './compose/images'
|
||||||
{ NetworkManager } = require './compose/network-manager'
|
{ NetworkManager } = require './compose/network-manager'
|
||||||
{ Network } = require './compose/network'
|
{ Network } = require './compose/network'
|
||||||
{ Volumes } = require './compose/volumes'
|
{ VolumeManager } = require './compose/volume-manager'
|
||||||
|
{ Volume } = require './compose/volume'
|
||||||
|
|
||||||
Proxyvisor = require './proxyvisor'
|
Proxyvisor = require './proxyvisor'
|
||||||
|
|
||||||
@ -68,7 +70,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@images = new Images({ @docker, @logger, @db, @config })
|
@images = new Images({ @docker, @logger, @db, @config })
|
||||||
@services = new ServiceManager({ @docker, @logger, @images, @config })
|
@services = new ServiceManager({ @docker, @logger, @images, @config })
|
||||||
@networks = new NetworkManager({ @docker, @logger })
|
@networks = new NetworkManager({ @docker, @logger })
|
||||||
@volumes = new Volumes({ @docker, @logger })
|
@volumes = new VolumeManager({ @docker, @logger })
|
||||||
@proxyvisor = new Proxyvisor({ @config, @logger, @db, @docker, @images, applications: this })
|
@proxyvisor = new Proxyvisor({ @config, @logger, @db, @docker, @images, applications: this })
|
||||||
@localModeManager = new LocalModeManager(@config, @docker, @logger, @db)
|
@localModeManager = new LocalModeManager(@config, @docker, @logger, @db)
|
||||||
@timeSpentFetching = 0
|
@timeSpentFetching = 0
|
||||||
@ -151,22 +153,12 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@images.cleanup()
|
@images.cleanup()
|
||||||
createNetworkOrVolume: (step) =>
|
createNetworkOrVolume: (step) =>
|
||||||
if step.model is 'network'
|
if step.model is 'network'
|
||||||
# TODO: These step targets should be the actual compose objects,
|
@networks.create(step.target)
|
||||||
# rather than recreating them
|
|
||||||
Network.fromComposeObject({ @docker, @logger },
|
|
||||||
step.target.name,
|
|
||||||
step.appId,
|
|
||||||
step.target.config
|
|
||||||
).create()
|
|
||||||
else
|
else
|
||||||
@volumes.create(step.target)
|
@volumes.create(step.target)
|
||||||
removeNetworkOrVolume: (step) =>
|
removeNetworkOrVolume: (step) =>
|
||||||
if step.model is 'network'
|
if step.model is 'network'
|
||||||
Network.fromComposeObject({ @docker, @logger },
|
@networks.remove(step.current)
|
||||||
step.current.name,
|
|
||||||
step.appId,
|
|
||||||
step.current.config
|
|
||||||
).remove()
|
|
||||||
else
|
else
|
||||||
@volumes.remove(step.current)
|
@volumes.remove(step.current)
|
||||||
ensureSupervisorNetwork: =>
|
ensureSupervisorNetwork: =>
|
||||||
@ -283,12 +275,12 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
for network in networks
|
for network in networks
|
||||||
appId = network.appId
|
appId = network.appId
|
||||||
apps[appId] ?= { appId, services: [], volumes: {}, networks: {} }
|
apps[appId] ?= { appId, services: [], volumes: {}, networks: {} }
|
||||||
apps[appId].networks[network.name] = network.config
|
apps[appId].networks[network.name] = network
|
||||||
|
|
||||||
for volume in volumes
|
for volume in volumes
|
||||||
appId = volume.appId
|
appId = volume.appId
|
||||||
apps[appId] ?= { appId, services: [], volumes: {}, networks: {} }
|
apps[appId] ?= { appId, services: [], volumes: {}, networks: {} }
|
||||||
apps[appId].volumes[volume.name] = volume.config
|
apps[appId].volumes[volume.name] = volume
|
||||||
|
|
||||||
# multi-app warning!
|
# multi-app warning!
|
||||||
# This is just wrong on every level
|
# This is just wrong on every level
|
||||||
@ -396,60 +388,23 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
outputPairs = []
|
outputPairs = []
|
||||||
currentNames = _.keys(current)
|
currentNames = _.keys(current)
|
||||||
targetNames = _.keys(target)
|
targetNames = _.keys(target)
|
||||||
|
|
||||||
toBeRemoved = _.difference(currentNames, targetNames)
|
toBeRemoved = _.difference(currentNames, targetNames)
|
||||||
for name in toBeRemoved
|
for name in toBeRemoved
|
||||||
outputPairs.push({
|
outputPairs.push({ current: current[name], target: null })
|
||||||
current: {
|
|
||||||
name
|
|
||||||
appId
|
|
||||||
config: current[name]
|
|
||||||
}
|
|
||||||
target: null
|
|
||||||
})
|
|
||||||
toBeInstalled = _.difference(targetNames, currentNames)
|
toBeInstalled = _.difference(targetNames, currentNames)
|
||||||
for name in toBeInstalled
|
for name in toBeInstalled
|
||||||
outputPairs.push({
|
outputPairs.push({ current: null, target: target[name] })
|
||||||
current: null
|
|
||||||
target: {
|
toBeUpdated = _.filter _.intersection(targetNames, currentNames), (name) ->
|
||||||
name
|
return !current[name].isEqualConfig(target[name])
|
||||||
appId
|
|
||||||
config: target[name]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toBeUpdated = _.filter _.intersection(targetNames, currentNames), (name) =>
|
|
||||||
# While we're in this in-between state of a network-manager, but not
|
|
||||||
# a volume-manager, we'll have to inspect the object to detect a
|
|
||||||
# network-manager
|
|
||||||
if model instanceof NetworkManager
|
|
||||||
opts = docker: @docker, logger: @logger
|
|
||||||
currentNet = Network.fromComposeObject(
|
|
||||||
opts,
|
|
||||||
name,
|
|
||||||
appId,
|
|
||||||
current[name]
|
|
||||||
)
|
|
||||||
targetNet = Network.fromComposeObject(
|
|
||||||
opts,
|
|
||||||
name,
|
|
||||||
appId,
|
|
||||||
target[name]
|
|
||||||
)
|
|
||||||
return !currentNet.isEqualConfig(targetNet)
|
|
||||||
else
|
|
||||||
return !model.isEqualConfig(current[name], target[name])
|
|
||||||
for name in toBeUpdated
|
for name in toBeUpdated
|
||||||
outputPairs.push({
|
outputPairs.push({
|
||||||
current: {
|
current: current[name],
|
||||||
name
|
target: target[name]
|
||||||
appId
|
|
||||||
config: current[name]
|
|
||||||
}
|
|
||||||
target: {
|
|
||||||
name
|
|
||||||
appId
|
|
||||||
config: target[name]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return outputPairs
|
return outputPairs
|
||||||
|
|
||||||
compareNetworksForUpdate: ({ current, target }, appId) =>
|
compareNetworksForUpdate: ({ current, target }, appId) =>
|
||||||
@ -628,7 +583,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
targetApp = emptyApp
|
targetApp = emptyApp
|
||||||
else
|
else
|
||||||
# Create the default network for the target app
|
# Create the default network for the target app
|
||||||
targetApp.networks['default'] ?= {}
|
targetApp.networks['default'] ?= @createTargetNetwork('default', targetApp.appId, {})
|
||||||
currentApp ?= emptyApp
|
currentApp ?= emptyApp
|
||||||
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
|
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
|
||||||
targetApp.services[0].serviceName == currentApp.services[0].serviceName and
|
targetApp.services[0].serviceName == currentApp.services[0].serviceName and
|
||||||
@ -715,6 +670,22 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
service.image = imageInfo.Id
|
service.image = imageInfo.Id
|
||||||
return Service.fromComposeObject(service, serviceOpts)
|
return Service.fromComposeObject(service, serviceOpts)
|
||||||
|
|
||||||
|
createTargetVolume: (name, appId, volume) ->
|
||||||
|
return Volume.fromComposeObject(
|
||||||
|
name,
|
||||||
|
appId,
|
||||||
|
volume,
|
||||||
|
{ @docker, @logger }
|
||||||
|
)
|
||||||
|
|
||||||
|
createTargetNetwork: (name, appId, network) ->
|
||||||
|
return Network.fromComposeObject(
|
||||||
|
name,
|
||||||
|
appId,
|
||||||
|
network
|
||||||
|
{ @docker, @logger },
|
||||||
|
)
|
||||||
|
|
||||||
normaliseAndExtendAppFromDB: (app) =>
|
normaliseAndExtendAppFromDB: (app) =>
|
||||||
Promise.join(
|
Promise.join(
|
||||||
@config.get('extendedEnvOptions')
|
@config.get('extendedEnvOptions')
|
||||||
@ -732,26 +703,33 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
hostnameOnHost
|
hostnameOnHost
|
||||||
}
|
}
|
||||||
_.assign(configOpts, opts)
|
_.assign(configOpts, opts)
|
||||||
|
|
||||||
volumes = JSON.parse(app.volumes)
|
volumes = JSON.parse(app.volumes)
|
||||||
volumes = _.mapValues volumes, (volumeConfig) ->
|
volumes = _.mapValues volumes, (volumeConfig, volumeName) =>
|
||||||
volumeConfig ?= {}
|
volumeConfig ?= {}
|
||||||
volumeConfig.labels ?= {}
|
volumeConfig.labels ?= {}
|
||||||
return volumeConfig
|
@createTargetVolume(volumeName, app.appId, volumeConfig)
|
||||||
|
|
||||||
|
networks = JSON.parse(app.networks)
|
||||||
|
networks = _.mapValues networks, (networkConfig, networkName) =>
|
||||||
|
networkConfig ?= {}
|
||||||
|
@createTargetNetwork(networkName, app.appId, networkConfig)
|
||||||
|
|
||||||
Promise.map(JSON.parse(app.services), (service) => @createTargetService(service, configOpts))
|
Promise.map(JSON.parse(app.services), (service) => @createTargetService(service, configOpts))
|
||||||
.then (services) ->
|
.then (services) =>
|
||||||
# If a named volume is defined in a service, we add it app-wide so that we can track it and purge it
|
# If a named volume is defined in a service, we add it app-wide so that we can track it and purge it
|
||||||
for s in services
|
for s in services
|
||||||
serviceNamedVolumes = s.getNamedVolumes()
|
serviceNamedVolumes = s.getNamedVolumes()
|
||||||
for name in serviceNamedVolumes
|
for name in serviceNamedVolumes
|
||||||
volumes[name] ?= { labels: {} }
|
volumes[name] = @createTargetVolume(name, app.appId, { labels: {} })
|
||||||
outApp = {
|
outApp = {
|
||||||
appId: app.appId
|
appId: app.appId
|
||||||
name: app.name
|
name: app.name
|
||||||
commit: app.commit
|
commit: app.commit
|
||||||
releaseId: app.releaseId
|
releaseId: app.releaseId
|
||||||
services: services
|
services: services
|
||||||
networks: JSON.parse(app.networks)
|
networks
|
||||||
volumes: volumes
|
volumes
|
||||||
}
|
}
|
||||||
return outApp
|
return outApp
|
||||||
)
|
)
|
||||||
@ -984,5 +962,4 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
svc.serviceId == serviceId
|
svc.serviceId == serviceId
|
||||||
.get('serviceName')
|
.get('serviceName')
|
||||||
|
|
||||||
|
|
||||||
localModeSwitchCompletion: => @localModeManager.switchCompletion()
|
localModeSwitchCompletion: => @localModeManager.switchCompletion()
|
||||||
|
@ -5,10 +5,12 @@ import { fs } from 'mz';
|
|||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import Docker from '../lib/docker-utils';
|
import Docker from '../lib/docker-utils';
|
||||||
import { ENOENT, NotFoundError } from '../lib/errors';
|
import { ENOENT, NotFoundError } from '../lib/errors';
|
||||||
|
import logTypes = require('../lib/log-types');
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { Network, NetworkOptions } from './network';
|
import { Network, NetworkOptions } from './network';
|
||||||
|
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
|
import { ResourceRecreationAttemptError } from './errors';
|
||||||
|
|
||||||
export class NetworkManager {
|
export class NetworkManager {
|
||||||
private docker: Docker;
|
private docker: Docker;
|
||||||
@ -40,17 +42,51 @@ export class NetworkManager {
|
|||||||
return this.getAll().filter((network: Network) => network.appId === appId);
|
return this.getAll().filter((network: Network) => network.appId === appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(network: { name: string; appId: number }): Bluebird<Network> {
|
public async get(network: {
|
||||||
return Network.fromNameAndAppId(
|
name: string;
|
||||||
{
|
appId: number;
|
||||||
logger: this.logger,
|
}): Bluebird<Network> {
|
||||||
docker: this.docker,
|
const dockerNet = await this.docker
|
||||||
},
|
.getNetwork(Network.generateDockerName(network.appId, network.name))
|
||||||
network.name,
|
.inspect();
|
||||||
network.appId,
|
return Network.fromDockerNetwork(
|
||||||
|
{ docker: this.docker, logger: this.logger },
|
||||||
|
dockerNet,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async create(network: Network) {
|
||||||
|
try {
|
||||||
|
const existing = await this.get({
|
||||||
|
name: network.name,
|
||||||
|
appId: network.appId,
|
||||||
|
});
|
||||||
|
if (!network.isEqualConfig(existing)) {
|
||||||
|
throw new ResourceRecreationAttemptError('network', network.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a network with the same config and name
|
||||||
|
// already created, we can skip this
|
||||||
|
} catch (e) {
|
||||||
|
if (!NotFoundError(e)) {
|
||||||
|
this.logger.logSystemEvent(logTypes.createNetworkError, {
|
||||||
|
network: { name: network.name, appId: network.appId },
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got a not found error, create the network
|
||||||
|
await network.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(network: Network) {
|
||||||
|
// We simply forward this to the network object, but we
|
||||||
|
// add this method to provide a consistent interface
|
||||||
|
await network.remove();
|
||||||
|
}
|
||||||
|
|
||||||
public supervisorNetworkReady(): Bluebird<boolean> {
|
public supervisorNetworkReady(): Bluebird<boolean> {
|
||||||
return Bluebird.resolve(
|
return Bluebird.resolve(
|
||||||
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
||||||
|
@ -2,13 +2,14 @@ import * as Bluebird from 'bluebird';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Docker from '../lib/docker-utils';
|
import Docker from '../lib/docker-utils';
|
||||||
import { InvalidAppIdError, NotFoundError } from '../lib/errors';
|
import { InvalidAppIdError } from '../lib/errors';
|
||||||
import logTypes = require('../lib/log-types');
|
import logTypes = require('../lib/log-types');
|
||||||
import { checkInt } from '../lib/validation';
|
import { checkInt } from '../lib/validation';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as ComposeUtils from './utils';
|
import * as ComposeUtils from './utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ComposeNetworkConfig,
|
||||||
DockerIPAMConfig,
|
DockerIPAMConfig,
|
||||||
DockerNetworkConfig,
|
DockerNetworkConfig,
|
||||||
NetworkConfig,
|
NetworkConfig,
|
||||||
@ -18,7 +19,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
InvalidNetworkConfigurationError,
|
InvalidNetworkConfigurationError,
|
||||||
InvalidNetworkNameError,
|
InvalidNetworkNameError,
|
||||||
ResourceRecreationAttemptError,
|
|
||||||
} from './errors';
|
} from './errors';
|
||||||
|
|
||||||
export interface NetworkOptions {
|
export interface NetworkOptions {
|
||||||
@ -33,12 +33,10 @@ export class Network {
|
|||||||
|
|
||||||
private docker: Docker;
|
private docker: Docker;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private networkOpts: NetworkOptions;
|
|
||||||
|
|
||||||
private constructor(opts: NetworkOptions) {
|
private constructor(opts: NetworkOptions) {
|
||||||
this.docker = opts.docker;
|
this.docker = opts.docker;
|
||||||
this.logger = opts.logger;
|
this.logger = opts.logger;
|
||||||
this.networkOpts = opts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromDockerNetwork(
|
public static fromDockerNetwork(
|
||||||
@ -93,20 +91,11 @@ export class Network {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async fromNameAndAppId(
|
|
||||||
opts: NetworkOptions,
|
|
||||||
name: string,
|
|
||||||
appId: number,
|
|
||||||
): Bluebird<Network> {
|
|
||||||
const network = await opts.docker.getNetwork(`${appId}_${name}`).inspect();
|
|
||||||
return Network.fromDockerNetwork(opts, network);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromComposeObject(
|
public static fromComposeObject(
|
||||||
opts: NetworkOptions,
|
|
||||||
name: string,
|
name: string,
|
||||||
appId: number,
|
appId: number,
|
||||||
network: NetworkConfig,
|
network: Partial<ComposeNetworkConfig>,
|
||||||
|
opts: NetworkOptions,
|
||||||
): Network {
|
): Network {
|
||||||
const net = new Network(opts);
|
const net = new Network(opts);
|
||||||
net.name = name;
|
net.name = name;
|
||||||
@ -114,53 +103,56 @@ export class Network {
|
|||||||
|
|
||||||
Network.validateComposeConfig(network);
|
Network.validateComposeConfig(network);
|
||||||
|
|
||||||
// Assign the default values for a network inspect,
|
const ipam =
|
||||||
// so when we come to compare, it will match
|
network.ipam != null
|
||||||
net.config = _.defaultsDeep(network, {
|
? network.ipam
|
||||||
driver: 'bridge',
|
: {
|
||||||
ipam: {
|
driver: 'default',
|
||||||
driver: 'default',
|
config: [],
|
||||||
config: [],
|
options: {},
|
||||||
options: {},
|
};
|
||||||
},
|
if (ipam.config == null) {
|
||||||
enableIPv6: false,
|
ipam.config = [];
|
||||||
internal: false,
|
}
|
||||||
labels: {},
|
if (ipam.options == null) {
|
||||||
options: {},
|
ipam.options = {};
|
||||||
});
|
}
|
||||||
|
net.config = {
|
||||||
|
driver: network.driver || 'bridge',
|
||||||
|
ipam,
|
||||||
|
enableIPv6: network.enable_ipv6 || false,
|
||||||
|
internal: network.internal || false,
|
||||||
|
labels: network.labels || {},
|
||||||
|
options: network.driver_opts || {},
|
||||||
|
};
|
||||||
|
|
||||||
net.config.labels = ComposeUtils.normalizeLabels(net.config.labels);
|
net.config.labels = ComposeUtils.normalizeLabels(net.config.labels);
|
||||||
|
|
||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
public create(): Bluebird<void> {
|
public toComposeObject(): ComposeNetworkConfig {
|
||||||
|
return {
|
||||||
|
driver: this.config.driver,
|
||||||
|
driver_opts: this.config.options,
|
||||||
|
enable_ipv6: this.config.enableIPv6,
|
||||||
|
internal: this.config.internal,
|
||||||
|
ipam: this.config.ipam,
|
||||||
|
labels: this.config.labels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(): Promise<void> {
|
||||||
this.logger.logSystemEvent(logTypes.createNetwork, {
|
this.logger.logSystemEvent(logTypes.createNetwork, {
|
||||||
network: { name: this.name },
|
network: { name: this.name },
|
||||||
});
|
});
|
||||||
|
|
||||||
return Network.fromNameAndAppId(this.networkOpts, this.name, this.appId)
|
return await this.docker.createNetwork(this.toDockerConfig());
|
||||||
.then(current => {
|
|
||||||
if (!this.isEqualConfig(current)) {
|
|
||||||
throw new ResourceRecreationAttemptError('network', this.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a network with the same config and name already created -
|
|
||||||
// we can skip this.
|
|
||||||
})
|
|
||||||
.catch(NotFoundError, () => {
|
|
||||||
return this.docker.createNetwork(this.toDockerConfig());
|
|
||||||
})
|
|
||||||
.tapCatch(err => {
|
|
||||||
this.logger.logSystemEvent(logTypes.createNetworkError, {
|
|
||||||
network: { name: this.name, appId: this.appId },
|
|
||||||
error: err,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toDockerConfig(): DockerNetworkConfig {
|
public toDockerConfig(): DockerNetworkConfig {
|
||||||
return {
|
return {
|
||||||
Name: this.getDockerName(),
|
Name: Network.generateDockerName(this.appId, this.name),
|
||||||
Driver: this.config.driver,
|
Driver: this.config.driver,
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
IPAM: {
|
IPAM: {
|
||||||
@ -201,7 +193,9 @@ export class Network {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Bluebird.resolve(
|
return Bluebird.resolve(
|
||||||
this.docker.getNetwork(this.getDockerName()).remove(),
|
this.docker
|
||||||
|
.getNetwork(Network.generateDockerName(this.appId, this.name))
|
||||||
|
.remove(),
|
||||||
).tapCatch(error => {
|
).tapCatch(error => {
|
||||||
this.logger.logSystemEvent(logTypes.createNetworkError, {
|
this.logger.logSystemEvent(logTypes.createNetworkError, {
|
||||||
network: { name: this.name, appId: this.appId },
|
network: { name: this.name, appId: this.appId },
|
||||||
@ -224,11 +218,9 @@ export class Network {
|
|||||||
return _.isEqual(configToCompare, network.config);
|
return _.isEqual(configToCompare, network.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDockerName(): string {
|
private static validateComposeConfig(
|
||||||
return `${this.appId}_${this.name}`;
|
config: Partial<ComposeNetworkConfig>,
|
||||||
}
|
): void {
|
||||||
|
|
||||||
private static validateComposeConfig(config: NetworkConfig): void {
|
|
||||||
// Check if every ipam config entry has both a subnet and a gateway
|
// Check if every ipam config entry has both a subnet and a gateway
|
||||||
_.each(_.get(config, 'config.ipam.config', []), ({ subnet, gateway }) => {
|
_.each(_.get(config, 'config.ipam.config', []), ({ subnet, gateway }) => {
|
||||||
if (subnet == null || gateway == null) {
|
if (subnet == null || gateway == null) {
|
||||||
@ -238,4 +230,8 @@ export class Network {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static generateDockerName(appId: number, name: string) {
|
||||||
|
return `${appId}_${name}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -542,6 +542,13 @@ export class Service {
|
|||||||
return svc;
|
return svc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toComposeObject(): ServiceConfig {
|
||||||
|
// This isn't techinically correct as we do some changes
|
||||||
|
// to the configuration which we cannot reverse. We also
|
||||||
|
// represent the ports as a class, which isn't ideal
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
public toDockerContainer(opts: {
|
public toDockerContainer(opts: {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
}): Dockerode.ContainerCreateOptions {
|
}): Dockerode.ContainerCreateOptions {
|
||||||
|
@ -34,6 +34,25 @@ export interface NetworkInspect {
|
|||||||
Labels: { [labelName: string]: string };
|
Labels: { [labelName: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComposeNetworkConfig {
|
||||||
|
driver: string;
|
||||||
|
driver_opts: Dictionary<string>;
|
||||||
|
ipam: {
|
||||||
|
driver: string;
|
||||||
|
config: Array<
|
||||||
|
Partial<{
|
||||||
|
subnet: string;
|
||||||
|
ip_range: string;
|
||||||
|
gateway: string;
|
||||||
|
aux_addresses: Dictionary<string>;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
options: Dictionary<string>;
|
||||||
|
};
|
||||||
|
enable_ipv6: boolean;
|
||||||
|
internal: boolean;
|
||||||
|
labels: Dictionary<string>;
|
||||||
|
}
|
||||||
export interface NetworkConfig {
|
export interface NetworkConfig {
|
||||||
driver: string;
|
driver: string;
|
||||||
ipam: {
|
ipam: {
|
||||||
|
150
src/compose/volume-manager.ts
Normal file
150
src/compose/volume-manager.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import * as Docker from 'dockerode';
|
||||||
|
import filter = require('lodash/filter');
|
||||||
|
import get = require('lodash/get');
|
||||||
|
import unionBy = require('lodash/unionBy');
|
||||||
|
import * as Path from 'path';
|
||||||
|
|
||||||
|
import constants = require('../lib/constants');
|
||||||
|
import { NotFoundError } from '../lib/errors';
|
||||||
|
import { safeRename } from '../lib/fs-utils';
|
||||||
|
import * as LogTypes from '../lib/log-types';
|
||||||
|
import { defaultLegacyVolume } from '../lib/migration';
|
||||||
|
import Logger from '../logger';
|
||||||
|
import { ResourceRecreationAttemptError } from './errors';
|
||||||
|
import Volume, { VolumeConfig } from './volume';
|
||||||
|
|
||||||
|
export interface VolumeMangerConstructOpts {
|
||||||
|
docker: Docker;
|
||||||
|
logger: Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeNameOpts {
|
||||||
|
name: string;
|
||||||
|
appId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VolumeManager {
|
||||||
|
private docker: Docker;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
public constructor(opts: VolumeMangerConstructOpts) {
|
||||||
|
this.docker = opts.docker;
|
||||||
|
this.logger = opts.logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get({ name, appId }: VolumeNameOpts): Promise<Volume> {
|
||||||
|
return Volume.fromDockerVolume(
|
||||||
|
{ docker: this.docker, logger: this.logger },
|
||||||
|
await this.docker
|
||||||
|
.getVolume(Volume.generateDockerName(appId, name))
|
||||||
|
.inspect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAll(): Promise<Volume[]> {
|
||||||
|
const volumeInspect = await this.listWithBothLabels();
|
||||||
|
return volumeInspect.map(inspect =>
|
||||||
|
Volume.fromDockerVolume(
|
||||||
|
{ logger: this.logger, docker: this.docker },
|
||||||
|
inspect,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAllByAppId(appId: number): Promise<Volume[]> {
|
||||||
|
const all = await this.getAll();
|
||||||
|
return filter(all, { appId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(volume: Volume): Promise<void> {
|
||||||
|
// First we check that we're not trying to recreate a
|
||||||
|
// volume
|
||||||
|
try {
|
||||||
|
const existing = await this.get({
|
||||||
|
name: volume.name,
|
||||||
|
appId: volume.appId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!volume.isEqualConfig(existing)) {
|
||||||
|
throw new ResourceRecreationAttemptError('volume', volume.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!NotFoundError(e)) {
|
||||||
|
this.logger.logSystemEvent(LogTypes.createVolumeError, {
|
||||||
|
volume: { name: volume.name },
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
await volume.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We simply forward this to the volume object, but we
|
||||||
|
// add this method to provide a consistent interface
|
||||||
|
public async remove(volume: Volume) {
|
||||||
|
await volume.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createFromLegacy(appId: number): Promise<Volume | void> {
|
||||||
|
const name = defaultLegacyVolume();
|
||||||
|
const legacyPath = Path.join(
|
||||||
|
constants.rootMountPoint,
|
||||||
|
'mnt/data/resin-data',
|
||||||
|
appId.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.createFromPath({ name, appId }, {}, legacyPath);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.logSystemMessage(
|
||||||
|
`Warning: could not migrate legacy /data volume: ${e.message}`,
|
||||||
|
{ error: e },
|
||||||
|
'Volume migration error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createFromPath(
|
||||||
|
{ name, appId }: VolumeNameOpts,
|
||||||
|
config: Partial<VolumeConfig>,
|
||||||
|
oldPath: string,
|
||||||
|
): Promise<Volume> {
|
||||||
|
const volume = Volume.fromComposeObject(name, appId, config, {
|
||||||
|
logger: this.logger,
|
||||||
|
docker: this.docker,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.create(volume);
|
||||||
|
const inspect = await this.docker
|
||||||
|
.getVolume(Volume.generateDockerName(volume.appId, volume.name))
|
||||||
|
.inspect();
|
||||||
|
|
||||||
|
const volumePath = Path.join(
|
||||||
|
constants.rootMountPoint,
|
||||||
|
'mnt/data',
|
||||||
|
...inspect.Mountpoint.split(Path.sep).slice(3),
|
||||||
|
);
|
||||||
|
|
||||||
|
await safeRename(oldPath, volumePath);
|
||||||
|
return volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async listWithBothLabels(): Promise<Docker.VolumeInspectInfo[]> {
|
||||||
|
const [legacyResponse, currentResponse] = await Promise.all([
|
||||||
|
this.docker.listVolumes({
|
||||||
|
filters: { label: ['io.resin.supervised'] },
|
||||||
|
}),
|
||||||
|
this.docker.listVolumes({
|
||||||
|
filters: { label: ['io.balena.supervised'] },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const legacyVolumes = get(legacyResponse, 'Volumes', []);
|
||||||
|
const currentVolumes = get(currentResponse, 'Volumes', []);
|
||||||
|
return unionBy(legacyVolumes, currentVolumes, 'Name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VolumeManager;
|
153
src/compose/volume.ts
Normal file
153
src/compose/volume.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import * as Docker from 'dockerode';
|
||||||
|
import assign = require('lodash/assign');
|
||||||
|
import isEqual = require('lodash/isEqual');
|
||||||
|
|
||||||
|
import constants = require('../lib/constants');
|
||||||
|
import { InternalInconsistencyError } from '../lib/errors';
|
||||||
|
import * as LogTypes from '../lib/log-types';
|
||||||
|
import { LabelObject } from '../lib/types';
|
||||||
|
import Logger from '../logger';
|
||||||
|
import * as ComposeUtils from './utils';
|
||||||
|
|
||||||
|
export interface VolumeConstructOpts {
|
||||||
|
logger: Logger;
|
||||||
|
docker: Docker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeConfig {
|
||||||
|
labels: LabelObject;
|
||||||
|
driverOpts: Docker.VolumeInspectInfo['Options'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComposeVolumeConfig {
|
||||||
|
driver_opts: Dictionary<string>;
|
||||||
|
labels: LabelObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Volume {
|
||||||
|
public appId: number;
|
||||||
|
public name: string;
|
||||||
|
public config: VolumeConfig;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
private docker: Docker;
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
name: string,
|
||||||
|
appId: number,
|
||||||
|
config: VolumeConfig,
|
||||||
|
opts: VolumeConstructOpts,
|
||||||
|
) {
|
||||||
|
this.name = name;
|
||||||
|
this.appId = appId;
|
||||||
|
|
||||||
|
this.logger = opts.logger;
|
||||||
|
this.docker = opts.docker;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromDockerVolume(
|
||||||
|
opts: VolumeConstructOpts,
|
||||||
|
inspect: Docker.VolumeInspectInfo,
|
||||||
|
): Volume {
|
||||||
|
// Convert the docker inspect to the config
|
||||||
|
const config: VolumeConfig = {
|
||||||
|
labels: inspect.Labels || {},
|
||||||
|
driverOpts: inspect.Options || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Detect the name and appId from the inspect data
|
||||||
|
const { name, appId } = this.deconstructDockerName(inspect.Name);
|
||||||
|
|
||||||
|
return new Volume(name, appId, config, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromComposeObject(
|
||||||
|
name: string,
|
||||||
|
appId: number,
|
||||||
|
config: Partial<ComposeVolumeConfig>,
|
||||||
|
opts: VolumeConstructOpts,
|
||||||
|
) {
|
||||||
|
const filledConfig: VolumeConfig = {
|
||||||
|
driverOpts: config.driver_opts || {},
|
||||||
|
labels: ComposeUtils.normalizeLabels(config.labels || {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// We only need to assign the labels here, as when we
|
||||||
|
// get it from the daemon, they should already be there
|
||||||
|
assign(filledConfig.labels, constants.defaultVolumeLabels);
|
||||||
|
|
||||||
|
return new Volume(name, appId, filledConfig, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toComposeObject(): ComposeVolumeConfig {
|
||||||
|
return {
|
||||||
|
driver_opts: this.config.driverOpts!,
|
||||||
|
labels: this.config.labels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEqualConfig(volume: Volume): boolean {
|
||||||
|
return (
|
||||||
|
isEqual(this.config.driverOpts, volume.config.driverOpts) &&
|
||||||
|
isEqual(this.config.labels, volume.config.labels)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(): Promise<void> {
|
||||||
|
this.logger.logSystemEvent(LogTypes.createVolume, {
|
||||||
|
volume: { name: this.name },
|
||||||
|
});
|
||||||
|
await this.docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(this.appId, this.name),
|
||||||
|
Labels: this.config.labels,
|
||||||
|
DriverOpts: this.config.driverOpts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(): Promise<void> {
|
||||||
|
this.logger.logSystemEvent(LogTypes.removeVolume, {
|
||||||
|
volume: { name: this.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.docker
|
||||||
|
.getVolume(Volume.generateDockerName(this.appId, this.name))
|
||||||
|
.remove();
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.logSystemEvent(LogTypes.removeVolumeError, {
|
||||||
|
volume: { name: this.name, appId: this.appId },
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static generateDockerName(appId: number, name: string) {
|
||||||
|
return `${appId}_${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static deconstructDockerName(
|
||||||
|
name: string,
|
||||||
|
): { name: string; appId: number } {
|
||||||
|
const match = name.match(/(\d+)_(\S+)/);
|
||||||
|
if (match == null) {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
`Could not detect volume data from docker name: ${name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = parseInt(match[1], 10);
|
||||||
|
if (isNaN(appId)) {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
`Could not detect application id from docker name: ${match[1]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appId,
|
||||||
|
name: match[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Volume;
|
@ -1,225 +0,0 @@
|
|||||||
import * as Dockerode from 'dockerode';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import Docker from '../lib/docker-utils';
|
|
||||||
import Logger from '../logger';
|
|
||||||
|
|
||||||
import constants = require('../lib/constants');
|
|
||||||
import { InternalInconsistencyError, NotFoundError } from '../lib/errors';
|
|
||||||
import { safeRename } from '../lib/fs-utils';
|
|
||||||
import * as LogTypes from '../lib/log-types';
|
|
||||||
import { defaultLegacyVolume } from '../lib/migration';
|
|
||||||
import { LabelObject } from '../lib/types';
|
|
||||||
import { checkInt } from '../lib/validation';
|
|
||||||
import * as ComposeUtils from './utils';
|
|
||||||
|
|
||||||
interface VolumeConstructOpts {
|
|
||||||
docker: Docker;
|
|
||||||
logger: Logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComposeVolume {
|
|
||||||
name: string;
|
|
||||||
appId: number;
|
|
||||||
config: {
|
|
||||||
labels: LabelObject;
|
|
||||||
driverOpts: Dockerode.VolumeInspectInfo['Options'];
|
|
||||||
};
|
|
||||||
dockerVolume: Dockerode.VolumeInspectInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VolumeNameOpts {
|
|
||||||
name: string;
|
|
||||||
appId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This weird type is currently needed because the create function (and helpers)
|
|
||||||
// accept either a docker volume or a compose volume (or an empty object too apparently).
|
|
||||||
// If we instead split the tasks into createFromCompose and createFromDocker, we will no
|
|
||||||
// longer have this issue (and weird typing)
|
|
||||||
type VolumeConfig = ComposeVolume['config'] | Dockerode.VolumeInspectInfo | {};
|
|
||||||
type VolumeCreateOpts = VolumeNameOpts & {
|
|
||||||
config?: VolumeConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Volumes {
|
|
||||||
private docker: Docker;
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
public constructor(opts: VolumeConstructOpts) {
|
|
||||||
this.docker = opts.docker;
|
|
||||||
this.logger = opts.logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAll(): Promise<ComposeVolume[]> {
|
|
||||||
const volumes = await this.listWithBothLabels();
|
|
||||||
return volumes.map(Volumes.format);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAllByAppId(appId: number): Promise<ComposeVolume[]> {
|
|
||||||
const all = await this.getAll();
|
|
||||||
return _.filter(all, { appId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get({ name, appId }: VolumeNameOpts): Promise<ComposeVolume> {
|
|
||||||
const volume = await this.docker.getVolume(`${appId}_${name}`).inspect();
|
|
||||||
return Volumes.format(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(opts: VolumeCreateOpts): Promise<ComposeVolume> {
|
|
||||||
const { name, config = {}, appId } = opts;
|
|
||||||
const camelCaseConfig: Dictionary<unknown> = _.mapKeys(config, (_v, k) =>
|
|
||||||
_.camelCase(k),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.logSystemEvent(LogTypes.createVolume, { volume: { name } });
|
|
||||||
|
|
||||||
const labels = _.clone(camelCaseConfig.labels as LabelObject) || {};
|
|
||||||
_.assign(labels, constants.defaultVolumeLabels);
|
|
||||||
|
|
||||||
const driverOpts: Dictionary<unknown> =
|
|
||||||
camelCaseConfig.driverOpts != null
|
|
||||||
? (camelCaseConfig.driverOpts as Dictionary<unknown>)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const volume = await this.get({ name, appId });
|
|
||||||
if (!this.isEqualConfig(volume.config, config)) {
|
|
||||||
throw new InternalInconsistencyError(
|
|
||||||
`Trying to create volume '${name}', but a volume with the same name and different configuration exists`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return volume;
|
|
||||||
} catch (e) {
|
|
||||||
if (!NotFoundError(e)) {
|
|
||||||
this.logger.logSystemEvent(LogTypes.createVolumeError, {
|
|
||||||
volume: { name },
|
|
||||||
error: e,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const volume = await this.docker.createVolume({
|
|
||||||
Name: Volumes.generateVolumeName({ name, appId }),
|
|
||||||
Labels: labels,
|
|
||||||
DriverOpts: driverOpts,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Volumes.format(await volume.inspect());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createFromLegacy(appId: number): Promise<ComposeVolume | void> {
|
|
||||||
const name = defaultLegacyVolume();
|
|
||||||
const legacyPath = path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
'mnt/data/resin-data',
|
|
||||||
appId.toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.createFromPath({ name, appId }, legacyPath);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.logSystemMessage(
|
|
||||||
`Warning: could not migrate legacy /data volume: ${e.message}`,
|
|
||||||
{ error: e },
|
|
||||||
'Volume migration error',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// oldPath must be a path inside /mnt/data
|
|
||||||
public async createFromPath(
|
|
||||||
opts: VolumeCreateOpts,
|
|
||||||
oldPath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const volume = await this.create(opts);
|
|
||||||
const handle = volume.dockerVolume;
|
|
||||||
|
|
||||||
// Convert the path to be of the same mountpoint so that rename can work
|
|
||||||
const volumePath = path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
'mnt/data',
|
|
||||||
...handle.Mountpoint.split(path.sep).slice(3),
|
|
||||||
);
|
|
||||||
await safeRename(oldPath, volumePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove({ name, appId }: VolumeNameOpts) {
|
|
||||||
this.logger.logSystemEvent(LogTypes.removeVolume, { volume: { name } });
|
|
||||||
try {
|
|
||||||
await this.docker
|
|
||||||
.getVolume(Volumes.generateVolumeName({ name, appId }))
|
|
||||||
.remove();
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.logSystemEvent(LogTypes.removeVolumeError, {
|
|
||||||
volume: { name, appId },
|
|
||||||
error: e,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEqualConfig(current: VolumeConfig, target: VolumeConfig): boolean {
|
|
||||||
const currentConfig = (_.mapKeys(current, (_v, k) =>
|
|
||||||
_.camelCase(k),
|
|
||||||
) as unknown) as ComposeVolume['config'];
|
|
||||||
const targetConfig = (_.mapKeys(target, (_v, k) =>
|
|
||||||
_.camelCase(k),
|
|
||||||
) as unknown) as ComposeVolume['config'];
|
|
||||||
|
|
||||||
const currentOpts = currentConfig.driverOpts || {};
|
|
||||||
const targetOpts = targetConfig.driverOpts || {};
|
|
||||||
|
|
||||||
const currentLabels = currentConfig.labels || {};
|
|
||||||
const targetLabels = targetConfig.labels || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
_.isEqual(currentOpts, targetOpts) &&
|
|
||||||
_.isEqual(currentLabels, targetLabels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static format(volume: Dockerode.VolumeInspectInfo): ComposeVolume {
|
|
||||||
const match = volume.Name.match(/^([0-9]+)_(.+)$/);
|
|
||||||
if (match == null) {
|
|
||||||
throw new Error('Malformed volume name in Volume.format');
|
|
||||||
}
|
|
||||||
const appId = checkInt(match[1]);
|
|
||||||
const name = match[2];
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
// We know this cast is fine due to the regex
|
|
||||||
appId: appId as number,
|
|
||||||
config: {
|
|
||||||
labels: _.omit(
|
|
||||||
ComposeUtils.normalizeLabels(volume.Labels),
|
|
||||||
_.keys(constants.defaultVolumeLabels),
|
|
||||||
),
|
|
||||||
driverOpts: volume.Options,
|
|
||||||
},
|
|
||||||
dockerVolume: volume,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listWithBothLabels(): Promise<Dockerode.VolumeInspectInfo[]> {
|
|
||||||
const [legacyResponse, currentResponse] = await Promise.all([
|
|
||||||
this.docker.listVolumes({
|
|
||||||
filters: { label: ['io.resin.supervised'] },
|
|
||||||
}),
|
|
||||||
this.docker.listVolumes({
|
|
||||||
filters: { label: ['io.balena.supervised'] },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const legacyVolumes = _.get(legacyResponse, 'Volumes', []);
|
|
||||||
const currentVolumes = _.get(currentResponse, 'Volumes', []);
|
|
||||||
return _.unionBy(legacyVolumes, currentVolumes, 'Name');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static generateVolumeName({ name, appId }: VolumeNameOpts) {
|
|
||||||
return `${appId}_${name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Volumes;
|
|
@ -244,9 +244,26 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This should really return the config as it
|
||||||
|
// is returned from the api, but currently that's not
|
||||||
|
// the easiest thing due to the way their stored and
|
||||||
|
// retrieved from the db - when all of the application
|
||||||
|
// manager is strongly typed, revisit this. The best
|
||||||
|
// thing to do would be to represent the input with
|
||||||
|
// io-ts and make sure the below conforms to it
|
||||||
|
const target = _.cloneDeep(await deviceState.getTarget());
|
||||||
|
if (target.local != null && !_.isEmpty(target.local.apps)) {
|
||||||
|
target.local.apps = _.mapValues(target.local.apps, app => {
|
||||||
|
app.services = _.map(app.services, s => s.toComposeObject());
|
||||||
|
app.volumes = _.mapValues(app.volumes, v => v.toComposeObject());
|
||||||
|
app.networks = _.mapValues(app.networks, n => n.toComposeObject());
|
||||||
|
return app;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
state: await deviceState.getTarget(),
|
state: target,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(503).send({
|
res.status(503).send({
|
||||||
@ -259,8 +276,6 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
|||||||
router.post('/v2/local/target-state', async (req, res) => {
|
router.post('/v2/local/target-state', async (req, res) => {
|
||||||
// let's first ensure that we're in local mode, otherwise
|
// let's first ensure that we're in local mode, otherwise
|
||||||
// this function should not do anything
|
// this function should not do anything
|
||||||
// TODO: We really should refactor the config module to provide bools
|
|
||||||
// as bools etc
|
|
||||||
try {
|
try {
|
||||||
const localMode = await deviceState.config.get('localMode');
|
const localMode = await deviceState.config.get('localMode');
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
|
@ -447,7 +447,8 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
# If the volume exists (from a previous incomplete run of this restoreBackup), we delete it first
|
# If the volume exists (from a previous incomplete run of this restoreBackup), we delete it first
|
||||||
@applications.volumes.get({ appId, name: volumeName })
|
@applications.volumes.get({ appId, name: volumeName })
|
||||||
.then =>
|
.then =>
|
||||||
@applications.volumes.remove({ appId, name: volumeName })
|
@applications.volumes.get({ appId, name: volumeName }).then (volume) ->
|
||||||
|
volume.remove()
|
||||||
.catch(NotFoundError, _.noop)
|
.catch(NotFoundError, _.noop)
|
||||||
.then =>
|
.then =>
|
||||||
@applications.volumes.createFromPath({ appId, name: volumeName, config: volumes[volumeName] }, path.join(backupPath, volumeName))
|
@applications.volumes.createFromPath({ appId, name: volumeName, config: volumes[volumeName] }, path.join(backupPath, volumeName))
|
||||||
|
@ -11,6 +11,7 @@ DeviceState = require '../src/device-state'
|
|||||||
{ DB } = require('../src/db')
|
{ DB } = require('../src/db')
|
||||||
{ Config } = require('../src/config')
|
{ Config } = require('../src/config')
|
||||||
{ Service } = require '../src/compose/service'
|
{ Service } = require '../src/compose/service'
|
||||||
|
{ Network } = require '../src/compose/network'
|
||||||
|
|
||||||
appDBFormatNormalised = {
|
appDBFormatNormalised = {
|
||||||
appId: 1234
|
appId: 1234
|
||||||
@ -143,12 +144,19 @@ describe 'ApplicationManager', ->
|
|||||||
env['ADDITIONAL_ENV_VAR'] = 'foo'
|
env['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||||
return env
|
return env
|
||||||
@normaliseCurrent = (current) ->
|
@normaliseCurrent = (current) ->
|
||||||
Promise.map current.local.apps, (app) ->
|
Promise.map current.local.apps, (app) =>
|
||||||
Promise.map app.services, (service) ->
|
Promise.map app.services, (service) ->
|
||||||
Service.fromComposeObject(service, { appName: 'test' })
|
Service.fromComposeObject(service, { appName: 'test' })
|
||||||
.then (normalisedServices) ->
|
.then (normalisedServices) =>
|
||||||
appCloned = _.cloneDeep(app)
|
appCloned = _.cloneDeep(app)
|
||||||
appCloned.services = normalisedServices
|
appCloned.services = normalisedServices
|
||||||
|
appCloned.networks = _.mapValues appCloned.networks, (config, name) =>
|
||||||
|
Network.fromComposeObject(
|
||||||
|
name,
|
||||||
|
app.appId,
|
||||||
|
config
|
||||||
|
{ docker: @applications.docker, @logger }
|
||||||
|
)
|
||||||
return appCloned
|
return appCloned
|
||||||
.then (normalisedApps) ->
|
.then (normalisedApps) ->
|
||||||
currentCloned = _.cloneDeep(current)
|
currentCloned = _.cloneDeep(current)
|
||||||
|
@ -8,7 +8,7 @@ describe 'compose/network', ->
|
|||||||
|
|
||||||
it 'should convert a compose configuration to an internal representation', ->
|
it 'should convert a compose configuration to an internal representation', ->
|
||||||
|
|
||||||
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
|
network = Network.fromComposeObject('test', 123, {
|
||||||
'driver': 'bridge',
|
'driver': 'bridge',
|
||||||
'ipam': {
|
'ipam': {
|
||||||
'driver': 'default',
|
'driver': 'default',
|
||||||
@ -19,7 +19,7 @@ describe 'compose/network', ->
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
}, { logger: null, docker: null })
|
||||||
|
|
||||||
expect(network.config).to.deep.equal({
|
expect(network.config).to.deep.equal({
|
||||||
driver: 'bridge'
|
driver: 'bridge'
|
||||||
@ -41,7 +41,7 @@ describe 'compose/network', ->
|
|||||||
|
|
||||||
it 'should convert an internal representation to a docker representation', ->
|
it 'should convert an internal representation to a docker representation', ->
|
||||||
|
|
||||||
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
|
network = Network.fromComposeObject('test', 123, {
|
||||||
'driver': 'bridge',
|
'driver': 'bridge',
|
||||||
'ipam': {
|
'ipam': {
|
||||||
'driver': 'default',
|
'driver': 'default',
|
||||||
@ -52,7 +52,7 @@ describe 'compose/network', ->
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
}, { logger: null, docker: null })
|
||||||
|
|
||||||
expect(network.toDockerConfig()).to.deep.equal({
|
expect(network.toDockerConfig()).to.deep.equal({
|
||||||
Name: '123_test',
|
Name: '123_test',
|
||||||
|
123
test/20-compose-volume.ts
Normal file
123
test/20-compose-volume.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { stub } from 'sinon';
|
||||||
|
|
||||||
|
import Volume from '../src/compose/volume';
|
||||||
|
import logTypes = require('../src/lib/log-types');
|
||||||
|
|
||||||
|
const fakeLogger = {
|
||||||
|
logSystemMessage: stub(),
|
||||||
|
logSystemEvent: stub(),
|
||||||
|
};
|
||||||
|
const fakeDocker = {
|
||||||
|
createVolume: stub(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const opts: any = { logger: fakeLogger, docker: fakeDocker };
|
||||||
|
|
||||||
|
describe('Compose volumes', () => {
|
||||||
|
describe('Parsing volumes', () => {
|
||||||
|
it('should correctly parse docker volumes', () => {
|
||||||
|
const volume = Volume.fromDockerVolume(opts, {
|
||||||
|
Driver: 'local',
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
},
|
||||||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||||||
|
Name: '1032480_one_volume',
|
||||||
|
Options: {},
|
||||||
|
Scope: 'local',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('appId')
|
||||||
|
.that.equals(1032480);
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('name')
|
||||||
|
.that.equals('one_volume');
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('config')
|
||||||
|
.that.has.property('labels')
|
||||||
|
.that.deep.equals({
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
});
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('config')
|
||||||
|
.that.has.property('driverOpts')
|
||||||
|
.that.deep.equals({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly parse compose volumes', () => {
|
||||||
|
const volume = Volume.fromComposeObject(
|
||||||
|
'one_volume',
|
||||||
|
1032480,
|
||||||
|
{
|
||||||
|
driver_opts: {
|
||||||
|
opt1: 'test',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
'my-label': 'test-label',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('appId')
|
||||||
|
.that.equals(1032480);
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('name')
|
||||||
|
.that.equals('one_volume');
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('config')
|
||||||
|
.that.has.property('labels')
|
||||||
|
.that.deep.equals({
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
'my-label': 'test-label',
|
||||||
|
});
|
||||||
|
expect(volume)
|
||||||
|
.to.have.property('config')
|
||||||
|
.that.has.property('driverOpts')
|
||||||
|
.that.deep.equals({
|
||||||
|
opt1: 'test',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Generating docker options', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
fakeDocker.createVolume.reset();
|
||||||
|
fakeLogger.logSystemEvent.reset();
|
||||||
|
fakeLogger.logSystemMessage.reset();
|
||||||
|
});
|
||||||
|
it('should correctly generate docker options', async () => {
|
||||||
|
const volume = Volume.fromComposeObject(
|
||||||
|
'one_volume',
|
||||||
|
1032480,
|
||||||
|
{
|
||||||
|
driver_opts: {
|
||||||
|
opt1: 'test',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
'my-label': 'test-label',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
|
||||||
|
await volume.create();
|
||||||
|
expect(
|
||||||
|
fakeDocker.createVolume.calledWith({
|
||||||
|
Labels: {
|
||||||
|
'my-label': 'test-label',
|
||||||
|
'io.balena.supervised': 'true',
|
||||||
|
},
|
||||||
|
Options: {
|
||||||
|
opt1: 'test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fakeLogger.logSystemEvent.calledWith(logTypes.createVolume));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
5
test/fast-mocha.opts
Normal file
5
test/fast-mocha.opts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
--exit
|
||||||
|
--require ts-node/register/transpile-only
|
||||||
|
--require coffeescript/register
|
||||||
|
--timeout 30000
|
||||||
|
test/*.{ts,coffee}
|
Reference in New Issue
Block a user