mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-06-19 07:40:20 +00:00
Compare commits
1672 Commits
v0.8
..
s3-parallel
| Author | SHA1 | Date | |
|---|---|---|---|
| ff66310b73 | |||
| 44c718d7ed | |||
| b930c85290 | |||
| b9bed90904 | |||
| 06ff8718ad | |||
| fcb9bd7bd6 | |||
| 3e54a6dc7d | |||
| 1693863499 | |||
| 308fb80a6e | |||
| 641d16178f | |||
| 40ba104838 | |||
| cd30723750 | |||
| f7b4df2f32 | |||
| 463c34a38e | |||
| 660cee2ce3 | |||
| 4675589cf6 | |||
| 32f03bfd62 | |||
| d1bfd29dfd | |||
| 27ec594606 | |||
| f652a522fd | |||
| a794e87490 | |||
| 5b04d4fbe1 | |||
| 1566e193f6 | |||
| 601c8e9d52 | |||
| 8e5707dbcc | |||
| ad4d0c7b96 | |||
| a11e004943 | |||
| f605d86a4e | |||
| f8bde63081 | |||
| 887ce71005 | |||
| 87233ceafe | |||
| 27c15680e8 | |||
| cb72e2d70f | |||
| 2cafbc8484 | |||
| 6dbb28b2b8 | |||
| d8a4a28259 | |||
| 9a217171c8 | |||
| c67cafcf94 | |||
| f7057a9517 | |||
| ae5379d84a | |||
| c05068c2e8 | |||
| 22bc2f9d0f | |||
| c07bf2b108 | |||
| e447fc0f1e | |||
| e062df68c5 | |||
| 664a5cd675 | |||
| 9ef217b351 | |||
| 67bd15487d | |||
| ab18da351d | |||
| 1abb735bfa | |||
| 9397d8ab36 | |||
| 82300d6944 | |||
| cf3841e35c | |||
| 1a0bffdc51 | |||
| 666b5c9700 | |||
| 2eabc6045f | |||
| cc32e79f2a | |||
| 7074fc8856 | |||
| a7d85e5905 | |||
| cad4233d0d | |||
| 9b9894c07d | |||
| 8546cf31ce | |||
| aa0830ff0c | |||
| 4076941bd7 | |||
| 4170c9e995 | |||
| a862192bc4 | |||
| 5a18428666 | |||
| 0b5a627c84 | |||
| 65820cdf7a | |||
| 2c3a107e00 | |||
| e028db585f | |||
| d523ca8186 | |||
| f008f245dc | |||
| f2f3196368 | |||
| 29eccc9226 | |||
| 9abbd74a9f | |||
| 846fe5e08a | |||
| da29961052 | |||
| e5b8315859 | |||
| c6bb5f76f7 | |||
| fea7acb56e | |||
| 50d3676847 | |||
| 8830354027 | |||
| 2467674fca | |||
| 9691b0f518 | |||
| 005114839a | |||
| a5d322252a | |||
| b49630d6fc | |||
| 93650efddb | |||
| d87327835e | |||
| 0d90ff96b9 | |||
| b14595cb2d | |||
| e50a5e175f | |||
| a7d6782176 | |||
| eb6dd4d69e | |||
| a15d8a3f7b | |||
| 22cfa4c8c7 | |||
| 4e566b4692 | |||
| 9d0c7b5ade | |||
| 87a36633e9 | |||
| 0e0189f0eb | |||
| a880a88fc0 | |||
| 465312b8a0 | |||
| e319f3cd14 | |||
| e92afd8f78 | |||
| 280563caa8 | |||
| 3d8968eff3 | |||
| 1301847b7e | |||
| 09c56342d2 | |||
| ea80f6d49c | |||
| 622072bd50 | |||
| 1f469e23b5 | |||
| d8b9777b40 | |||
| e5e3c49ace | |||
| c6e0a06b14 | |||
| 75e5f95277 | |||
| a59fc6b8e8 | |||
| 4ff3c894fa | |||
| abfad37640 | |||
| 63b8cc9ad9 | |||
| a69c00a5bc | |||
| 4f229a5bcf | |||
| 83f7c869f0 | |||
| 397362bb1a | |||
| d5571c41c7 | |||
| 39921809ee | |||
| 68fe2bc852 | |||
| 398fec13b0 | |||
| 9fc7ebdac2 | |||
| 74bc3f5db3 | |||
| a5f8ce2503 | |||
| 2171c05ef8 | |||
| 8f8de4bd29 | |||
| c055611914 | |||
| 9b8f6b1d56 | |||
| 096fa47c6d | |||
| e677a2e84a | |||
| 69a1e2561d | |||
| 2df82e87b7 | |||
| cc1fc7ccfe | |||
| ba86851d07 | |||
| f9ae9b323a | |||
| 5e91b10c8c | |||
| 568345c396 | |||
| 8c3fe8dabb | |||
| ef6815222c | |||
| 0c76677b16 | |||
| 3b785e4165 | |||
| 320307f504 | |||
| 88ef8efba5 | |||
| 3fad19650d | |||
| 7d9f020ae8 | |||
| 2f540a8026 | |||
| b2d05828a5 | |||
| b7e91f0994 | |||
| 247f5e7c60 | |||
| 36121e5643 | |||
| 763b810ca8 | |||
| d80b905945 | |||
| e2cbd637b8 | |||
| d96ef0f178 | |||
| de181bef0f | |||
| 14e4f3dad8 | |||
| d2b9adf6f2 | |||
| 036422399a | |||
| 53c2f8b778 | |||
| 6050051e04 | |||
| 8c2ea639fd | |||
| 5ddb718eab | |||
| 9ca9569714 | |||
| 1357d246d8 | |||
| 03f189b62c | |||
| 8d8f4714c3 | |||
| c75c2c7594 | |||
| 17186b0c73 | |||
| 2aac7baf52 | |||
| bc090e1dce | |||
| 31ccce1343 | |||
| 147955c682 | |||
| 840b76228a | |||
| 8436001d5b | |||
| c86b888b0a | |||
| 0936922172 | |||
| 62a0a1a560 | |||
| 596f59d3c4 | |||
| e642847a82 | |||
| 26c14e218a | |||
| 26c775ccfd | |||
| d6284148f9 | |||
| 4c58266a87 | |||
| 2f06b0690c | |||
| 19d213d748 | |||
| c8fca7953c | |||
| 55d4d98353 | |||
| dd4f90e4c2 | |||
| 6647f6d0e0 | |||
| 9d05949d19 | |||
| 9da58b1ea2 | |||
| 38485a5b1e | |||
| bf4b660568 | |||
| c028d5e8cb | |||
| eafec74c29 | |||
| 74364544c2 | |||
| a4c53689ca | |||
| 0ceff44421 | |||
| f79423a4ee | |||
| c9309c926c | |||
| ee3124cfc6 | |||
| eb94211053 | |||
| bd01cd4033 | |||
| 21013a8317 | |||
| 451de79666 | |||
| 755fdfaca2 | |||
| f4057850b9 | |||
| a56f52ff18 | |||
| 4d6688d68e | |||
| 7a7ff1142c | |||
| 8cceed12f7 | |||
| f8f28e9554 | |||
| ac5ecf946d | |||
| d87d8bac92 | |||
| 9dffe791ad | |||
| 3057aed571 | |||
| 14c29ff912 | |||
| 73cdf5417b | |||
| fa0d2860f0 | |||
| dcbb2a06a5 | |||
| bd64232eb6 | |||
| 767bc6bd0b | |||
| 8ddb81eb5c | |||
| f16a68f59c | |||
| 0e6f9c38fb | |||
| 037da55de1 | |||
| 0666f8784f | |||
| 01f16d35c2 | |||
| f8e0a8d880 | |||
| ae0fa20aa6 | |||
| cefc09a41b | |||
| 7742980426 | |||
| 57639c4adf | |||
| 75ca51b23b | |||
| ce2966e547 | |||
| 861260198a | |||
| 3e7bec5604 | |||
| 14a343a0d7 | |||
| 704af8f2f0 | |||
| ac2dd1dfd3 | |||
| df18133179 | |||
| be6d06a653 | |||
| a998755245 | |||
| 98c82a3684 | |||
| 4658bec08b | |||
| 33047c2c55 | |||
| b2b7f11d17 | |||
| f0ad0f9496 | |||
| d6a156b181 | |||
| 880f487093 | |||
| bce54d5878 | |||
| dbc336f921 | |||
| 71085969f5 | |||
| c35cd783cf | |||
| 38ea720fc5 | |||
| 06b2b920da | |||
| a3078fa93e | |||
| 0bc45c822d | |||
| af5b04b24f | |||
| 678d0c61f1 | |||
| 37ee41af67 | |||
| 8b8eb57555 | |||
| d6efe8636e | |||
| caf55bb8e0 | |||
| cc4798472f | |||
| e3a95d5c4e | |||
| 997ffe9c31 | |||
| c6c607a406 | |||
| c25693b009 | |||
| fca153cc8b | |||
| 06cbd29d0d | |||
| aff7b0db50 | |||
| f7ff964085 | |||
| 94dc4f2365 | |||
| 09954b2c73 | |||
| 87fc8c16af | |||
| 75530810b5 | |||
| bdabfa1071 | |||
| 26a9219f7d | |||
| 5b92336668 | |||
| fb538333fa | |||
| 1d1bd41bb8 | |||
| 96e60ae540 | |||
| 4195ad90bc | |||
| eaa363eb82 | |||
| 795870bca5 | |||
| 7e73165409 | |||
| 2d6d371292 | |||
| d9168ed723 | |||
| 57fc7f7098 | |||
| 20c81d7f9a | |||
| 67d04ca878 | |||
| f5dda668e3 | |||
| 769150dec6 | |||
| 02d080955b | |||
| 04739a41fa | |||
| 9771747916 | |||
| 497196886a | |||
| 056df39a3c | |||
| 5e86a0b9e6 | |||
| ba2c86361d | |||
| b92ca5d6e5 | |||
| fe2c623638 | |||
| 0e9f047c37 | |||
| 582201ab7c | |||
| 88f4101866 | |||
| 373a157163 | |||
| 0178093f6c | |||
| 8e8cf90a71 | |||
| a22dc9be1b | |||
| 2306993b7b | |||
| c248dc1803 | |||
| 53f96c98ad | |||
| 98b1ed07d1 | |||
| b342af0d96 | |||
| 52faf78324 | |||
| 8fd48e9fa9 | |||
| 32a3943821 | |||
| f7f220aa18 | |||
| 372ce3c4bc | |||
| 95915480a0 | |||
| d2332e6452 | |||
| 1428f54a02 | |||
| feb87c0f19 | |||
| 934fa0598b | |||
| 6d6761e234 | |||
| ab18d4835b | |||
| ff8a02959c | |||
| 37a9fbe530 | |||
| 735d7a4d61 | |||
| 40eb4b4751 | |||
| 0a6e8e3c9e | |||
| 556d7fa4b8 | |||
| 5718f3f2f5 | |||
| 0251fddae4 | |||
| 0215925608 | |||
| 696b78f207 | |||
| 674f4f784b | |||
| 83a05a1900 | |||
| 48a0bca35e | |||
| a7690c375e | |||
| a0bd32a39c | |||
| 0b3dd2709b | |||
| ff1557afee | |||
| 67771795ca | |||
| 7a01c9c62d | |||
| 9768ecef22 | |||
| 640c202ee5 | |||
| f10acb3df8 | |||
| 5b74f82edb | |||
| 59bf4501e8 | |||
| f29449db14 | |||
| 78172d11d7 | |||
| f42ff697d4 | |||
| 57e2c5c670 | |||
| 8b41ec48c8 | |||
| 49ff832f94 | |||
| 09a44ba409 | |||
| deae90485a | |||
| 4a0bdcbb64 | |||
| 9f1860dff7 | |||
| fe25414b45 | |||
| 49184c9163 | |||
| 440c3debdc | |||
| 8029305d32 | |||
| 02bdb7c76a | |||
| 8d537b4e3e | |||
| 11401ca472 | |||
| 66429bff45 | |||
| 8114786179 | |||
| d8c1e432c6 | |||
| 3a286ae07f | |||
| a93ccd4100 | |||
| c1f7e5fe96 | |||
| d16110068c | |||
| 4661913265 | |||
| ecc88e7a40 | |||
| 5cf8c54cb2 | |||
| b758033ccb | |||
| 13f4bb441d | |||
| 1af09069f7 | |||
| b5bf2cbcda | |||
| fb3436b23d | |||
| 1a3cfea348 | |||
| c33141d49b | |||
| f9325fbc91 | |||
| 810df17009 | |||
| 19255debb9 | |||
| 1ebd37f9ad | |||
| 8e37813129 | |||
| 91574b53d9 | |||
| b8e0aba3cf | |||
| 0eccaf2b60 | |||
| 859edb10cd | |||
| f0bf519d36 | |||
| d8cfafccc9 | |||
| 34c1c1f83a | |||
| 3e1485faf5 | |||
| efc5573b8e | |||
| 45035802be | |||
| 2d97ba2bbd | |||
| 05fed16f6d | |||
| 9b1f4272c0 | |||
| 1987220f1e | |||
| 47696a3303 | |||
| 787f954833 | |||
| e9bdb983c8 | |||
| b4cd86aa14 | |||
| 4bd26f5977 | |||
| 57ff7c69cd | |||
| c843709eab | |||
| 4bc2180eed | |||
| 5353890b24 | |||
| 79975bf2b6 | |||
| 8d09c202db | |||
| 27013c0b2b | |||
| 756c499314 | |||
| 6c6d1b18ba | |||
| 8cb1236a8c | |||
| 5636a9990b | |||
| 8ab8398c50 | |||
| 53c4a567c0 | |||
| ff8f79f883 | |||
| 06be6f23a6 | |||
| 24e62b58bc | |||
| b37a3341e8 | |||
| eee6ccc311 | |||
| f233a21787 | |||
| 940538e39b | |||
| 3a29e08ff2 | |||
| 2a2e35c096 | |||
| 287a947c62 | |||
| 32595e7cb7 | |||
| 9deb031c44 | |||
| 6be4f5e8d0 | |||
| b5b0a52cbe | |||
| a0af6a25aa | |||
| c2ee086487 | |||
| 43a0ceb350 | |||
| 50eaf6c0bb | |||
| e564b7c150 | |||
| 943e76ae8b | |||
| 72a7780054 | |||
| 1001ca92c8 | |||
| d060ad7200 | |||
| eeb5bd79d0 | |||
| fad660450c | |||
| 01893a492f | |||
| e61a4dd53c | |||
| 183e6ec436 | |||
| ebd5aa5fe9 | |||
| 1b6e5e5b3b | |||
| 7b7ebc5711 | |||
| 92e16c81df | |||
| 5c1fd4dd2c | |||
| d7cc9b89d1 | |||
| a69aa7c533 | |||
| a71186bcc3 | |||
| aeef41bf70 | |||
| 99dbe31d20 | |||
| 5ca3a97bd3 | |||
| cfcab13c2a | |||
| f1649a647b | |||
| 11deb9425b | |||
| 3aaf0a8c44 | |||
| 322e5c1587 | |||
| ed45c44931 | |||
| 889fcc2158 | |||
| f155ed3ba9 | |||
| 0d20c59a98 | |||
| ae61706a34 | |||
| f4a152ab22 | |||
| 972bf6b3cf | |||
| 18203c614d | |||
| 40c242f9d1 | |||
| ee4c83e323 | |||
| 214e9075ad | |||
| 847fd90e36 | |||
| ecc055180c | |||
| 1501a4e531 | |||
| 8f53e01749 | |||
| 1df8cff842 | |||
| 76744ead86 | |||
| f6a7030654 | |||
| 7c8dd7362d | |||
| 0ae9884836 | |||
| 95ca6fb376 | |||
| c9b1782d62 | |||
| d1102e2e9c | |||
| 9c6f896666 | |||
| 0fdba29d51 | |||
| f74217ed9c | |||
| e25ade8af3 | |||
| bece12ad4d | |||
| 77e02bf7a3 | |||
| 90932cdac5 | |||
| 5b5307cc15 | |||
| aaa622288c | |||
| dbf1ac7867 | |||
| c187b0d52c | |||
| 8e62195eb5 | |||
| 0c749922c9 | |||
| ecc41f0c0f | |||
| 81582bffd2 | |||
| ced5ac7876 | |||
| 2020ca9971 | |||
| 352f4e8772 | |||
| 71fd730598 | |||
| e90ac6767f | |||
| 268c39ea8c | |||
| b3d9055059 | |||
| 904265120b | |||
| 47930a4214 | |||
| a59cad6f20 | |||
| 393d1a6888 | |||
| 42cfee2c09 | |||
| afdc10b919 | |||
| af899149c7 | |||
| abf8abb59b | |||
| f0a85b2b6e | |||
| 515e5532c8 | |||
| ff3bf4b180 | |||
| 1d4e6183be | |||
| 69d473ea6f | |||
| bfc86d3b30 | |||
| 3ce27743ae | |||
| c9f5763a70 | |||
| f61514edaf | |||
| 2aca913e92 | |||
| 26254a0ad8 | |||
| 6f130e1583 | |||
| 35ad6cacc8 | |||
| f519ecded7 | |||
| 4b2efeec7a | |||
| a687df2f4f | |||
| 29deae6fe0 | |||
| f9f1c8ee75 | |||
| a0544dc2b5 | |||
| 0a1798869a | |||
| 751fd2f9ba | |||
| 954b222fb6 | |||
| 4c04e77489 | |||
| 152538ccc1 | |||
| d955b06f03 | |||
| db19a56458 | |||
| 6539e1b856 | |||
| 8046fb1eb9 | |||
| 0302e39d57 | |||
| c29ccaadbc | |||
| d2d168f363 | |||
| cf98718a79 | |||
| 47bda055e0 | |||
| c1e577c1ac | |||
| 5b98039291 | |||
| 5a23f71a7f | |||
| c46f12f0d6 | |||
| f89350e6cd | |||
| fd404064c9 | |||
| 21029c326b | |||
| e8ec6385f3 | |||
| 1361bf20dd | |||
| 5d98546e1d | |||
| ff5eb53f48 | |||
| 814d4dbb51 | |||
| 2c68175b5c | |||
| 551a370c13 | |||
| 1afcd68e01 | |||
| cc30ef3ee2 | |||
| 235e35a2f3 | |||
| 4c54f967b7 | |||
| 8925949be3 | |||
| e96372c999 | |||
| e5d9d27069 | |||
| 853c990b6e | |||
| 86c1ffab2a | |||
| 952287a787 | |||
| b5d90b7b13 | |||
| 3e06af8515 | |||
| eff2e56d0d | |||
| eaac04ccf6 | |||
| 894192851e | |||
| f93bc6ef0f | |||
| 035d5314b0 | |||
| a40cfc679c | |||
| bda6eb4200 | |||
| 70f7d7409a | |||
| 48635c8057 | |||
| 122ff609e8 | |||
| 30e94064e5 | |||
| d60e575af5 | |||
| 91c3ed8ec4 | |||
| 0dc49d2a70 | |||
| a83dea705f | |||
| ed7a960e31 | |||
| 9bf1a44f75 | |||
| 8ecd01be1f | |||
| 4933e3caf4 | |||
| 4a9a5bc713 | |||
| 7412b84e04 | |||
| 3775d69a60 | |||
| 4cf57ae84d | |||
| ef2541776b | |||
| 78082bc10f | |||
| 5342e549cd | |||
| e2d1e9a7df | |||
| 9aa9917952 | |||
| c1cdb69f56 | |||
| 5b8c909ac3 | |||
| 20b038bfb7 | |||
| 9f9a1a138b | |||
| 4cb9ac5357 | |||
| cd76e481fd | |||
| 8e309b57b3 | |||
| beb9d43f4d | |||
| b281819cba | |||
| 6826efc723 | |||
| 370e3cdfea | |||
| fb8b05e7fd | |||
| 8c94973cf9 | |||
| 787cc8e3ee | |||
| ff51c46915 | |||
| 0914cd16af | |||
| 9b28d8984f | |||
| bd4c3a246d | |||
| 914ddf4859 | |||
| 2fa3adee1d | |||
| fd83c1a5bf | |||
| de2be9b8ae | |||
| 209b030502 | |||
| 5a65ce6adb | |||
| 79a7cf864e | |||
| 19f7b0fe8d | |||
| 2b7bb24c92 | |||
| faf2d588b1 | |||
| 8e02a03170 | |||
| d13de0464e | |||
| c0528888f4 | |||
| b4efe6a810 | |||
| f09a273ad7 | |||
| 3cd168c44d | |||
| b0ab8f417d | |||
| d7ccf95499 | |||
| 6ab5e60833 | |||
| e63d74dff2 | |||
| 25d7d7c037 | |||
| 1c7c07ace7 | |||
| f7f42a9cd8 | |||
| 1e7731c317 | |||
| 208a2151c1 | |||
| 4a6d53e16d | |||
| a778ff8903 | |||
| bb42a2158d | |||
| ab2f5420c6 | |||
| 174943cd0f | |||
| 0bc66032d2 | |||
| 899ed92ebc | |||
| 129eb8644d | |||
| d582f9bab2 | |||
| 0f1575d5af | |||
| f9c0d99790 | |||
| 1f56fb86e3 | |||
| f9d08e1377 | |||
| cbf0416d7e | |||
| 2422d3ab40 | |||
| 960cf76c42 | |||
| af2564c580 | |||
| b385b1e975 | |||
| ce1d4b852a | |||
| c43d31f693 | |||
| e4259c5045 | |||
| 993dd2ad1c | |||
| 3201244d9b | |||
| f4dc87fa44 | |||
| 24a027194e | |||
| 62c4dc1472 | |||
| b7f74b4e55 | |||
| 2da853dcbe | |||
| 0438a7c76b | |||
| c86c3a803f | |||
| 19db62d74f | |||
| 0146411483 | |||
| b731e17850 | |||
| bb66b2296d | |||
| c75ef8546e | |||
| d80c2b6104 | |||
| ec4bf35647 | |||
| 669d99bebc | |||
| 715af5950f | |||
| 7a5ac3dbc2 | |||
| ae61cbb4c0 | |||
| bde6e6bda4 | |||
| a656241d5e | |||
| 7ae5a12f4a | |||
| 769e984ef4 | |||
| 98e75f6d97 | |||
| 586f879e80 | |||
| e2112670bf | |||
| 060c6669c1 | |||
| aa02c5cbe9 | |||
| 77d7c3871a | |||
| 67e38955ae | |||
| 26098f6c8d | |||
| 021b6f694b | |||
| f0a370db24 | |||
| 2e7f624b34 | |||
| b63c0c7dfc | |||
| 906cbf1e6f | |||
| 5aefc741f2 | |||
| 5c28ea3064 | |||
| 70cd11e30f | |||
| 94a72b23ff | |||
| d08be990ef | |||
| ca5b7758ce | |||
| bb1def2910 | |||
| 673abae1be | |||
| 3b8c067e70 | |||
| 528459eeb2 | |||
| bc1ab4e55c | |||
| 5aefd0b393 | |||
| 7bc53a4253 | |||
| 8b12dccd76 | |||
| 952afb6040 | |||
| 56ca5e9e62 | |||
| a834461752 | |||
| 37166af321 | |||
| 2c91bcdc30 | |||
| e2d6a53de5 | |||
| 89537b1521 | |||
| 152b3cae90 | |||
| f104e53fd4 | |||
| fd99ae0e59 | |||
| 4b6c159e3a | |||
| 50f8cfbc15 | |||
| 22848b010d | |||
| 3b5840e248 | |||
| f955707201 | |||
| 86dc10028f | |||
| a64807efda | |||
| 61e00b5fbd | |||
| 1b2fccb615 | |||
| 702c1ff217 | |||
| d1b2814ec6 | |||
| ec57d1786c | |||
| 9f7c1f90ec | |||
| e23e30eb44 | |||
| 2b4a61b84c | |||
| fbafde6e27 | |||
| 14e5a75d35 | |||
| ea32d8627e | |||
| 814a0498df | |||
| e45f85cc1e | |||
| 1e9c032072 | |||
| c741cca5f2 | |||
| 72ff71f59c | |||
| 699323e2e0 | |||
| fb5985bbbe | |||
| 5a9f4bee12 | |||
| 4717793d8e | |||
| de38011dd2 | |||
| 0f4bbc4752 | |||
| 86a1c41e5d | |||
| 021b8c4cff | |||
| bcacb7b7f0 | |||
| 747b9752ce | |||
| b0be6c8a7a | |||
| 58c7358113 | |||
| b1a2523ef0 | |||
| b7323db31b | |||
| 2e52692ba6 | |||
| 0075ead526 | |||
| 6df4a746f1 | |||
| 074904ee92 | |||
| 108b0ea226 | |||
| 9a704de43b | |||
| 7dfc12d138 | |||
| 9000446663 | |||
| 814ac6c28c | |||
| d1a284298f | |||
| 9509629bcf | |||
| f1882cfe2c | |||
| 90e446ec16 | |||
| 464ed8269b | |||
| 57a51d94ed | |||
| 53c557271d | |||
| b6fe16095b | |||
| 6a1c439325 | |||
| 06b0be7bad | |||
| e5acf22285 | |||
| 5f904a164c | |||
| 9a30a11786 | |||
| d31144b9ae | |||
| c7a3a10846 | |||
| 2be0b7859d | |||
| 65528fc357 | |||
| 5a713534c6 | |||
| f89e322ece | |||
| cd6075ba94 | |||
| 77033df27b | |||
| b8c5303fdb | |||
| eaab66da58 | |||
| 2a8aff9746 | |||
| 797b2dd996 | |||
| e65fff058c | |||
| 548dcdb242 | |||
| 9a65bbe12c | |||
| 72d741a9b7 | |||
| 2f5bf96fc9 | |||
| 4d3b42eb11 | |||
| 5b85522400 | |||
| 5f96abc271 | |||
| 0e6ee35942 | |||
| 14798b2063 | |||
| cef4fefc40 | |||
| 181806a9a2 | |||
| 99a205c716 | |||
| dd78c026c1 | |||
| 20516dbbc2 | |||
| ba80f377a9 | |||
| dc7bbf35eb | |||
| b3b0dbb217 | |||
| d76259496d | |||
| aa3a2ab595 | |||
| 0f1fd1bca6 | |||
| 581876df9a | |||
| d0101be955 | |||
| caa5433787 | |||
| 9125745416 | |||
| b893c0a7ca | |||
| 02ac416561 | |||
| 00bb0ca8f3 | |||
| 2d0baef3b1 | |||
| 3ea803e3bb | |||
| 2fa9d7402f | |||
| 3c04c56639 | |||
| 7cd4b7a908 | |||
| 9242ea4d72 | |||
| 58790dadc6 | |||
| 182fbdef50 | |||
| 4fb57f65fb | |||
| 12e2982362 | |||
| 60fb415150 | |||
| 75c4d6da3b | |||
| 1aa88701fb | |||
| 43ddcd27cb | |||
| 9cb2a302f8 | |||
| d836334767 | |||
| 565fcf4390 | |||
| b7490fe909 | |||
| b2bf4f7884 | |||
| e504fdcd54 | |||
| 3efa1052fa | |||
| 2e488608ca | |||
| 674a0e84be | |||
| f5e1e194b3 | |||
| b4f3573d11 | |||
| 4718625388 | |||
| d6b4b795a5 | |||
| 2bd0b786ea | |||
| 092a7ed8f3 | |||
| 438e206b3d | |||
| 7498fd8fc8 | |||
| e07912770e | |||
| bb2db7e500 | |||
| c94e048198 | |||
| 3b4c06d28d | |||
| 15618c8ea8 | |||
| a037615962 | |||
| 5d301fb1b7 | |||
| 8a4d866810 | |||
| b98abcc049 | |||
| 10e0966edc | |||
| 340d1fdd7c | |||
| 14d4a2706c | |||
| 308ea83cc0 | |||
| 9c018ce636 | |||
| 359cda9d99 | |||
| e682639b20 | |||
| afd2c5fcea | |||
| 5a1d006850 | |||
| 67c26368ee | |||
| 1885cbd6a2 | |||
| 79d68ec3a7 | |||
| f43801cb96 | |||
| 46c2182ade | |||
| 5ef45bddda | |||
| 0d94f29c27 | |||
| 04b7543dea | |||
| 9051f13ce6 | |||
| 1b704db5c0 | |||
| 2d66a4ca0a | |||
| 182c21e38c | |||
| 9a767b7631 | |||
| 3756db2491 | |||
| ff8e4a8659 | |||
| aec6c2f2e2 | |||
| d611d0d829 | |||
| b4deedda01 | |||
| 0f14143141 | |||
| e5198178a5 | |||
| 1c44b4f787 | |||
| 6d2f265980 | |||
| 325d391007 | |||
| 91a3dc9e94 | |||
| 31f4af5722 | |||
| e0aaa8bb80 | |||
| 50035d5bc4 | |||
| 985f1a17b5 | |||
| 72ac1bc33c | |||
| f0d6b1c29f | |||
| bd5fc8ae62 | |||
| 9ca81ff3bc | |||
| d9607cf88c | |||
| 4f56f34d82 | |||
| e2956a84ce | |||
| 00a9eb72d8 | |||
| cbc8051c5c | |||
| a27b489ba2 | |||
| 790d85881b | |||
| d6a3917141 | |||
| 35e2253944 | |||
| a584b2e058 | |||
| 587bfd742f | |||
| 84ef963d7d | |||
| e70ef0a518 | |||
| e05768737f | |||
| a626e4693b | |||
| 4d9b4298d8 | |||
| 4cca7272ce | |||
| e9b2c18e2f | |||
| cbb576cbcc | |||
| bcc83bff31 | |||
| 68da8a674a | |||
| cafa82f018 | |||
| 83a9c394f3 | |||
| 2c0a1b836c | |||
| 28ae18792d | |||
| 2811ad02d5 | |||
| ab20c2d329 | |||
| d137bcf8d4 | |||
| 3674e1adee | |||
| 05a5e69483 | |||
| 5e9515a912 | |||
| 84a6d573f8 | |||
| 6228a399cf | |||
| 0e9f966dd1 | |||
| 07fde3177b | |||
| f9377b2aa6 | |||
| 499ab35012 | |||
| 3c95f92b95 | |||
| d7a7aa93a4 | |||
| 58ab4e8902 | |||
| fcd453118b | |||
| 7d179dd405 | |||
| 20b874f81f | |||
| e3f1880ad4 | |||
| 39293d7faf | |||
| c13eb99925 | |||
| 211ac0501f | |||
| af2f7baf63 | |||
| 3c25db3ffb | |||
| 12a6b0ceb8 | |||
| 0d041898ca | |||
| 982c093fbf | |||
| f54e798eac | |||
| cafb89f30f | |||
| f0360cf2d3 | |||
| 1be8d39105 | |||
| c026106352 | |||
| c507d0620b | |||
| f84672239a | |||
| c9bd7b4b5d | |||
| 470165a419 | |||
| 9de9fbe6bd | |||
| e396a2e6c3 | |||
| 829ea2e65c | |||
| 39d2d273dc | |||
| 5a3e660c0d | |||
| 589dc93380 | |||
| 33357c1fe4 | |||
| 5ce6bf8718 | |||
| d7bcf372c4 | |||
| 3aa044d722 | |||
| a9a5a73dfd | |||
| 66b44e68a9 | |||
| 51213899b7 | |||
| 7a7c9cd26c | |||
| 1b9ab46c5f | |||
| 2cbed28446 | |||
| 39aa0fdbfe | |||
| c983810e2d | |||
| c798db8056 | |||
| 1e4a80252e | |||
| bae3f949b4 | |||
| 7a7b981d4f | |||
| 2ffefeb1e0 | |||
| 1941418c10 | |||
| 186bb2dff0 | |||
| 2308632683 | |||
| ee21b69402 | |||
| 01512df853 | |||
| 7dcc0d597d | |||
| 154ef7fe65 | |||
| 4601f07349 | |||
| b7b9f12c88 | |||
| b48e8425ec | |||
| 3ce8227122 | |||
| 0bc3f71d27 | |||
| c1d4c0fb88 | |||
| 8078f3b588 | |||
| 5dd11a2ec2 | |||
| cc34a021ce | |||
| 10c096fbb6 | |||
| a85d8b6f90 | |||
| 5566111a7b | |||
| 6994e35119 | |||
| 4eedb62418 | |||
| 1f3cb2db5d | |||
| c40025a335 | |||
| 4171a73995 | |||
| 29e5f4ca10 | |||
| 05f6c75743 | |||
| 45d187bc14 | |||
| bc7903f86e | |||
| 72d233b587 | |||
| 2535367c3c | |||
| f4ff8d957f | |||
| 7bad358408 | |||
| 94b49818a1 | |||
| a245b722a8 | |||
| 8dc6a14766 | |||
| d66185ca03 | |||
| c3acabe303 | |||
| 4697d8eaf8 | |||
| 8bf71a5561 | |||
| 898cbd2c83 | |||
| 62762f1616 | |||
| 4d38e0bc87 | |||
| 25f9c29f00 | |||
| 096b30b5e8 | |||
| ac475c0a10 | |||
| 60800b5f25 | |||
| 36a4d78162 | |||
| 50cf2b49bd | |||
| 675d35c7a1 | |||
| bc469eecfb | |||
| bc01d9ed5b | |||
| 7a5be6736d | |||
| eb48460b7b | |||
| 85b4a8b1ae | |||
| e6bad637fd | |||
| 47b5cc27c8 | |||
| ca16841223 | |||
| 800c5c1e06 | |||
| 7fd8bd0171 | |||
| 4707efe4d6 | |||
| 8ae61f9448 | |||
| a138d0111d | |||
| af1adb44ce | |||
| 4ddf85bbc1 | |||
| 9978595c59 | |||
| 2943422d5d | |||
| 91219e3a0a | |||
| 7f8db9087a | |||
| aa16899c60 | |||
| 16a0d0d428 | |||
| 66f51d2b17 | |||
| 92c844b8ac | |||
| 2b56a3937b | |||
| 9cea9b6470 | |||
| e3e68b9f22 | |||
| d56839664d | |||
| 516dd7b044 | |||
| 53e59d3765 | |||
| 11d828b3b1 | |||
| 07472bec50 | |||
| f737787c01 | |||
| c6c1012330 | |||
| 070347295e | |||
| acd8d4a6ea | |||
| c9768416ed | |||
| bfb9ffad1d | |||
| 9cfe1307e3 | |||
| db8595711b | |||
| ce0001f94c | |||
| b102562478 | |||
| 69cbe10690 | |||
| e3e4ea91bd | |||
| 02c582e227 | |||
| 6e96cd29dc | |||
| 17044f43dc | |||
| 5d3b170ffc | |||
| a0f7b2242d | |||
| b8e7ad9022 | |||
| 1b80d55ea4 | |||
| a0832adfa5 | |||
| f17d398e8f | |||
| bc3b2ed5a8 | |||
| 07cf8925f9 | |||
| 564ebf3130 | |||
| dbee214259 | |||
| 6267c5cb25 | |||
| 4c06e26d85 | |||
| f2dc4eeec9 | |||
| f86e6ebf1f | |||
| 0d208c93bc | |||
| 485f311498 | |||
| 46b0d637e2 | |||
| 5a71847b7f | |||
| 38a9917815 | |||
| 4456f8da57 | |||
| 970b1a424a | |||
| edffa24658 | |||
| 3040e7360a | |||
| b948180b4e | |||
| f58d2627c1 | |||
| ab0d77f6f9 | |||
| 33d6cd8c0a | |||
| 4eef4f1803 | |||
| c75d4c749c | |||
| c8a1b9a1f0 | |||
| d8d8973ad5 | |||
| d1ded5c224 | |||
| 155a801bc1 | |||
| 6212b39264 | |||
| 92116072c2 | |||
| b0ab39e07f | |||
| 4bf27d1dae | |||
| 207ebffbb8 | |||
| b0dd83335f | |||
| 8df6457931 | |||
| 7d2a396b27 | |||
| d5df049630 | |||
| 7c62a706c4 | |||
| 96948d6f18 | |||
| 43e6498713 | |||
| 91561b40f6 | |||
| 0e8ea6363a | |||
| 345fa02fdc | |||
| 064adbae57 | |||
| ab458f4dfc | |||
| 0fdee9cbf6 | |||
| 50e3e93166 | |||
| 570835227b | |||
| 781c22e256 | |||
| babccfa21f | |||
| 891113717e | |||
| bfb9045fa9 | |||
| 1c6b174b8a | |||
| fb27fb01ea | |||
| b6327ecc43 | |||
| af71b9541c | |||
| f31b5ec3f8 | |||
| 6becd5a3aa | |||
| 653255c728 | |||
| 5f0ce38161 | |||
| d100033b46 | |||
| d008cabf07 | |||
| f3214144a4 | |||
| d41841b84a | |||
| 2a95e0eb1b | |||
| 81f8ab2691 | |||
| 4e61db8d0f | |||
| 273d4cfa1b | |||
| d290950d4f | |||
| 2ade5b8a7f | |||
| fcd4429370 | |||
| 8e62620880 | |||
| 511fb439ac | |||
| 34ea7e8d61 | |||
| 543c986885 | |||
| f939532461 | |||
| aa4e225455 | |||
| 65541a1df2 | |||
| 0a74b50a12 | |||
| 902c6487da | |||
| 1d5b7f59cf | |||
| 1c45c79cc1 | |||
| 85c5aeddae | |||
| a95e409f52 | |||
| 53b571d6fc | |||
| 7a8af044ee | |||
| a667744502 | |||
| aa53b8da15 | |||
| 52f7c83f95 | |||
| d7665119e4 | |||
| 587086beb4 | |||
| 644d24d1cc | |||
| 2fe8cfdc12 | |||
| 2ecd933d50 | |||
| 90ea1111e2 | |||
| 165a1c53b5 | |||
| 876935050a | |||
| d9a1299f6b | |||
| ff52d2655a | |||
| bc438ff694 | |||
| 0db3cac281 | |||
| 9ed6e8dbbd | |||
| 7294241c08 | |||
| 60cca0245b | |||
| 75b860e0b1 | |||
| 7f5a7323a6 | |||
| 1069458aee | |||
| 76edf9649b | |||
| 8b0d293c6a | |||
| 281d0dd68d | |||
| cfaa8f3881 | |||
| f1b6841757 | |||
| b966b2eabf | |||
| a4e573bb07 | |||
| 067d197dac | |||
| 18d04c7977 | |||
| a29453805c | |||
| 05b1296144 | |||
| 29e33069aa | |||
| ee05bb23c9 | |||
| 505da096e6 | |||
| 77be7b9e3b | |||
| ffafed472c | |||
| 8c9cc41099 | |||
| f50e008763 | |||
| 64b04c2764 | |||
| d6c7a9a89c | |||
| 0339f0fe23 | |||
| fcedaa3fc5 | |||
| 7acfc84c9d | |||
| 02b937ad17 | |||
| 7ad1c1ad17 | |||
| 640bd2b530 | |||
| 06149ef2bb | |||
| b271e8fe31 | |||
| efc6ab27db | |||
| 05c063839d | |||
| fd30b37a0e | |||
| 9738687116 | |||
| 219315c01d | |||
| 62f44e53fd | |||
| b25f8e438c | |||
| f14fce01e9 | |||
| a790770a19 | |||
| 7bb052ac37 | |||
| 631fe44c6b | |||
| ca319c804e | |||
| 3e368690fd | |||
| 339bf0a90b | |||
| c5b48f0362 | |||
| d5f50732c1 | |||
| 9d973aeceb | |||
| 7caeac7515 | |||
| 7f6a52019f | |||
| 16101b56fe | |||
| a294a91685 | |||
| cf644289a3 | |||
| 33c905ce02 | |||
| 8fdc222196 | |||
| 1e4d825d36 | |||
| 76bf7cba04 | |||
| 08bc5ac934 | |||
| c160cbccc7 | |||
| c473a5cba8 | |||
| 84801bce78 | |||
| b95b3473bf | |||
| f1d5caab8b | |||
| 6a973554ad | |||
| 698e239f45 | |||
| 205297d0b8 | |||
| ba4669a9c4 | |||
| 8bda799545 | |||
| 6c28e3aca8 | |||
| 901babe500 | |||
| 0c6f38ab08 | |||
| a131d6093c | |||
| 974cec3e73 | |||
| 442c5f090f | |||
| d04f08c1cf | |||
| 767c7ca0db | |||
| dd27aad751 | |||
| ddfdeaf2d0 | |||
| 4c51350517 | |||
| 40e48c963a | |||
| c44d347540 | |||
| 4a54bff225 | |||
| e39736153d | |||
| f032196d70 | |||
| a030e24b96 | |||
| c2993c6691 | |||
| 7d4a70ba25 | |||
| 38dfe3435a | |||
| 313c71dff6 | |||
| a88d92436f | |||
| 9d298dee51 | |||
| 9abc772b16 | |||
| 2f1df39204 | |||
| 0f328ec1fe | |||
| 78b6d6ca7b | |||
| 5cd3c33854 | |||
| 9af76843b5 | |||
| 53506124a4 | |||
| 9bbf9c7b13 | |||
| 82e6e8242e | |||
| 2bf11a556c | |||
| c62828bf14 | |||
| b53cf7e710 | |||
| 780277d0a6 | |||
| a6f5631542 | |||
| 52b1501ec0 | |||
| c9339f5cca | |||
| a9c23fb4aa | |||
| 72e3eaebfe | |||
| f3bcaa6cfb | |||
| 1c8f1517f8 | |||
| 50ae34cc19 | |||
| 8cc7d1345b | |||
| 0791c88a02 | |||
| ba08ffe38b | |||
| 1bec1e4dc4 | |||
| bcf8074f31 | |||
| 6a2d564eee | |||
| 709e14ecc1 | |||
| 5b1f446a6b | |||
| f41146c750 | |||
| d56ac81fd6 | |||
| fb213ef6eb | |||
| 933b019f71 | |||
| 6293ca3206 | |||
| d46d8de5f7 | |||
| 4e3284cd98 | |||
| 10876b99f5 | |||
| 61d31ce7c0 | |||
| e0f284d68f | |||
| df887d871b | |||
| 99f6ffe1ca | |||
| 138f9f7994 | |||
| 3886db9d4f | |||
| b877e06a02 | |||
| 38f4fc209b | |||
| b223acdecb | |||
| cc8a87b448 | |||
| ee3d414ed5 | |||
| d791aa0f15 | |||
| 393ae8adbd | |||
| 7037c6be7e | |||
| c10645f4f2 | |||
| 27da1015af | |||
| 78b0fe0e90 | |||
| 4651e41247 | |||
| a6c40f3193 | |||
| 3e138fd6db | |||
| 3c20b5472e | |||
| 8b782ce370 | |||
| a160a39d53 | |||
| 1c4b44e772 | |||
| b4b03f2752 | |||
| 1d21d3cfeb | |||
| d2ce33e66a | |||
| f0fbb8259b | |||
| 962c4a842d | |||
| 54e21afee7 | |||
| cc3f5149c6 | |||
| c8713aa412 | |||
| 02a82f3545 | |||
| c573746896 | |||
| 813b9593fa | |||
| bc68513708 | |||
| c4692bec3d | |||
| c53060d95a | |||
| 22c656d18e | |||
| 4d622e467c | |||
| 36326788b0 | |||
| 782ac1a36a | |||
| 8ca07d9acd | |||
| 4a57fe3c39 | |||
| 7579f1998c | |||
| 67a31d5eaa | |||
| 5b9d287b62 | |||
| 775670181c | |||
| 2a3bd5546a | |||
| 197e230ef1 | |||
| c6eeac11a4 | |||
| 90d3b623b4 | |||
| a59c2ac859 | |||
| 103fa5310f | |||
| 71b7de7a63 | |||
| a937ebc744 | |||
| 925882b253 | |||
| 615a5ee3f9 | |||
| 4a6d6a85f7 | |||
| 2937435960 | |||
| 2f3b5f5a51 | |||
| 5b4563f250 | |||
| 5da4bde428 | |||
| 42c4644be3 | |||
| 1845c493f4 | |||
| 8a0f754fe2 | |||
| 77bb4d423d | |||
| 1d483dc817 | |||
| a7103623af | |||
| 903e999cdc | |||
| 69eff97b34 | |||
| 8e20daa927 | |||
| 9e39dbf81e | |||
| 7a4feebe6f | |||
| 1d1561c6c3 | |||
| 9a5b3aeedc | |||
| ed931e7ed4 | |||
| 5ff9cecc5a | |||
| f8bca463bb | |||
| d5c6f0b623 | |||
| 7e57f443ed | |||
| b4cf2e7065 | |||
| 2ceabb69e6 | |||
| aa9d3360ba | |||
| 4580a64192 | |||
| 4cb0526980 | |||
| 03e2a8d558 | |||
| ab09cbfe3c | |||
| 0467e0c929 | |||
| 6e1c9afdd9 | |||
| 4b3b961b69 | |||
| e63adffdf5 | |||
| d00659b0cb | |||
| 66e73782e5 | |||
| 68f332628d | |||
| 01c0d19243 | |||
| eb0443ed51 | |||
| 4b974b038c | |||
| 2d9ee81c95 | |||
| 5c9d4d2844 | |||
| 49a9ad79dd | |||
| 7e60466c7b | |||
| 233ad2528f | |||
| 2f1afa54c2 | |||
| 6bf910ea56 | |||
| 8fcfedf708 | |||
| 26b46ee2a0 | |||
| e33a2a6f96 | |||
| 06dc1ef9a4 | |||
| 4c57c358b7 | |||
| 65532b3dbf | |||
| fb25dec58e | |||
| e320499f84 | |||
| 4715b12f16 | |||
| c6a30a30de | |||
| 618d06678c | |||
| 903d4cefba | |||
| 79292dc6c8 | |||
| 43414be2ee | |||
| 3c34ae6071 | |||
| 642957e3a3 | |||
| e5d646c007 | |||
| e0f811dab1 | |||
| 48b8311150 | |||
| 8111460e36 | |||
| 0490d0c928 | |||
| b323e315d1 | |||
| 77f928db69 | |||
| b67f3dd6f7 | |||
| 88ff4493b0 | |||
| 6e8fd6e907 | |||
| 9c3095e42c | |||
| c737b8c544 | |||
| 87cecac4ea | |||
| 76ee53e9f8 | |||
| f153c7c3ea | |||
| 36792bba29 | |||
| 0b05964faa | |||
| ff00a5a026 | |||
| fc0310f468 | |||
| 63bf30b890 | |||
| 3004473bbb | |||
| 4356fe5cbe | |||
| d6271b6542 | |||
| 26a65b2336 | |||
| 20adfd49a7 | |||
| 355a98b51f | |||
| 1f73a34a54 | |||
| 7925af9fd6 | |||
| fb03a3baf9 | |||
| f097cd20c1 | |||
| 0489ba9d16 | |||
| 46b3f8fbaf | |||
| cacd0cf103 | |||
| c933668c16 | |||
| 24418ab0a4 | |||
| 4963d0a1d7 | |||
| ea8bfeb8a7 | |||
| a582493a6e | |||
| 930f76887b | |||
| a4201a40d2 | |||
| 4990bb98e5 | |||
| 00d4674aa5 | |||
| 06b4016338 | |||
| c1b2e4fabb | |||
| f438637a98 | |||
| ce208f347e | |||
| 06502584cf | |||
| 0f22dc590a | |||
| 11716f06f0 | |||
| 1ba06e828d | |||
| bc357a19a1 | |||
| 9004f8578c | |||
| 7f038be1cb | |||
| 13fc1122f0 | |||
| cb99cbec58 | |||
| 5d16cf06cf | |||
| b0489117c8 | |||
| fa2eef564c | |||
| d20300b152 | |||
| 398303235a | |||
| 25d048fe49 | |||
| 8c15a0ca95 | |||
| 8e8ff8ba65 | |||
| 1b0eb9d45a | |||
| 403c7272cd | |||
| 0412646151 | |||
| 0725003107 | |||
| 7a1553dc55 | |||
| 8375a2c30f | |||
| 5bbbdb3c19 | |||
| 1fd80c40d0 | |||
| ae5ab2d138 | |||
| eb087fd291 | |||
| 3f6491b8a3 | |||
| 9250479846 | |||
| 9c60421bd6 | |||
| ebea4f10a0 | |||
| d828732307 | |||
| 7c3629337c | |||
| a29034caa5 | |||
| c1fd633ed7 | |||
| bd2cc45524 | |||
| 0665f2231a | |||
| 836abdc81e | |||
| 876eeedb14 | |||
| fd502264a9 | |||
| b155eaa91c | |||
| a0d7ae28bf | |||
| 2816647809 | |||
| 67ce828eeb | |||
| 427c42f4b8 | |||
| b50cb70a0e | |||
| c832a5cdc4 | |||
| 1bd625f17f | |||
| a0fa0becc2 | |||
| d489694ea9 | |||
| 982b5dc886 | |||
| 1ddaecfb94 | |||
| 129c34806c | |||
| 6c7f3b3bbd | |||
| 38cb6bd133 | |||
| 98ca0cdf33 | |||
| 6e32e3dcf4 | |||
| 6a1a871dda | |||
| 6bc7048166 | |||
| 87fbd5201b | |||
| dcf5798229 | |||
| 382ad10cf7 | |||
| ddb2dd7eb6 | |||
| 9b1b43c8b4 | |||
| ee7d84205b | |||
| 93e8e18ca6 | |||
| d586f31247 | |||
| dd9fc8e40e | |||
| d847cba870 | |||
| d983e10d08 | |||
| a6fc65ff4e | |||
| 85f38cd739 | |||
| acde6ff2b2 | |||
| c733129de9 | |||
| e138212593 | |||
| 64ef342121 | |||
| 66c9bb86f5 | |||
| 923e2e1e50 | |||
| 0e552eda55 | |||
| ba32d16c8a | |||
| 4320144024 | |||
| 2564564601 | |||
| f228ad811b | |||
| 5fe442f191 | |||
| 50c4aba9ab | |||
| b3627738c2 | |||
| eec6743fe4 | |||
| 42bf2f5e98 | |||
| 35b9a8ea91 | |||
| 26c0502307 | |||
| 5fa487e2dc | |||
| d9c62780c2 | |||
| 8c54e15a11 | |||
| cc2cc16004 | |||
| 726f12c537 | |||
| f1c235f5c5 | |||
| 9072ba5981 | |||
| 7beb90d4fc | |||
| 61e22743af | |||
| 036baa2264 | |||
| ccd8c2551f | |||
| 74f9787884 | |||
| 83af66a8f6 | |||
| daf887e54f | |||
| 552b11e28d | |||
| 80a88a2248 | |||
| 4c1d6d1463 | |||
| 140a11c04a | |||
| 7be2ef8b85 | |||
| d2d21c3df7 | |||
| f81a91bde9 | |||
| 7efd0de67c | |||
| 6a9db17460 | |||
| b1053826e3 | |||
| 18953c1c90 | |||
| aeb85a1b3c | |||
| 0a6d57ea1a | |||
| aa4dee3c60 | |||
| 9fbe33b356 | |||
| 2a9871e2e9 | |||
| 951b6e9004 | |||
| 2173d3ab65 | |||
| 9c834f410c | |||
| eef44f5cd5 | |||
| aa77ea2835 | |||
| 119bb0195b | |||
| 017dca57ed | |||
| 88351503b0 | |||
| 37b2d49aea | |||
| 0afb1f4306 | |||
| 6ac0658478 | |||
| 50c8e35a90 | |||
| d6c3389d7c | |||
| 6b83213cf4 | |||
| 24927f9a29 | |||
| ecbb9ad20c | |||
| 81e9189853 | |||
| 8efb7903b2 | |||
| c1995beff1 | |||
| 192152b215 | |||
| 972e8c1373 | |||
| c501fc63f8 | |||
| bf08ad800f | |||
| ebc223a895 | |||
| 01b1f23d6b | |||
| 6b08b64d62 | |||
| 89bb20388f | |||
| 9857789204 | |||
| 6d1efe0200 | |||
| a85aa11ecd | |||
| 27ea769ad3 | |||
| 523d0d0945 | |||
| 53f7fef4cf | |||
| b590efa45f | |||
| 59055d7fbd | |||
| 22bcacf143 | |||
| 877109b3b7 | |||
| 10056b8571 | |||
| ac983ff65d | |||
| 2ed76f1e4c | |||
| cb6b18acfe | |||
| 3cd8c5adab | |||
| dd7b7b5f20 | |||
| 1f6880fcad | |||
| c8d9bef686 | |||
| 8a787d2c35 | |||
| 159608cef3 | |||
| 52c5934eb6 | |||
| e4b9e974d2 | |||
| d541b4f137 | |||
| eece643ea5 |
@@ -0,0 +1,10 @@
|
||||
.go/
|
||||
.git/
|
||||
obj-x86_64-linux-gnu/
|
||||
obj-aarch64-linux-gnu/
|
||||
obj-arm-linux-gnueabihf/
|
||||
obj-i686-linux-gnu/
|
||||
unit.out
|
||||
aptly.test
|
||||
build/
|
||||
dpkgs/
|
||||
@@ -0,0 +1,7 @@
|
||||
[flake8]
|
||||
max-line-length = 240
|
||||
ignore = E126,E241,E741,W504
|
||||
include =
|
||||
system
|
||||
exclude =
|
||||
system/env
|
||||
@@ -0,0 +1 @@
|
||||
github: aptly-dev
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Detailed Description
|
||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
||||
|
||||
## Context
|
||||
<!--- Why is this change important to you? How would you use it? -->
|
||||
<!--- How can it benefit other users? -->
|
||||
|
||||
## Possible Implementation
|
||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
@@ -0,0 +1,22 @@
|
||||
Fixes #
|
||||
|
||||
## Requirements
|
||||
|
||||
All new code should be covered with tests, documentation should be updated. CI should pass.
|
||||
|
||||
## Description of the Change
|
||||
|
||||
<!--
|
||||
|
||||
Why this change is important?
|
||||
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] unit-test added (if change is algorithm)
|
||||
- [ ] functional test added/updated (if change is functional)
|
||||
- [ ] man page updated (if applicable)
|
||||
- [ ] bash completion updated (if applicable)
|
||||
- [ ] documentation updated
|
||||
- [ ] author name in `AUTHORS`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Read go version from go.mod"
|
||||
run: |
|
||||
echo "GOVER=$(sed -n 's/^go \(.*\)/\1/p' go.mod)" >> $GITHUB_OUTPUT
|
||||
id: goversion
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ steps.goversion.outputs.GOVER }}
|
||||
|
||||
- name: Create VERSION file
|
||||
run: |
|
||||
make -s version | tr -d '\n' > VERSION
|
||||
shell: sh
|
||||
|
||||
- name: Install and initialize swagger
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init -q --markdownFiles docs
|
||||
shell: sh
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
# Require: The version of golangci-lint to use.
|
||||
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
|
||||
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
|
||||
version: v1.64.5
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
#
|
||||
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
|
||||
# The location of the configuration file can be changed by using `--config=`
|
||||
# args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true, then all caching functionality will be completely disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
|
||||
# install-mode: "goinstall"
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
builds=build/
|
||||
packages=${builds}*.deb
|
||||
folder=`mktemp -u tmp.XXXXXXXXXXXXXXX`
|
||||
aptly_user="$APTLY_USER"
|
||||
aptly_password="$APTLY_PASSWORD"
|
||||
aptly_api="https://aptly-ops.aptly.info"
|
||||
version=`make version`
|
||||
|
||||
action=$1
|
||||
dist=$2
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 ci buster|bullseye|bookworm|focal|jammy|noble" >&2
|
||||
echo " $0 release" >&2
|
||||
}
|
||||
|
||||
# repos and publish must be created beforehand:
|
||||
#!/bin/sh
|
||||
#for dist in buster bullseye bookworm focal jammy noble
|
||||
#do
|
||||
# for build in ci release
|
||||
# do
|
||||
# echo
|
||||
# echo "# Creating and publishing $build/$dist"
|
||||
# aptly repo create -distribution=$dist -component=main aptly-$build-$dist
|
||||
# aptly publish repo -multi-dist -architectures="amd64,i386,arm64,armhf" -acquire-by-hash -component=main \
|
||||
# -distribution=$dist -batch -keyring=aptly.pub \
|
||||
# aptly-$build-$dist \
|
||||
# s3:repo.aptly.info:$build
|
||||
# done
|
||||
#done
|
||||
|
||||
if [ -z "$action" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "action" = "ci" ] && [ -z "$dist" ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$aptly_user" ] || [ -z "$aptly_password" ]; then
|
||||
usage
|
||||
echo Error: please set APTLY_USER and APTLY_PASSWORD
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Publishing version '$version' to $action for $dist...\n"
|
||||
|
||||
upload()
|
||||
{
|
||||
echo "\nUploading files:"
|
||||
for file in $packages; do
|
||||
echo " - $file"
|
||||
jsonret=`curl -fsS -X POST -F "file=@$file" -u $aptly_user:$aptly_password ${aptly_api}/api/files/$folder`
|
||||
done
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo "\nCleanup..."
|
||||
jsonret=`curl -fsS -X DELETE -u $aptly_user:$aptly_password ${aptly_api}/api/files/$folder`
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sleeptime=5
|
||||
retries=60
|
||||
wait_task()
|
||||
{
|
||||
_id=$1
|
||||
_success=0
|
||||
sleep $sleeptime
|
||||
for t in `seq $retries`
|
||||
do
|
||||
jsonret=`curl -fsS -u $aptly_user:$aptly_password ${aptly_api}/api/tasks/$_id`
|
||||
_state=`echo $jsonret | jq .State`
|
||||
if [ "$_state" = "2" ]; then
|
||||
_success=1
|
||||
curl -fsS -X DELETE -u $aptly_user:$aptly_password ${aptly_api}/api/tasks/$_id
|
||||
break
|
||||
fi
|
||||
if [ "$_state" = "3" ]; then
|
||||
echo Error: task failed
|
||||
return 1
|
||||
fi
|
||||
sleep $sleeptime
|
||||
done
|
||||
if [ "$_success" -ne 1 ]; then
|
||||
echo Error: task timeout
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
add_packages() {
|
||||
_aptly_repository=$1
|
||||
_folder=$2
|
||||
jsonret=`curl -fsS -X POST -u $aptly_user:$aptly_password ${aptly_api}/api/repos/$_aptly_repository/file/$_folder?_async=true`
|
||||
_task_id=`echo $jsonret | jq .ID`
|
||||
wait_task $_task_id
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "Error: adding packages to $_aptly_repository failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
update_publish() {
|
||||
_publish=$1
|
||||
_dist=$2
|
||||
jsonret=`curl -fsS -X PUT -H 'Content-Type: application/json' --data \
|
||||
'{"AcquireByHash": true, "MultiDist": true,
|
||||
"Signing": {"Batch": true, "Keyring": "aptly.repo/aptly.pub", "secretKeyring": "aptly.repo/aptly.sec", "PassphraseFile": "aptly.repo/passphrase"}}' \
|
||||
-u $aptly_user:$aptly_password ${aptly_api}/api/publish/$_publish/$_dist?_async=true`
|
||||
_task_id=`echo $jsonret | jq .ID`
|
||||
wait_task $_task_id
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "Error: publish failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$action" = "ci" ]; then
|
||||
if echo "$version" | grep -vq "+"; then
|
||||
# skip ci when on release tag
|
||||
exit 0
|
||||
fi
|
||||
|
||||
aptly_repository=aptly-ci-$dist
|
||||
aptly_published=s3:repo.aptly.info:ci
|
||||
|
||||
elif [ "$action" = "release" ]; then
|
||||
aptly_repository=aptly-release-$dist
|
||||
aptly_published=s3:repo.aptly.info:release
|
||||
fi
|
||||
|
||||
upload
|
||||
|
||||
echo "\nAdding packages to $aptly_repository ..."
|
||||
add_packages $aptly_repository $folder
|
||||
|
||||
echo "\nUpdating published repo $aptly_published ..."
|
||||
update_publish $aptly_published $dist
|
||||
|
||||
# if [ "$action" = "OBSOLETErelease" ]; then
|
||||
# aptly_repository=aptly-release
|
||||
# aptly_snapshot=aptly-$version
|
||||
# aptly_published=s3:repo.aptly.info:./squeeze
|
||||
#
|
||||
# echo "\nAdding packages to $aptly_repository..."
|
||||
# curl -fsS -X POST -u $aptly_user:$aptly_password ${aptly_api}/api/repos/$aptly_repository/file/$folder
|
||||
# echo
|
||||
#
|
||||
# echo "\nCreating snapshot $aptly_snapshot from repo $aptly_repository..."
|
||||
# curl -fsS -X POST -u $aptly_user:$aptly_password -H 'Content-Type: application/json' --data \
|
||||
# "{\"Name\":\"$aptly_snapshot\"}" ${aptly_api}/api/repos/$aptly_repository/snapshots
|
||||
# echo
|
||||
#
|
||||
# echo "\nSwitching published repo $aptly_published to use snapshot $aptly_snapshot..."
|
||||
# curl -fsS -X PUT -H 'Content-Type: application/json' --data \
|
||||
# "{\"AcquireByHash\": true, \"Snapshots\": [{\"Component\": \"main\", \"Name\": \"$aptly_snapshot\"}],
|
||||
# \"Signing\": {\"Batch\": true, \"Keyring\": \"aptly.repo/aptly.pub\",
|
||||
# \"secretKeyring\": \"aptly.repo/aptly.sec\", \"PassphraseFile\": \"aptly.repo/passphrase\"}}" \
|
||||
# -u $aptly_user:$aptly_password ${aptly_api}/api/publish/$aptly_published
|
||||
# echo
|
||||
# fi
|
||||
|
||||
+85
-5
@@ -2,11 +2,14 @@
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
unit.out
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
tmp/
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
@@ -22,13 +25,90 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
|
||||
coverage.html
|
||||
coverage*.out
|
||||
coverage.txt
|
||||
|
||||
*.pyc
|
||||
|
||||
_vendor/
|
||||
xc-out/
|
||||
root/
|
||||
|
||||
gen
|
||||
man/aptly.1.html
|
||||
man/aptly.1.ronn
|
||||
man/aptly.1.ronn
|
||||
|
||||
system/env/
|
||||
|
||||
# created by make build for release artifacts
|
||||
aptly.test
|
||||
|
||||
build/
|
||||
|
||||
system/files/aptly2.gpg~
|
||||
system/files/aptly2_passphrase.gpg~
|
||||
|
||||
*.creds
|
||||
|
||||
.go/
|
||||
obj-x86_64-linux-gnu/
|
||||
obj-aarch64-linux-gnu/
|
||||
obj-arm-linux-gnueabihf/
|
||||
obj-i686-linux-gnu/
|
||||
|
||||
# debian
|
||||
debian/.debhelper/
|
||||
debian/aptly.substvars
|
||||
debian/aptly/
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/aptly-api/
|
||||
debian/*.debhelper
|
||||
debian/*.debhelper.log
|
||||
debian/aptly-api.substvars
|
||||
debian/aptly-dbg.substvars
|
||||
debian/aptly-dbg/
|
||||
usr/bin/aptly
|
||||
dpkgs/
|
||||
debian/changelog.dpkg-bak
|
||||
|
||||
docs/docs.go
|
||||
docs/swagger.json
|
||||
docs/swagger.yaml
|
||||
docs/swagger.conf
|
||||
.secrets
|
||||
|
||||
# Coverage reports
|
||||
*.out
|
||||
coverage.html
|
||||
*_coverage.html
|
||||
|
||||
# Binaries
|
||||
aptly-binary
|
||||
aptly-test
|
||||
|
||||
# Downloaded archives
|
||||
*.tar.gz
|
||||
|
||||
# Test artifacts
|
||||
test_results.log
|
||||
|
||||
# Python virtual environments
|
||||
system/venv/
|
||||
venv/
|
||||
|
||||
# act local CI runner
|
||||
.actrc
|
||||
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Temporary directories
|
||||
coverage/
|
||||
scripts/
|
||||
|
||||
# Binary executables
|
||||
aptly/aptly
|
||||
|
||||
# Coverage reports
|
||||
coverage_report.html
|
||||
*.coverage
|
||||
coverage.out
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
# golangci-lint configuration for aptly
|
||||
# Run with: golangci-lint run
|
||||
|
||||
run:
|
||||
# Timeout for analysis
|
||||
timeout: 5m
|
||||
|
||||
# Include test files
|
||||
tests: true
|
||||
|
||||
output:
|
||||
# Format of output
|
||||
formats:
|
||||
- format: colored-line-number
|
||||
|
||||
# Print lines of code with issue
|
||||
print-issued-lines: true
|
||||
|
||||
# Print linter name in the end of issue text
|
||||
print-linter-name: true
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# Default linters
|
||||
- errcheck # Check for unchecked errors
|
||||
- gosimple # Simplify code
|
||||
- govet # Go vet
|
||||
- ineffassign # Detect ineffectual assignments
|
||||
- staticcheck # Static analysis
|
||||
- typecheck # Type checking
|
||||
- unused # Find unused code
|
||||
|
||||
# Additional linters for code quality
|
||||
- bodyclose # Check HTTP response body is closed
|
||||
- dupl # Code duplication
|
||||
- copyloopvar # Check loop variable export (replacement for exportloopref)
|
||||
- gocognit # Cognitive complexity
|
||||
- gocritic # Opinionated linter
|
||||
- gocyclo # Cyclomatic complexity
|
||||
- gofmt # Formatting
|
||||
- goimports # Import formatting
|
||||
- revive # Fast, configurable linter
|
||||
- unconvert # Unnecessary type conversions
|
||||
- unparam # Unused function parameters
|
||||
- gosec # Security issues
|
||||
- prealloc # Preallocate slices
|
||||
- predeclared # Shadowing of predeclared identifiers
|
||||
- makezero # Make slice with non-zero length
|
||||
- nakedret # Naked returns in long functions
|
||||
|
||||
disable:
|
||||
# Disabled because they're too strict or noisy
|
||||
- exhaustive # Too strict for switch statements
|
||||
- wsl # Whitespace linter (too opinionated)
|
||||
- godox # TODO/FIXME comments
|
||||
- gochecknoglobals # We use some globals
|
||||
- gochecknoinits # We use init functions
|
||||
|
||||
linters-settings:
|
||||
# errcheck
|
||||
errcheck:
|
||||
# Report about not checking of errors in type assertions
|
||||
check-type-assertions: true
|
||||
# Report about assignment of errors to blank identifier
|
||||
check-blank: true
|
||||
# Exclude some functions from checking
|
||||
exclude-functions:
|
||||
- io/ioutil.ReadFile
|
||||
- io.Copy(*bytes.Buffer)
|
||||
- io.Copy(os.Stdout)
|
||||
|
||||
# govet
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment # Too many false positives
|
||||
|
||||
# gocyclo
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
# gocognit
|
||||
gocognit:
|
||||
min-complexity: 20
|
||||
|
||||
# dupl
|
||||
dupl:
|
||||
threshold: 200
|
||||
|
||||
# gocritic
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- commentedOutCode
|
||||
- whyNoLint
|
||||
|
||||
# gosec
|
||||
gosec:
|
||||
severity: low
|
||||
confidence: low
|
||||
excludes:
|
||||
- G404 # Weak random for non-crypto use is ok
|
||||
|
||||
# revive
|
||||
revive:
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: empty-block
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
|
||||
# goimports
|
||||
goimports:
|
||||
local-prefixes: github.com/aptly-dev/aptly
|
||||
|
||||
# gofmt
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
# unparam
|
||||
unparam:
|
||||
check-exported: false
|
||||
|
||||
# nakedret
|
||||
nakedret:
|
||||
max-func-lines: 30
|
||||
|
||||
# prealloc
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
|
||||
issues:
|
||||
# Maximum issues count per one linter
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text
|
||||
max-same-issues: 0
|
||||
|
||||
# Skip directories
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
- testdata
|
||||
- system/files
|
||||
|
||||
# Skip files matching these patterns
|
||||
exclude-files:
|
||||
- ".*\\.pb\\.go$"
|
||||
- ".*\\.gen\\.go$"
|
||||
|
||||
# Exclude some linters from running on tests files
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- gosec
|
||||
- gocognit
|
||||
- gocyclo
|
||||
|
||||
# Exclude some linters from running on generated files
|
||||
- path: ".*\\.gen\\.go$"
|
||||
linters:
|
||||
- all
|
||||
|
||||
# Exclude known issues in vendor
|
||||
- path: vendor/
|
||||
linters:
|
||||
- all
|
||||
|
||||
# Allow fmt.Printf in main/cmd
|
||||
- path: (cmd|main)\.go
|
||||
linters:
|
||||
- forbidigo
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns
|
||||
exclude-use-default: true
|
||||
|
||||
# Fix found issues (if it's supported by the linter)
|
||||
fix: false
|
||||
|
||||
severity:
|
||||
# Set the default severity for issues
|
||||
default-severity: warning
|
||||
|
||||
# The list of ids of default excludes to include or disable
|
||||
rules:
|
||||
- linters:
|
||||
- gosec
|
||||
severity: info
|
||||
- linters:
|
||||
- dupl
|
||||
severity: info
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.2.1
|
||||
- 1.3.1
|
||||
- tip
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: "YSwtFrMqh4oUvdSQTXBXMHHLWeQgyNEL23ChIZwU0nuDGIcQZ65kipu0PzefedtUbK4ieC065YCUi4UDDh6gPotB/Wu1pnYg3dyQ7rFvhaVYAAUEpajAdXZhlx+7+J8a4FZMeC/kqiahxoRgLbthF9019ouIqhGB9zHKI6/yZwc="
|
||||
- secure: "V7OjWrfQ8UbktgT036jYQPb/7GJT3Ol9LObDr8FYlzsQ+F1uj2wLac6ePuxcOS4FwWOJinWGM1h+JiFkbxbyFqfRNJ0jj0O2p93QyDojxFVOn1mXqqvV66KFqAWR2Vzkny/gDvj8LTvdB1cgAIm2FNOkQc6E1BFnyWS2sN9ea5E="
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y python-boto
|
||||
install:
|
||||
- make prepare
|
||||
|
||||
|
||||
script: make travis
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
@@ -3,5 +3,69 @@ List of contributors, in chronological order:
|
||||
* Andrey Smirnov (https://github.com/smira)
|
||||
* Sebastien Binet (https://github.com/sbinet)
|
||||
* Ryan Uber (https://github.com/ryanuber)
|
||||
* Simon Aquino (https://github.com/simonaquino)
|
||||
* Vincent Batoufflet (https://github.com/vbatoufflet)
|
||||
* Simon Aquino (https://github.com/queeno)
|
||||
* Vincent Batoufflet (https://github.com/vbatoufflet)
|
||||
* Ivan Kurnosov (https://github.com/zerkms)
|
||||
* Dmitrii Kashin (https://github.com/freehck)
|
||||
* Chris Read (https://github.com/cread)
|
||||
* Rohan Garg (https://github.com/shadeslayer)
|
||||
* Russ Allbery (https://github.com/rra)
|
||||
* Sylvain Baubeau (https://github.com/lebauce)
|
||||
* Andrea Bernardo Ciddio (https://github.com/bcandrea)
|
||||
* Michael Koval (https://github.com/mkoval)
|
||||
* Alexander Guy (https://github.com/alexanderguy)
|
||||
* Sebastien Badia (https://github.com/sbadia)
|
||||
* Szymon Sobik (https://github.com/sobczyk)
|
||||
* Paul Krohn (https://github.com/paul-krohn)
|
||||
* Vincent Bernat (https://github.com/vincentbernat)
|
||||
* x539 (https://github.com/x539)
|
||||
* Phil Frost (https://github.com/bitglue)
|
||||
* Benoit Foucher (https://github.com/bentoi)
|
||||
* Geoffrey Thomas (https://github.com/geofft)
|
||||
* Oliver Sauder (https://github.com/sliverc)
|
||||
* Harald Sitter (https://github.com/apachelogger)
|
||||
* Johannes Layher (https://github.com/jola5)
|
||||
* Charles Hsu (https://github.com/charz)
|
||||
* Clemens Rabe (https://github.com/seeraven)
|
||||
* TJ Merritt (https://github.com/tjmerritt)
|
||||
* Matt Martyn (https://github.com/MMartyn)
|
||||
* Ludovico Cavedon (https://github.com/cavedon)
|
||||
* Petr Jediny (https://github.com/pjediny)
|
||||
* Maximilian Stein (https://github.com/steinymity)
|
||||
* Strajan Sebastian (https://github.com/strajansebastian)
|
||||
* Artem Smirnov (https://github.com/urpylka)
|
||||
* William Manley (https://github.com/wmanley)
|
||||
* Shengjing Zhu (https://github.com/zhsj)
|
||||
* Nabil Bendafi (https://github.com/nabilbendafi)
|
||||
* Raphael Medaer (https://github.com/rmedaer)
|
||||
* Raul Benencia (https://github.com/rul)
|
||||
* Don Kuntz (https://github.com/dkuntz2)
|
||||
* Joshua Colson (https://github.com/freakinhippie)
|
||||
* Andre Roth (https://github.com/neolynx)
|
||||
* Lorenzo Bolla (https://github.com/lbolla)
|
||||
* Benj Fassbind (https://github.com/randombenj)
|
||||
* Markus Muellner (https://github.com/mmianl)
|
||||
* Chuan Liu (https://github.com/chuan)
|
||||
* Samuel Mutel (https://github.com/smutel)
|
||||
* Russell Greene (https://github.com/russelltg)
|
||||
* Wade Simmons (https://github.com/wadey)
|
||||
* Steven Stone (https://github.com/smstone)
|
||||
* Josh Bayfield (https://github.com/jbayfield)
|
||||
* Boxjan (https://github.com/boxjan)
|
||||
* Mauro Regli (https://github.com/reglim)
|
||||
* Alexander Zubarev (https://github.com/strike)
|
||||
* Nicolas Dostert (https://github.com/acdn-ndostert)
|
||||
* Ryan Gonzalez (https://github.com/refi64)
|
||||
* Paul Cacheux (https://github.com/paulcacheux)
|
||||
* Nic Waller (https://github.com/sf-nwaller)
|
||||
* iofq (https://github.com/iofq)
|
||||
* Noa Resare (https://github.com/nresare)
|
||||
* Ramon N.Rodriguez (https://github.com/runitonmetal)
|
||||
* Golf Hu (https://github.com/hudeng-go)
|
||||
* Cookie Fei (https://github.com/wuhuang26)
|
||||
* Andrey Loukhnov (https://github.com/aol-nnov)
|
||||
* Christoph Fiehe (https://github.com/cfiehe)
|
||||
* Blake Kostner (https://github.com/btkostner)
|
||||
* Leigh London (https://github.com/leighlondon)
|
||||
* Gordian Schoenherr (https://github.com/schoenherrg)
|
||||
* Silke Hofstra (https://github.com/silkeh)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team on [Aptly Discussions](https://github.com/aptly-dev/aptly/discussions). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
# Contributing to aptly
|
||||
|
||||
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
The following is a set of guidelines for contributing to [aptly](https://github.com/aptly-dev/aplty) and related repositories, which are hosted in the [aptly-dev Organization](https://github.com/aptly-dev) on GitHub.
|
||||
These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## What should I know before I get started?
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
|
||||
By participating, you are expected to uphold this code.
|
||||
Please report unacceptable behavior on [https://github.com/aptly-dev/aptly/discussions](https://github.com/aptly-dev/aptly/discussions)
|
||||
|
||||
### List of Repositories
|
||||
|
||||
* [aptly-dev/aptly](https://github.com/aptly-dev/aptly) - aptly source code, functional tests, man page
|
||||
* [apty-dev/aptly-dev.github.io](https://github.com/aptly-dev/aptly-dev.github.io) - aptly website (https://www.aptly.info/)
|
||||
* [aptly-dev/aptly-fixture-db](https://github.com/aptly-dev/aptly-fixture-db) & [aptly-dev/aptly-fixture-pool](https://github.com/aptly-dev/aptly-fixture-pool) provide
|
||||
fixtures for aptly functional tests
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
1. Please search for similar bug report in [issue tracker](https://github.com/aptly-dev/aptly/issues)
|
||||
2. Please verify that bug is not fixed in latest aptly nightly ([download information](https://www.aptly.info/download/))
|
||||
3. Steps to reproduce increases chances for bug to be fixed quickly. If possible, submit PR with new functional test which fails.
|
||||
4. If bug is reproducible with specific package, please provide link to package file.
|
||||
5. Open issue at [GitHub](https://github.com/aptly-dev/aptly/issues)
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
1. Please search [issue tracker](https://github.com/aptly-dev/aptly/issues) for similar feature requests.
|
||||
2. Describe why enhancement is important to you.
|
||||
3. Include any additional details or implementation details.
|
||||
|
||||
### Improving Documentation
|
||||
|
||||
There are two kinds of documentation:
|
||||
|
||||
* [aptly website](https://www.aptly.info)
|
||||
* aptly `man` page
|
||||
|
||||
Core content is mostly the same, but website contains more information, tutorials, examples.
|
||||
|
||||
If you want to update `man` page, please open PR to [main aptly repo](https://github.com/aptly-dev/aptly),
|
||||
details in [man page](#man-page) section.
|
||||
|
||||
If you want to update website, please follow steps below:
|
||||
|
||||
1. Install [hugo](http://gohugo.io/)
|
||||
2. Fork [website source](https://github.com/aptly-dev/aptly-dev.github.io) and clone it
|
||||
3. Launch hugo in development mode: `hugo -w server`
|
||||
4. Navigate to `http://localhost:1313/`: you should see aptly website
|
||||
5. Update documentation, most of the time editing Markdown is all you need.
|
||||
6. Page in browser should reload automatically as you make changes to source files.
|
||||
|
||||
We're always looking for new contributions to [FAQ](https://www.aptly.info/doc/faq/), [tutorials](https://www.aptly.info/tutorial/),
|
||||
general fixes, clarifications, misspellings, grammar mistakes!
|
||||
|
||||
### Code Contribution
|
||||
|
||||
Please follow [next section](#development-setup) on development process. When change is ready, please submit PR
|
||||
following [PR template](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
Make sure that purpose of your change is clear, all the tests and checks pass, and all new code is covered with tests
|
||||
if that is possible.
|
||||
|
||||
### Get the Source
|
||||
|
||||
To clone the git repo, run the following commands:
|
||||
```
|
||||
git clone git@github.com:aptly-dev/aptly.git
|
||||
cd aptly
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
Working on aptly code can be done locally on the development machine, or for convenience by using docker. The next sections describe the setup process.
|
||||
|
||||
### Docker Development Setup
|
||||
|
||||
This section describes the docker setup to start contributing to aptly.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
Install the following on your development machine:
|
||||
- docker
|
||||
- make
|
||||
- git
|
||||
|
||||
##### Docker installation on macOS
|
||||
1. Install [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) (or via [Homebrew](https://brew.sh/))
|
||||
2. Allow directory sharing
|
||||
- Open Docker Desktop
|
||||
- Go to `Settings → Resources → File Sharing → Virtual File Shares`
|
||||
- Add the aptly git repository path to the shared list (eg. /home/Users/john/aptly)
|
||||
|
||||
#### Create docker container
|
||||
|
||||
To build the development docker image, run:
|
||||
```
|
||||
make docker-image
|
||||
```
|
||||
|
||||
#### Build aptly
|
||||
|
||||
To build the aptly in the development docker container, run:
|
||||
```
|
||||
make docker-build
|
||||
```
|
||||
|
||||
#### Running aptly commands
|
||||
|
||||
To run aptly commands in the development docker container, run:
|
||||
```
|
||||
make docker-shell
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
$ make docker-shell
|
||||
aptly@b43e8473ef81:/work/src$ aptly version
|
||||
aptly version: 1.5.0+189+g0fc90dff
|
||||
```
|
||||
|
||||
#### Running unit tests
|
||||
|
||||
In order to run aptly unit tests, enter the following:
|
||||
```
|
||||
make docker-unit-tests
|
||||
```
|
||||
|
||||
#### Running system tests
|
||||
|
||||
In order to run aptly system tests, enter the following:
|
||||
```
|
||||
make docker-system-tests
|
||||
```
|
||||
|
||||
#### Running golangci-lint
|
||||
|
||||
In order to run aptly unit tests, run:
|
||||
```
|
||||
make docker-lint
|
||||
```
|
||||
|
||||
#### More info
|
||||
|
||||
Run `make help` for more information.
|
||||
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
This section describes local setup to start contributing to aptly.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
Building aptly requires go version 1.24.
|
||||
|
||||
On Debian bookworm with backports enabled, go can be installed with:
|
||||
|
||||
apt install -t bookworm-backports golang-go
|
||||
|
||||
#### Building
|
||||
|
||||
To build aptly, run:
|
||||
|
||||
make build
|
||||
|
||||
Run aptly:
|
||||
|
||||
build/aptly
|
||||
|
||||
To install aptly into `$GOPATH/bin`, run:
|
||||
|
||||
make install
|
||||
|
||||
#### Platform-Specific Setup
|
||||
|
||||
##### macOS
|
||||
|
||||
This guide explains how to run aptly tests on macOS, including Apple Silicon (M1/M2) machines.
|
||||
|
||||
###### Prerequisites
|
||||
|
||||
1. **Install Go** (1.24 or later):
|
||||
```bash
|
||||
brew install go
|
||||
```
|
||||
|
||||
2. **Install Docker** (for etcd and other services):
|
||||
```bash
|
||||
brew install --cask docker
|
||||
```
|
||||
|
||||
3. **Install test dependencies**:
|
||||
```bash
|
||||
# Add Go binaries to PATH
|
||||
export PATH=$PATH:~/go/bin
|
||||
|
||||
# Install swag for API documentation
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# Install other tools
|
||||
brew install etcd # Optional: for local etcd instead of Docker
|
||||
```
|
||||
|
||||
###### Running Tests on macOS
|
||||
|
||||
**Option 1: Using Docker Compose (Recommended)**
|
||||
|
||||
```bash
|
||||
# Start test services
|
||||
docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
|
||||
# Run tests
|
||||
PATH=$PATH:~/go/bin make test
|
||||
```
|
||||
|
||||
**Option 2: Using Local etcd**
|
||||
|
||||
```bash
|
||||
# Install and start etcd locally
|
||||
brew services start etcd
|
||||
|
||||
# Run tests with local etcd
|
||||
ETCD_ENDPOINTS=localhost:2379 go test ./...
|
||||
```
|
||||
|
||||
**Option 3: Run Specific Test Suites**
|
||||
|
||||
```bash
|
||||
# Fix VERSION file if needed
|
||||
echo "1.5.0" > VERSION
|
||||
|
||||
# Run unit tests only
|
||||
PATH=$PATH:~/go/bin make test-unit GOTEST="go test -short -timeout=5m"
|
||||
|
||||
# Run specific packages
|
||||
go test ./deb ./s3 ./utils ./context -short -v
|
||||
|
||||
# Run with race detection
|
||||
go test -race ./deb ./s3 ./utils -short
|
||||
```
|
||||
|
||||
###### macOS-Specific Considerations
|
||||
|
||||
1. **CPU Architecture**: The install scripts now support both Intel (x86_64) and Apple Silicon (arm64).
|
||||
|
||||
2. **File System**: macOS is case-insensitive by default, which may affect some tests.
|
||||
|
||||
3. **Network**: Some tests may require adjusting firewall settings.
|
||||
|
||||
4. **Timeouts**: Some tests may need longer timeouts on macOS:
|
||||
```bash
|
||||
go test -timeout=10m ./...
|
||||
```
|
||||
|
||||
###### Troubleshooting on macOS
|
||||
|
||||
**etcd Installation Fails**
|
||||
|
||||
If the automatic etcd installation fails, use Docker or Homebrew:
|
||||
```bash
|
||||
# Using Docker
|
||||
docker run -d -p 2379:2379 --name etcd quay.io/coreos/etcd:latest
|
||||
|
||||
# Using Homebrew
|
||||
brew install etcd
|
||||
etcd --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://localhost:2379
|
||||
```
|
||||
|
||||
**Test Timeouts**
|
||||
|
||||
Increase timeouts for slower tests:
|
||||
```bash
|
||||
go test -timeout=30m ./...
|
||||
```
|
||||
|
||||
**Race Detector Issues**
|
||||
|
||||
The race detector may be slower on macOS. Disable for faster runs:
|
||||
```bash
|
||||
go test ./... -short
|
||||
```
|
||||
|
||||
###### CI Integration for macOS
|
||||
|
||||
For GitHub Actions on macOS:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install etcd
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
export PATH=$PATH:~/go/bin
|
||||
make test
|
||||
```
|
||||
|
||||
###### Test Coverage on macOS
|
||||
|
||||
Generate coverage reports:
|
||||
```bash
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
open coverage.html
|
||||
```
|
||||
|
||||
#### Unit-tests
|
||||
|
||||
aptly has two kinds of tests: unit-tests and functional (system) tests. Functional tests are preferred way to test any
|
||||
feature, but some features are much easier to test with unit-tests (e.g. algorithms, failure scenarios, ...)
|
||||
|
||||
aptly is using standard Go unit-test infrastructure plus [gocheck](http://labix.org/gocheck). Run the unit-tests with:
|
||||
|
||||
make test
|
||||
|
||||
#### Functional Tests
|
||||
|
||||
Functional tests are implemented in Python, and they use custom test runner which is similar to Python unit-test
|
||||
runner. Most of the tests start with clean aptly state, run some aptly commands to prepare environment, and finally
|
||||
run some aptly commands capturing output, exit code, checking any additional files being created and so on. API tests
|
||||
are a bit different, as they re-use same aptly process serving API requests.
|
||||
|
||||
The easiest way to run functional tests is to use `make`:
|
||||
|
||||
make system-test
|
||||
|
||||
This would check all the dependencies and run all the tests. Some tests (S3, Swift) require access credentials to
|
||||
be set up in the environment. For example, it needs AWS credentials to run S3 tests (they would be used to publish to S3).
|
||||
If credentials are missing, tests would be skipped.
|
||||
|
||||
You can also run subset of tests manually:
|
||||
|
||||
system/run.py t04_mirror
|
||||
|
||||
This would run all the mirroring tests under `system/t04_mirror` folder.
|
||||
|
||||
Or you can run tests by test name mask:
|
||||
|
||||
system/run.py UpdateMirror*
|
||||
|
||||
Or, you can run specific test by name:
|
||||
|
||||
system/run.py UpdateMirror7Test
|
||||
|
||||
Test runner can update expected output instead of failing on mismatch (this is especially useful while
|
||||
working on new tests):
|
||||
|
||||
system/run.py --capture <test>
|
||||
|
||||
Output for some tests might contain environment-specific things, e.g. your home directory. In that case
|
||||
you can use `${HOME}` and similar variable expansion in expected output files.
|
||||
|
||||
Some tests depend on fixtures, for example pre-populated GPG trusted keys. There are also test fixtures
|
||||
captured after mirror update which contain pre-build aptly database and pool contents. They're useful if you
|
||||
don't want to waste time in the test on populating aptly database while you need some packages to work with.
|
||||
There are some packages available under `system/files/` directory which are used to build contents of local repos.
|
||||
|
||||
*WARNING*: tests are running under current `$HOME` directory with aptly default settings, so they clear completely
|
||||
`~/.aptly.conf` and `~/.aptly` subdirectory between the runs. So it's not wise to have non-dev aptly being used with
|
||||
this default location. You can run aptly under different user or by using non-default config location with non-default
|
||||
aptly root directory.
|
||||
|
||||
### Continuous Integration (CI)
|
||||
|
||||
aptly uses GitHub Actions for continuous integration. The CI pipeline includes:
|
||||
|
||||
- **Quick checks**: Code formatting, go vet, mod tidy, and flake8 linting
|
||||
- **Security scanning**: govulncheck and Trivy vulnerability scanning
|
||||
- **Linting**: golangci-lint with extensive checks
|
||||
- **Unit tests**: With race detection on Go 1.23 and 1.24
|
||||
- **Integration tests**: Full system tests with cloud storage backends
|
||||
- **Benchmarks**: Performance testing
|
||||
- **Extended tests**: Combined unit tests and benchmarks with coverage merging
|
||||
- **Cross-platform builds**: Binaries for Linux, macOS, Windows, FreeBSD (multiple architectures)
|
||||
- **Debian packages**: Built for Debian (buster, bullseye, bookworm, trixie) and Ubuntu (focal, jammy, noble)
|
||||
- **Docker images**: Multi-architecture container images (linux/amd64, linux/arm64)
|
||||
|
||||
All pull requests must pass CI checks before merging. Build artifacts are available for download from GitHub Actions runs with the following retention:
|
||||
- CI builds: 7 days
|
||||
- Tagged releases: 90 days
|
||||
|
||||
#### Testing CI Locally with act
|
||||
|
||||
You can test GitHub Actions workflows locally using [act](https://github.com/nektos/act):
|
||||
|
||||
```bash
|
||||
# Install act
|
||||
brew install act # macOS
|
||||
# or
|
||||
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash # Linux
|
||||
|
||||
# Run default push event
|
||||
act
|
||||
|
||||
# Run pull request event
|
||||
act pull_request
|
||||
|
||||
# Run specific job
|
||||
act -j test-unit
|
||||
|
||||
# Run with specific matrix values
|
||||
act -j test-unit --matrix go:1.24
|
||||
|
||||
# List all available jobs
|
||||
act -l
|
||||
```
|
||||
|
||||
For Apple Silicon Macs, use: `act --container-architecture linux/amd64`
|
||||
|
||||
Common use cases:
|
||||
- Test a job before pushing: `act -j quick-checks`
|
||||
- Test PR workflows: Create a PR event file and run `act pull_request -e pr-event.json`
|
||||
- Debug failures: `act -j failing-job -v` for verbose output
|
||||
- Use secrets: Create `.secrets` file with `KEY=value` format and run `act --secret-file .secrets`
|
||||
|
||||
### man Page
|
||||
|
||||
aptly is using combination of [Go templates](http://godoc.org/text/template) and automatically generated text to build `aptly.1` man page. If either source
|
||||
template [man/aptly.1.ronn.tmpl](man/aptly.1.ronn.tmpl) is changed or any command help is changed, run `make man` to regenerate
|
||||
final rendered man page [man/aptly.1](man/aptly.1). In the end of the build, new man page is displayed for visual
|
||||
verification.
|
||||
|
||||
Man page is built with small helper [\_man/gen.go](man/gen.go) which pulls in template, command-line help from [cmd/](cmd/) folder
|
||||
and runs that through [forked copy](https://github.com/smira/ronn) of [ronn](https://github.com/rtomayko/ronn).
|
||||
|
||||
### Bash and Zsh Completion
|
||||
|
||||
Bash and Zsh completion for aptly reside in the same repo under in [completion.d/aptly](completion.d/aptly) and
|
||||
[completion.d/\_aptly](completion.d/_aptly), respectively. It's all hand-crafted.
|
||||
When new option or command is introduced, bash completion should be updated to reflect that change.
|
||||
|
||||
When aptly package is being built, it automatically pulls bash completion and man page into the package.
|
||||
@@ -1,27 +0,0 @@
|
||||
gom 'code.google.com/p/go-uuid/uuid', :commit => '5fac954758f5'
|
||||
gom 'code.google.com/p/go.crypto/ssh/terminal', :commit => '7aa593ce8cea'
|
||||
gom 'code.google.com/p/gographviz', :commit => '454bc64fdfa2'
|
||||
gom 'code.google.com/p/mxk/go1/flowcontrol', :commit => '5ff2502e2556'
|
||||
gom 'code.google.com/p/snappy-go/snappy', :commit => '12e4b4183793'
|
||||
gom 'github.com/cheggaaa/pb', :commit => '74be7a1388046f374ac36e93d46f5d56e856f827'
|
||||
gom 'github.com/vaughan0/go-ini', :commit => 'a98ad7ee00ec53921f08832bc06ecf7fd600e6a1'
|
||||
gom 'github.com/mattn/go-shellwords', :commit => 'c7ca6f94add751566a61cf2199e1de78d4c3eee4'
|
||||
gom 'github.com/mitchellh/goamz/s3', :commit => 'e7664b32019f31fd1bdf33f9e85f28722f700405'
|
||||
gom 'github.com/mkrautz/goar', :commit => '36eb5f3452b1283a211fa35bc00c646fd0db5c4b'
|
||||
gom 'github.com/smira/commander', :commit => 'f408b00e68d5d6e21b9f18bd310978dafc604e47'
|
||||
gom 'github.com/smira/flag', :commit => '357ed3e599ffcbd4aeaa828e1d10da2df3ea5107'
|
||||
gom 'github.com/smira/go-ftp-protocol/protocol', :commit => '066b75c2b70dca7ae10b1b88b47534a3c31ccfaa'
|
||||
gom 'github.com/syndtr/goleveldb/leveldb', :commit => 'e2fa4e6ac1cc41a73bc9fd467878ecbf65df5cc3'
|
||||
gom 'github.com/ugorji/go/codec', :commit => '71c2886f5a673a35f909803f38ece5810165097b'
|
||||
gom 'github.com/wsxiaoys/terminal/color', :commit => '5668e431776a7957528361f90ce828266c69ed08'
|
||||
|
||||
group :test do
|
||||
gom 'launchpad.net/gocheck'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gom 'github.com/golang/lint/golint'
|
||||
gom 'github.com/mattn/goveralls'
|
||||
gom 'github.com/axw/gocov/gocov'
|
||||
gom 'code.google.com/p/go.tools/cmd/cover'
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2013-2014 Andrey Smirnov. All rights reserved.
|
||||
Copyright 2013-2015 aptly authors. All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
|
||||
@@ -1,84 +1,314 @@
|
||||
GOVERSION=$(shell go version | awk '{print $$3;}')
|
||||
PACKAGES=database deb files http query s3 utils
|
||||
ALL_PACKAGES=aptly cmd console database deb files http query s3 utils
|
||||
BINPATH=$(abspath ./_vendor/bin)
|
||||
GOM_ENVIRONMENT=-test
|
||||
PYTHON?=python
|
||||
# Modern Makefile for aptly with improved tooling and practices
|
||||
|
||||
ifeq ($(GOVERSION), devel)
|
||||
TRAVIS_TARGET=coveralls
|
||||
GOM_ENVIRONMENT+=-development
|
||||
SHELL := /bin/bash
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help
|
||||
|
||||
# Version and build info
|
||||
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Go parameters
|
||||
GOCMD := go
|
||||
GOBUILD := $(GOCMD) build
|
||||
GOTEST := $(GOCMD) test
|
||||
GOGET := $(GOCMD) get
|
||||
GOMOD := $(GOCMD) mod
|
||||
GOFMT := gofmt
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
BINPATH := $(GOPATH)/bin
|
||||
GOOS := $(shell go env GOHOSTOS)
|
||||
GOARCH := $(shell go env GOHOSTARCH)
|
||||
|
||||
# OS detection
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
OS_TYPE := macos
|
||||
else
|
||||
TRAVIS_TARGET=test
|
||||
OS_TYPE := linux
|
||||
endif
|
||||
|
||||
ifeq ($(TRAVIS), true)
|
||||
GOM=$(HOME)/gopath/bin/gom
|
||||
else
|
||||
GOM=gom
|
||||
endif
|
||||
# Tool versions
|
||||
GOLANGCI_VERSION := v1.64.5
|
||||
AIR_VERSION := v1.52.3
|
||||
SWAG_VERSION := v1.16.4
|
||||
GOVULNCHECK_VERSION := latest
|
||||
|
||||
all: test check system-test
|
||||
# Build parameters
|
||||
BINARY_NAME := aptly
|
||||
BUILD_DIR := build
|
||||
COVERAGE_DIR := coverage
|
||||
COVERAGE_FILE := $(COVERAGE_DIR)/coverage.out
|
||||
|
||||
prepare:
|
||||
go get -u github.com/mattn/gom
|
||||
$(GOM) $(GOM_ENVIRONMENT) install
|
||||
# Docker parameters
|
||||
DOCKER_IMAGE := aptly/aptly
|
||||
DOCKER_TAG := $(VERSION)
|
||||
|
||||
coverage.out:
|
||||
rm -f coverage.*.out
|
||||
for i in $(PACKAGES); do $(GOM) test -coverprofile=coverage.$$i.out -covermode=count ./$$i; done
|
||||
echo "mode: count" > coverage.out
|
||||
grep -v -h "mode: count" coverage.*.out >> coverage.out
|
||||
rm -f coverage.*.out
|
||||
# Colors for output
|
||||
COLOR_RESET := \033[0m
|
||||
COLOR_BOLD := \033[1m
|
||||
COLOR_GREEN := \033[32m
|
||||
COLOR_YELLOW := \033[33m
|
||||
COLOR_RED := \033[31m
|
||||
COLOR_BLUE := \033[34m
|
||||
|
||||
coverage: coverage.out
|
||||
$(GOM) exec go tool cover -html=coverage.out
|
||||
rm -f coverage.out
|
||||
##@ General
|
||||
|
||||
check:
|
||||
$(GOM) exec go tool vet -all=true -shadow=true $(ALL_PACKAGES:%=./%)
|
||||
$(GOM) exec golint $(ALL_PACKAGES:%=./%)
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
install:
|
||||
$(GOM) build -o $(BINPATH)/aptly
|
||||
version: ## Show version
|
||||
@ci="" ; \
|
||||
if [ "`make -s releasetype`" = "ci" ]; then \
|
||||
ci=`TZ=UTC git show -s --format='+%cd.%h' --date=format-local:'%Y%m%d%H%M%S'`; \
|
||||
fi ; \
|
||||
if which dpkg-parsechangelog > /dev/null 2>&1; then \
|
||||
echo `dpkg-parsechangelog -S Version`$$ci; \
|
||||
else \
|
||||
echo `grep ^aptly -m1 debian/changelog | sed 's/.*(\([^)]\+\)).*/\1/'`$$ci ; \
|
||||
fi
|
||||
|
||||
system-test: install
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
PATH=$(BINPATH)/:$(PATH) $(PYTHON) system/run.py --long
|
||||
releasetype: # Print release type: ci (on any branch/commit), release (on a tag)
|
||||
@reltype=ci ; \
|
||||
gitbranch=`git rev-parse --abbrev-ref HEAD` ; \
|
||||
if [ "$$gitbranch" = "HEAD" ] && [ "$$FORCE_CI" != "true" ]; then \
|
||||
gittag=`git describe --tags --exact-match 2>/dev/null` ;\
|
||||
if echo "$$gittag" | grep -q '^v[0-9]'; then \
|
||||
reltype=release ; \
|
||||
fi ; \
|
||||
fi ; \
|
||||
echo $$reltype
|
||||
|
||||
travis: $(TRAVIS_TARGET) system-test
|
||||
##@ Development
|
||||
|
||||
test:
|
||||
$(GOM) test -v ./... -gocheck.v=true
|
||||
prepare: ## Prepare development environment
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Preparing development environment...$(COLOR_RESET)"
|
||||
$(GOMOD) download
|
||||
$(GOMOD) verify
|
||||
$(GOMOD) tidy -v
|
||||
@go generate ./...
|
||||
|
||||
coveralls: coverage.out
|
||||
$(GOM) exec $(BINPATH)/goveralls -service travis-ci.org -coverprofile=coverage.out -repotoken=$(COVERALLS_TOKEN)
|
||||
dev-tools: ## Install development tools
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Installing development tools...$(COLOR_RESET)"
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_VERSION)
|
||||
@go install github.com/air-verse/air@$(AIR_VERSION)
|
||||
@go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
@go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Development tools installed$(COLOR_RESET)"
|
||||
|
||||
mem.png: mem.dat mem.gp
|
||||
gnuplot mem.gp
|
||||
open mem.png
|
||||
##@ Build
|
||||
|
||||
package:
|
||||
rm -rf root/
|
||||
mkdir -p root/usr/bin/ root/usr/share/man/man1/ root/etc/bash_completion.d
|
||||
cp $(BINPATH)/aptly root/usr/bin
|
||||
cp man/aptly.1 root/usr/share/man/man1
|
||||
(cd root/etc/bash_completion.d && wget https://raw.github.com/aptly-dev/aptly-bash-completion/master/aptly)
|
||||
gzip root/usr/share/man/man1/aptly.1
|
||||
fpm -s dir -t deb -n aptly -v $(VERSION) --url=http://www.aptly.info/ --license=MIT --vendor="Andrey Smirnov <me@smira.ru>" \
|
||||
-f -m "Andrey Smirnov <me@smira.ru>" --description="Debian repository management tool" --deb-recommends bzip2 -C root/ .
|
||||
mv aptly_$(VERSION)_*.deb ~
|
||||
build: prepare swagger ## Build aptly binary
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building aptly...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Build complete: $(BUILD_DIR)/$(BINARY_NAME)$(COLOR_RESET)"
|
||||
|
||||
src-package:
|
||||
rm -rf aptly-$(VERSION)
|
||||
mkdir -p aptly-$(VERSION)/src/github.com/smira/aptly/
|
||||
cd aptly-$(VERSION)/src/github.com/smira/ && git clone https://github.com/smira/aptly && cd aptly && git checkout v$(VERSION)
|
||||
cd aptly-$(VERSION)/src/github.com/smira/aptly && gom -production install
|
||||
cd aptly-$(VERSION)/src/github.com/smira/aptly && find . \( -name .git -o -name .bzr -o -name .hg \) -print | xargs rm -rf
|
||||
rm -rf aptly-$(VERSION)/src/github.com/smira/aptly/_vendor/{pkg,bin}
|
||||
tar cyf aptly-$(VERSION)-src.tar.bz2 aptly-$(VERSION)
|
||||
rm -rf aptly-$(VERSION)
|
||||
curl -T aptly-$(VERSION)-src.tar.bz2 -usmira:$(BINTRAY_KEY) https://api.bintray.com/content/smira/aptly/aptly/$(VERSION)/$(VERSION)/aptly-$(VERSION)-src.tar.bz2
|
||||
build-all: prepare swagger ## Build for all platforms
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building for all platforms...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
# Linux
|
||||
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64
|
||||
GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64
|
||||
# macOS
|
||||
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64
|
||||
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64
|
||||
# Windows
|
||||
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Multi-platform build complete$(COLOR_RESET)"
|
||||
|
||||
.PHONY: coverage.out
|
||||
install: build ## Install aptly to GOPATH/bin
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Installing aptly...$(COLOR_RESET)"
|
||||
@cp $(BUILD_DIR)/$(BINARY_NAME) $(BINPATH)/
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Installed to $(BINPATH)/$(BINARY_NAME)$(COLOR_RESET)"
|
||||
|
||||
##@ Testing
|
||||
|
||||
test: prepare test-unit test-integration ## Run all tests
|
||||
|
||||
test-unit: prepare swagger etcd-install ## Run unit tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running unit tests...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
$(GOTEST) -v -race -coverprofile=$(COVERAGE_DIR)/unit.out -covermode=atomic ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Unit tests complete$(COLOR_RESET)"
|
||||
|
||||
test-integration: prepare swagger etcd-install ## Run integration tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running integration tests...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
# Download fixtures if needed
|
||||
@if [ ! -e ~/aptly-fixture-db ]; then \
|
||||
git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; \
|
||||
fi
|
||||
@if [ ! -e ~/aptly-fixture-pool ]; then \
|
||||
git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; \
|
||||
fi
|
||||
# Run system tests
|
||||
PATH=$(BINPATH):$$PATH python3 system/run.py --coverage-dir $(COVERAGE_DIR) $(TEST)
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Integration tests complete$(COLOR_RESET)"
|
||||
|
||||
test-race: ## Run tests with race detector
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running tests with race detector...$(COLOR_RESET)"
|
||||
$(GOTEST) -race -short ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Race detection complete$(COLOR_RESET)"
|
||||
|
||||
coverage: test ## Generate coverage report
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating coverage report...$(COLOR_RESET)"
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
@go tool cover -html=$(COVERAGE_DIR)/unit.out -o $(COVERAGE_DIR)/coverage.html
|
||||
@go tool cover -func=$(COVERAGE_DIR)/unit.out
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Coverage report: $(COVERAGE_DIR)/coverage.html$(COLOR_RESET)"
|
||||
|
||||
benchmark: ## Run benchmarks
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running benchmarks...$(COLOR_RESET)"
|
||||
$(GOTEST) -bench=. -benchmem ./deb ./files ./utils
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Benchmarks complete$(COLOR_RESET)"
|
||||
|
||||
##@ Code Quality
|
||||
|
||||
lint: dev-tools ## Run linters
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running linters...$(COLOR_RESET)"
|
||||
@golangci-lint run --timeout=5m
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Linting complete$(COLOR_RESET)"
|
||||
|
||||
fmt: ## Format code
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Formatting code...$(COLOR_RESET)"
|
||||
@$(GOFMT) -w -s .
|
||||
@$(GOMOD) tidy
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Code formatted$(COLOR_RESET)"
|
||||
|
||||
vet: ## Run go vet
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running go vet...$(COLOR_RESET)"
|
||||
@go vet ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Vet complete$(COLOR_RESET)"
|
||||
|
||||
security: dev-tools ## Run security checks
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Running security checks...$(COLOR_RESET)"
|
||||
@govulncheck ./...
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Security check complete$(COLOR_RESET)"
|
||||
|
||||
##@ Dependencies
|
||||
|
||||
deps-update: ## Update dependencies
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Updating dependencies...$(COLOR_RESET)"
|
||||
@./scripts/update-deps.sh
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Dependencies updated$(COLOR_RESET)"
|
||||
|
||||
deps-check: ## Check for outdated dependencies
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Checking for outdated dependencies...$(COLOR_RESET)"
|
||||
@go list -u -m all | grep '\[' || echo "All dependencies are up to date!"
|
||||
|
||||
deps-graph: ## Generate dependency graph
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating dependency graph...$(COLOR_RESET)"
|
||||
@go mod graph | grep -v '@' | sort | uniq
|
||||
|
||||
##@ Documentation
|
||||
|
||||
swagger: swagger-install ## Generate Swagger documentation
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Generating Swagger documentation...$(COLOR_RESET)"
|
||||
@cp docs/swagger.conf.tpl docs/swagger.conf
|
||||
@echo "// @version $(VERSION)" >> docs/swagger.conf
|
||||
@swag init --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Swagger docs generated$(COLOR_RESET)"
|
||||
|
||||
swagger-install: ## Install swagger tools
|
||||
@test -f $(BINPATH)/swag || go install github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)
|
||||
|
||||
docs: swagger ## Generate all documentation
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Documentation generated$(COLOR_RESET)"
|
||||
|
||||
##@ Development Server
|
||||
|
||||
serve: dev-tools prepare ## Run development server with hot reload
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting development server...$(COLOR_RESET)"
|
||||
@cp debian/aptly.conf ~/.aptly.conf || true
|
||||
@sed -i.bak '/enable_swagger_endpoint/s/false/true/' ~/.aptly.conf || true
|
||||
@air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' \
|
||||
-build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu \
|
||||
-- api serve -listen 0.0.0.0:3142
|
||||
|
||||
##@ Docker
|
||||
|
||||
docker-build: ## Build Docker image
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Building Docker image...$(COLOR_RESET)"
|
||||
docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -t $(DOCKER_IMAGE):latest .
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Docker image built: $(DOCKER_IMAGE):$(DOCKER_TAG)$(COLOR_RESET)"
|
||||
|
||||
docker-push: ## Push Docker image
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Pushing Docker image...$(COLOR_RESET)"
|
||||
docker push $(DOCKER_IMAGE):$(DOCKER_TAG)
|
||||
docker push $(DOCKER_IMAGE):latest
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Docker image pushed$(COLOR_RESET)"
|
||||
|
||||
##@ Cleanup
|
||||
|
||||
clean: ## Clean build artifacts
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Cleaning build artifacts...$(COLOR_RESET)"
|
||||
@rm -rf $(BUILD_DIR) $(COVERAGE_DIR)
|
||||
@rm -f docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
@rm -rf obj-* *.out *.test
|
||||
@docker-compose -f docker-compose.ci.yml down || true
|
||||
@docker volume prune -f || true
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Clean complete$(COLOR_RESET)"
|
||||
|
||||
clean-deps: ## Clean dependency cache
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Cleaning dependency cache...$(COLOR_RESET)"
|
||||
@go clean -modcache
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Dependency cache cleaned$(COLOR_RESET)"
|
||||
|
||||
##@ CI/CD
|
||||
|
||||
ci: prepare lint test security ## Run CI pipeline
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ CI pipeline complete$(COLOR_RESET)"
|
||||
|
||||
release: clean build-all ## Prepare release artifacts
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Preparing release...$(COLOR_RESET)"
|
||||
@mkdir -p $(BUILD_DIR)/release
|
||||
@for file in $(BUILD_DIR)/$(BINARY_NAME)-*; do \
|
||||
base=$$(basename $$file); \
|
||||
tar -czf $(BUILD_DIR)/release/$$base.tar.gz -C $(BUILD_DIR) $$base; \
|
||||
done
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Release artifacts ready in $(BUILD_DIR)/release$(COLOR_RESET)"
|
||||
|
||||
##@ Utilities
|
||||
|
||||
etcd-install: ## Install etcd for testing
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting fresh etcd container...$(COLOR_RESET)"
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Waiting for etcd to be ready...$(COLOR_RESET)"
|
||||
@sleep 5
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd started in Docker$(COLOR_RESET)"
|
||||
|
||||
etcd-start: ## Start etcd
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting fresh etcd container...$(COLOR_RESET)"
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml up -d etcd
|
||||
@sleep 5
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd started in Docker$(COLOR_RESET)"
|
||||
|
||||
etcd-stop: ## Stop etcd
|
||||
@docker-compose -f docker-compose.ci.yml down etcd 2>/dev/null || true
|
||||
@docker-compose -f docker-compose.ci.yml rm -f -v etcd 2>/dev/null || true
|
||||
@docker volume rm aptly_etcd-data 2>/dev/null || true
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ etcd stopped and cleaned$(COLOR_RESET)"
|
||||
|
||||
azurite-start: ## Start Azurite (Azure Storage Emulator) for tests
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Starting Azurite...$(COLOR_RESET)"
|
||||
@azurite -l /tmp/aptly-azurite > ~/.azurite.log 2>&1 & \
|
||||
echo $$! > ~/.azurite.pid
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Azurite started (PID: $$(cat ~/.azurite.pid))$(COLOR_RESET)"
|
||||
|
||||
azurite-stop: ## Stop Azurite
|
||||
@echo -e "$(COLOR_YELLOW)$(COLOR_BOLD)Stopping Azurite...$(COLOR_RESET)"
|
||||
@-kill `cat ~/.azurite.pid` 2>/dev/null || true
|
||||
@rm -f ~/.azurite.pid
|
||||
@echo -e "$(COLOR_GREEN)$(COLOR_BOLD)✓ Azurite stopped$(COLOR_RESET)"
|
||||
|
||||
.PHONY: all build build-all install test test-unit test-integration test-race coverage benchmark \
|
||||
lint fmt vet security deps-update deps-check deps-graph docs swagger swagger-install serve \
|
||||
docker-build docker-push clean clean-deps ci release prepare dev-tools etcd-install etcd-start etcd-stop \
|
||||
azurite-start azurite-stop
|
||||
+198
-40
@@ -1,25 +1,27 @@
|
||||
=====
|
||||
.. image:: https://github.com/aptly-dev/aptly/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/aptly-dev/aptly/actions
|
||||
|
||||
.. image:: https://codecov.io/gh/aptly-dev/aptly/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/aptly-dev/aptly
|
||||
|
||||
.. image:: https://badges.gitter.im/Join Chat.svg
|
||||
:target: https://matrix.to/#/#aptly:gitter.im
|
||||
|
||||
.. image:: https://goreportcard.com/badge/github.com/aptly-dev/aptly
|
||||
:target: https://goreportcard.com/report/aptly-dev/aptly
|
||||
|
||||
aptly
|
||||
=====
|
||||
|
||||
.. image:: https://travis-ci.org/smira/aptly.png?branch=master
|
||||
:target: https://travis-ci.org/smira/aptly
|
||||
|
||||
.. image:: https://coveralls.io/repos/smira/aptly/badge.png?branch=HEAD
|
||||
:target: https://coveralls.io/r/smira/aptly?branch=HEAD
|
||||
|
||||
.. image:: http://gobuild.io/badge/github.com/smira/aptly/download.png
|
||||
:target: http://gobuild.io/github.com/smira/aptly
|
||||
|
||||
Aptly is a swiss army knife for Debian repository management.
|
||||
|
||||
.. image:: http://www.aptly.info/img/aptly_logo.png
|
||||
:target: http://www.aptly.info/
|
||||
|
||||
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support use
|
||||
mailing list `aptly-discuss <https://groups.google.com/forum/#!forum/aptly-discuss>`_.
|
||||
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support please use
|
||||
open `issues <https://github.com/aptly-dev/aptly/issues>`_ or `discussions <https://github.com/aptly-dev/aptly/discussions>`_.
|
||||
|
||||
Aptly features: ("+" means planned features)
|
||||
Aptly features:
|
||||
|
||||
* make mirrors of remote Debian/Ubuntu repositories, limiting by components/architectures
|
||||
* take snapshots of mirrors at any point in time, fixing state of repository at some moment of time
|
||||
@@ -28,47 +30,203 @@ Aptly features: ("+" means planned features)
|
||||
* merge two or more snapshots into one
|
||||
* filter repository by search query, pulling dependencies when required
|
||||
* publish self-made packages as Debian repositories
|
||||
* mirror repositories "as-is" (without resigning with user's key) (+)
|
||||
* support for yum repositories (+)
|
||||
* REST API for remote access
|
||||
|
||||
Current limitations:
|
||||
Any contributions are welcome! Please see `CONTRIBUTING.md <CONTRIBUTING.md>`_.
|
||||
|
||||
* translations are not supported yet
|
||||
Installation
|
||||
=============
|
||||
|
||||
Download
|
||||
--------
|
||||
Aptly can be installed on several operating systems.
|
||||
|
||||
To install aptly on Debian/Ubuntu, add new repository to /etc/apt/sources.list::
|
||||
Debian / Ubuntu
|
||||
----------------
|
||||
|
||||
deb http://repo.aptly.info/ squeeze main
|
||||
Aptly is provided in the following debian packages:
|
||||
|
||||
And import key that is used to sign the release::
|
||||
* **aptly**: Includes the main Aptly binary, man pages, and shell completions
|
||||
* **aptly-api**: A systemd service for the REST API, using the global /etc/aptly.conf
|
||||
* **aptly-dbg**: Debug symbols for troubleshooting
|
||||
|
||||
$ gpg --keyserver keys.gnupg.net --recv-keys 2A194991
|
||||
$ gpg -a --export 2A194991 | sudo apt-key add -
|
||||
The packages can be installed on official `Debian <https://packages.debian.org/search?keywords=aptly>`_ and `Ubuntu <https://packages.ubuntu.com/search?keywords=aptly>`_ distributions.
|
||||
|
||||
After that you can install aptly as any other software package::
|
||||
Upstream Debian Packages
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
$ apt-get update
|
||||
$ apt-get install aptly
|
||||
If a newer version (not available in Debian/Ubuntu) of aptly is required, upstream debian packages (built from git tags) can be installed as follows:
|
||||
|
||||
Don't worry about squeeze part in repo name: aptly package should work on Debian squeeze+,
|
||||
Ubuntu 10.0+. Package contains aptly binary, man page and bash completion.
|
||||
Install the following APT key (as root)::
|
||||
|
||||
Binary executables (depends almost only on libc) are available for download from `Bintray <http://dl.bintray.com/smira/aptly/>`_.
|
||||
wget -O /etc/apt/keyrings/aptly.asc https://www.aptly.info/pubkey.txt
|
||||
|
||||
If you have Go environment set up, you can build aptly from source by running (go 1.2+ required)::
|
||||
Define Release APT sources in ``/etc/apt/sources.list.d/aptly.list``::
|
||||
|
||||
go get -u github.com/mattn/gom
|
||||
mkdir -p $GOPATH/src/github.com/smira/aptly
|
||||
git clone https://github.com/smira/aptly $GOPATH/src/github.com/smira/aptly
|
||||
cd $GOPATH/src/github.com/smira/aptly
|
||||
gom -production install
|
||||
gom build -o $GOPATH/bin/aptly
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/release DIST main
|
||||
|
||||
Aptly is using `gom <https://github.com/mattn/gom>`_ to fix external dependencies, so regular ``go get github.com/smira/aptly``
|
||||
should work as well, but might fail or produce different result (if external libraries got updated).
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
If you don't have Go installed (or older version), you can easily install Go using `gvm <https://github.com/moovweb/gvm/>`_.
|
||||
Install aptly packages::
|
||||
|
||||
apt-get update
|
||||
apt-get install aptly
|
||||
apt-get install aptly-api # REST API systemd service
|
||||
|
||||
CI Builds
|
||||
~~~~~~~~~~
|
||||
|
||||
For testing new features or bugfixes, recent builds are available as CI builds (built from master, may be unstable!) and can be installed as follows:
|
||||
|
||||
Define CI APT sources in ``/etc/apt/sources.list.d/aptly-ci.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/ci DIST main
|
||||
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Note: same gpg key is used as for the Upstream Debian Packages.
|
||||
|
||||
Other Operating Systems
|
||||
------------------------
|
||||
|
||||
Binary executables (depends almost only on libc) are available on `GitHub Releases <https://github.com/aptly-dev/aptly/releases>`_ for:
|
||||
|
||||
- macOS / darwin (amd64, arm64)
|
||||
- FreeBSD (amd64, arm64, 386, arm)
|
||||
- Generic Linux (amd64, arm64, 386, arm)
|
||||
|
||||
Integrations
|
||||
=============
|
||||
|
||||
Vagrant:
|
||||
|
||||
- `Vagrant configuration <https://github.com/sepulworld/aptly-vagrant>`_ by
|
||||
Zane Williamson, allowing to bring two virtual servers, one with aptly installed
|
||||
and another one set up to install packages from repository published by aptly
|
||||
|
||||
Docker:
|
||||
|
||||
- `Docker container <https://github.com/mikepurvis/aptly-docker>`_ with aptly inside by Mike Purvis
|
||||
- `Docker container <https://github.com/urpylka/docker-aptly>`_ with aptly and nginx by Artem Smirnov
|
||||
|
||||
With configuration management systems:
|
||||
|
||||
- `Chef cookbook <https://github.com/hw-cookbooks/aptly>`_ by Aaron Baer
|
||||
(Heavy Water Operations, LLC)
|
||||
- `Puppet module <https://github.com/alphagov/puppet-aptly>`_ by
|
||||
Government Digital Services
|
||||
- `Puppet module <https://github.com/tubemogul/puppet-aptly>`_ by
|
||||
TubeMogul
|
||||
- `SaltStack Formula <https://github.com/saltstack-formulas/aptly-formula>`_ by
|
||||
Forrest Alvarez and Brian Jackson
|
||||
- `Ansible role <https://github.com/aioue/ansible-role-aptly>`_ by Tom Paine
|
||||
|
||||
CLI for aptly API:
|
||||
|
||||
- `Ruby aptly CLI/library <https://github.com/sepulworld/aptly_cli>`_ by Zane Williamson
|
||||
- `Python aptly CLI (good for CI) <https://github.com/TimSusa/aptly_api_cli>`_ by Tim Susa
|
||||
|
||||
GUI for aptly API:
|
||||
|
||||
- `Python aptly GUI (via pyqt5) <https://github.com/chnyda/python-aptly-gui>`_ by Cedric Hnyda
|
||||
|
||||
Scala sbt:
|
||||
|
||||
- `sbt aptly plugin <https://github.com/amalakar/sbt-aptly>`_ by Arup Malakar
|
||||
|
||||
Molior:
|
||||
|
||||
- `Molior Debian Build System <https://github.com/molior-dbs/molior>`_ by André Roth
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
etcd Database Configuration
|
||||
---------------------------
|
||||
|
||||
When using etcd as the database backend, aptly supports several environment variables for configuration:
|
||||
|
||||
**Timeout Configuration:**
|
||||
|
||||
- ``APTLY_ETCD_TIMEOUT``: Operation timeout for etcd requests (default: ``60s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_TIMEOUT=30s``
|
||||
|
||||
- ``APTLY_ETCD_DIAL_TIMEOUT``: Connection timeout when establishing etcd connection (default: ``60s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_DIAL_TIMEOUT=10s``
|
||||
|
||||
**Connection Configuration:**
|
||||
|
||||
- ``APTLY_ETCD_KEEPALIVE``: Keep-alive timeout for etcd connections (default: ``7200s``)
|
||||
|
||||
Example: ``export APTLY_ETCD_KEEPALIVE=3600s``
|
||||
|
||||
- ``APTLY_ETCD_MAX_MSG_SIZE``: Maximum message size in bytes for etcd requests/responses (default: ``52428800`` - 50MB)
|
||||
|
||||
Example: ``export APTLY_ETCD_MAX_MSG_SIZE=104857600`` # 100MB
|
||||
|
||||
**Example Configuration:**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Set shorter timeouts for faster failure detection
|
||||
export APTLY_ETCD_TIMEOUT=30s
|
||||
export APTLY_ETCD_DIAL_TIMEOUT=10s
|
||||
|
||||
# Increase message size for large package operations
|
||||
export APTLY_ETCD_MAX_MSG_SIZE=104857600
|
||||
|
||||
# Run aptly with etcd backend
|
||||
aptly -config=/etc/aptly-etcd.conf mirror update debian-stable
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Automatic Retry**: Read operations (Get) automatically retry up to 3 times with exponential backoff on temporary failures
|
||||
- **Timeout Protection**: All etcd operations use context with timeout to prevent indefinite hangs
|
||||
- **Enhanced Logging**: All etcd errors are logged with operation context for better debugging
|
||||
- **Configurable Limits**: Message size limits can be adjusted for large package operations
|
||||
|
||||
etcd Write Queue Configuration
|
||||
------------------------------
|
||||
|
||||
To prevent etcd overload during concurrent operations (e.g., multiple mirror updates), aptly supports an optional write queue that serializes database write operations:
|
||||
|
||||
**Configuration in aptly.conf:**
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"databaseBackend": {
|
||||
"type": "etcd",
|
||||
"url": "localhost:2379",
|
||||
"timeout": "120s",
|
||||
"writeRetries": 3,
|
||||
"writeQueue": {
|
||||
"enabled": true,
|
||||
"queueSize": 1000,
|
||||
"maxWritesPerSec": 100,
|
||||
"batchMaxSize": 50,
|
||||
"batchMaxWaitMs": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
**Write Queue Options:**
|
||||
|
||||
- ``enabled``: Enable/disable the write queue (default: ``false``)
|
||||
- ``queueSize``: Size of the write operation queue (default: ``1000``)
|
||||
- ``maxWritesPerSec``: Maximum write operations per second (default: ``100``)
|
||||
- ``batchMaxSize``: Maximum batch size for future batching support (default: ``50``)
|
||||
- ``batchMaxWaitMs``: Maximum wait time for batch accumulation in milliseconds (default: ``10``)
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Prevents etcd Overload**: Serializes write operations to avoid overwhelming etcd
|
||||
- **Maintains Parallelism**: I/O operations like downloads remain parallel
|
||||
- **Rate Limiting**: Configurable writes per second to match etcd capacity
|
||||
- **Transparent**: No code changes required, just enable in configuration
|
||||
|
||||
**Example Impact:**
|
||||
|
||||
Without write queue: 5 mirror updates → 5 parallel writers → 1000s of concurrent etcd operations → timeouts
|
||||
|
||||
With write queue: 5 mirror updates → 5 parallel processes → 1 sequential etcd writer → stable performance
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Creating a Release
|
||||
|
||||
- create branch release/1.x.y
|
||||
- update debian/changelog
|
||||
- create PR, merge when approved
|
||||
- on updated master, create release:
|
||||
```
|
||||
version=$(dpkg-parsechangelog -S Version)
|
||||
echo Releasing prod version $version
|
||||
git tag -a v$version -m 'aptly: release $version'
|
||||
git push origin v$version master
|
||||
```
|
||||
- run swagger locally (`make docker-serve`)
|
||||
- copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json
|
||||
- add new version to select tag in content/doc/api/swagger.md line 48
|
||||
- push commit to master
|
||||
- create release announcement on https://github.com/aptly-dev/aptly/discussions
|
||||
@@ -2,16 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/cmd"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/aptly-dev/aptly/cmd"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func allFlags(flags *flag.FlagSet) []*flag.Flag {
|
||||
@@ -43,22 +44,42 @@ func capitalize(s string) string {
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
var authorsS string
|
||||
|
||||
func authors() string {
|
||||
return authorsS
|
||||
}
|
||||
|
||||
func main() {
|
||||
command := cmd.RootCommand()
|
||||
command.UsageLine = "aptly"
|
||||
command.Dispatch(nil)
|
||||
|
||||
_, _File, _, _ := runtime.Caller(0)
|
||||
_File, _ = filepath.Abs(_File)
|
||||
_File, _ := filepath.Abs("./man")
|
||||
|
||||
templ := template.New("man").Funcs(template.FuncMap{
|
||||
"allFlags": allFlags,
|
||||
"findCommand": findCommand,
|
||||
"toUpper": strings.ToUpper,
|
||||
"capitalize": capitalize,
|
||||
"authors": authors,
|
||||
})
|
||||
template.Must(templ.ParseFiles(filepath.Join(filepath.Dir(_File), "aptly.1.ronn.tmpl")))
|
||||
|
||||
authorsF, err := os.Open(filepath.Join(filepath.Dir(_File), "..", "AUTHORS"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
authorsB, err := ioutil.ReadAll(authorsF)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
authorsF.Close()
|
||||
|
||||
authorsS = string(authorsB)
|
||||
|
||||
output, err := os.Create(filepath.Join(filepath.Dir(_File), "aptly.1.ronn"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
// Package api provides implementation of aptly REST API
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Lock order acquisition (canonical):
|
||||
// 1. RemoteRepoCollection
|
||||
// 2. LocalRepoCollection
|
||||
// 3. SnapshotCollection
|
||||
// 4. PublishedRepoCollection
|
||||
|
||||
type aptlyVersion struct {
|
||||
// Aptly Version
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// @Summary Aptly Version
|
||||
// @Description **Get aptly version**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/version
|
||||
// @Description {"Version":"0.9~dev"}
|
||||
// @Description ```
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyVersion
|
||||
// @Router /api/version [get]
|
||||
func apiVersion(c *gin.Context) {
|
||||
version := aptlyVersion{
|
||||
Version: aptly.Version,
|
||||
}
|
||||
c.JSON(200, version)
|
||||
}
|
||||
|
||||
type aptlyStatus struct {
|
||||
// Aptly Status
|
||||
Status string `json:"Status" example:"'Aptly is ready', 'Aptly is unavailable', 'Aptly is healthy'"`
|
||||
}
|
||||
|
||||
// @Summary Get Ready State
|
||||
// @Description **Get aptly ready state**
|
||||
// @Description
|
||||
// @Description Return aptly ready state:
|
||||
// @Description - `Aptly is ready` (HTTP 200)
|
||||
// @Description - `Aptly is unavailable` (HTTP 503)
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyStatus "Aptly is ready"
|
||||
// @Failure 503 {object} aptlyStatus "Aptly is unavailable"
|
||||
// @Router /api/ready [get]
|
||||
func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
if isReady == nil || !isReady.Load().(bool) {
|
||||
c.JSON(503, gin.H{"Status": "Aptly is unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Get Health State
|
||||
// @Description **Get aptly health state**
|
||||
// @Description
|
||||
// @Description Return aptly health state:
|
||||
// @Description - `Aptly is healthy` (HTTP 200)
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} aptlyStatus
|
||||
// @Router /api/healthy [get]
|
||||
func apiHealthy(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"Status": "Aptly is healthy"})
|
||||
}
|
||||
|
||||
type dbRequestKind int
|
||||
|
||||
const (
|
||||
acquiredb dbRequestKind = iota
|
||||
releasedb
|
||||
)
|
||||
|
||||
type dbRequest struct {
|
||||
kind dbRequestKind
|
||||
err chan<- error
|
||||
}
|
||||
|
||||
var (
|
||||
dbRequests chan dbRequest
|
||||
dbRequestsOnce sync.Once
|
||||
)
|
||||
|
||||
// initDBRequests initializes the database request channel in a thread-safe manner
|
||||
func initDBRequests() {
|
||||
dbRequestsOnce.Do(func() {
|
||||
dbRequests = make(chan dbRequest, 1)
|
||||
go acquireDatabase()
|
||||
})
|
||||
}
|
||||
|
||||
// Acquire database lock and release it when not needed anymore.
|
||||
//
|
||||
// Should be run in a goroutine!
|
||||
func acquireDatabase() {
|
||||
clients := 0
|
||||
for request := range dbRequests {
|
||||
var err error
|
||||
|
||||
switch request.kind {
|
||||
case acquiredb:
|
||||
if clients == 0 {
|
||||
err = context.ReOpenDatabase()
|
||||
}
|
||||
|
||||
request.err <- err
|
||||
|
||||
if err == nil {
|
||||
clients++
|
||||
}
|
||||
case releasedb:
|
||||
clients--
|
||||
if clients == 0 {
|
||||
err = context.CloseDatabase()
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
|
||||
request.err <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should be called before database access is needed in any api call.
|
||||
// Happens per default for each api call. It is important that you run
|
||||
// runTaskInBackground to run a task which accquire database.
|
||||
// Important do not forget to defer to releaseDatabaseConnection
|
||||
func acquireDatabaseConnection() error {
|
||||
// Ensure channel is initialized
|
||||
initDBRequests()
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{acquiredb, errCh}
|
||||
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// Release database connection when not needed anymore
|
||||
func releaseDatabaseConnection() error {
|
||||
// Ensure channel is initialized
|
||||
initDBRequests()
|
||||
|
||||
errCh := make(chan error)
|
||||
dbRequests <- dbRequest{releasedb, errCh}
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// runs tasks in background. Acquires database connection first.
|
||||
func runTaskInBackground(name string, resources []string, proc task.Process) (task.Task, *task.ResourceConflictError) {
|
||||
return context.TaskList().RunTaskInBackground(name, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := acquireDatabaseConnection()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = releaseDatabaseConnection() }()
|
||||
return proc(out, detail)
|
||||
})
|
||||
}
|
||||
|
||||
func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(v) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case int:
|
||||
return v != 0
|
||||
case bool:
|
||||
return v
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func maybeRunTaskInBackground(c *gin.Context, name string, resources []string, proc task.Process) {
|
||||
// Run this task in background if configured globally or per-request
|
||||
background := truthy(c.DefaultQuery("_async", strconv.FormatBool(context.Config().AsyncAPI)))
|
||||
if background {
|
||||
log.Debug().Msg("Executing task asynchronously")
|
||||
task, conflictErr := runTaskInBackground(name, resources, proc)
|
||||
if conflictErr != nil {
|
||||
AbortWithJSONError(c, 409, conflictErr)
|
||||
return
|
||||
}
|
||||
c.JSON(202, task)
|
||||
} else {
|
||||
log.Debug().Msg("Executing task synchronously")
|
||||
task, conflictErr := runTaskInBackground(name, resources, proc)
|
||||
if conflictErr != nil {
|
||||
AbortWithJSONError(c, 409, conflictErr)
|
||||
return
|
||||
}
|
||||
|
||||
// wait for task to finish
|
||||
_, _ = context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
retValue, _ := context.TaskList().GetTaskReturnValueByID(task.ID)
|
||||
err, _ := context.TaskList().GetTaskErrorByID(task.ID)
|
||||
_, _ = context.TaskList().DeleteTaskByID(task.ID)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, retValue.Code, err)
|
||||
return
|
||||
}
|
||||
if retValue != nil {
|
||||
c.JSON(retValue.Code, retValue.Value)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common piece of code to show list of packages,
|
||||
// with searching & details if requested
|
||||
func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory *deb.CollectionFactory) {
|
||||
result := []*deb.Package{}
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
queryS := c.Request.URL.Query().Get("q")
|
||||
if queryS != "" {
|
||||
q, err := query.Parse(c.Request.URL.Query().Get("q"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
withDeps := c.Request.URL.Query().Get("withDeps") == "1"
|
||||
architecturesList := []string{}
|
||||
|
||||
if withDeps {
|
||||
if len(context.ArchitecturesList()) > 0 {
|
||||
architecturesList = context.ArchitecturesList()
|
||||
} else {
|
||||
architecturesList = list.Architectures(false)
|
||||
}
|
||||
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to determine list of architectures, please specify explicitly"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
|
||||
list, err = list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{q},
|
||||
WithDependencies: withDeps,
|
||||
Source: nil,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to search: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// filter packages by version
|
||||
if c.Request.URL.Query().Get("maximumVersion") == "1" {
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
versionQ, err := query.Parse(fmt.Sprintf("Name (%s), $Version (<= %s)", p.Name, p.Version))
|
||||
if err != nil {
|
||||
fmt.Println("filter packages by version, query string parse err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
|
||||
} else {
|
||||
tmpList, err := list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{versionQ},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
if tmpList.Len() > 0 {
|
||||
_ = tmpList.ForEach(func(tp *deb.Package) error {
|
||||
list.Remove(tp)
|
||||
return nil
|
||||
})
|
||||
_ = list.Add(p)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("filter packages by version, filter err: ", err)
|
||||
_ = c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
} else {
|
||||
c.JSON(200, list.Strings())
|
||||
}
|
||||
}
|
||||
|
||||
func AbortWithJSONError(c *gin.Context, code int, err error) {
|
||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = c.AbortWithError(code, err)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ApiPackagesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ApiPackagesSuite{})
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackages(c *C) {
|
||||
// Test showPackages function with nil reflist
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
showPackages(ginCtx, nil, s.context.NewCollectionFactory())
|
||||
|
||||
// Should return 404 for nil reflist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesWithEmptyList(c *C) {
|
||||
// Test showPackages with empty package reflist
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesCompact(c *C) {
|
||||
// Test showPackages with compact format (default)
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ApiPackagesSuite) TestShowPackagesDetails(c *C) {
|
||||
// Test showPackages with details format
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?format=details", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []*deb.Package
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type APISuite struct {
|
||||
context *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&APISuite{})
|
||||
|
||||
func createTestConfig() *os.File {
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"enableMetricsEndpoint": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, _ = file.Write(jsonString)
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *APISuite) setupContext() error {
|
||||
aptly.Version = "testVersion"
|
||||
file := createTestConfig()
|
||||
if nil == file {
|
||||
return fmt.Errorf("unable to create the test configuration file")
|
||||
}
|
||||
s.configFile = file
|
||||
|
||||
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", false, "dummy")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "dummy")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
context, err := ctx.NewContext(s.flags)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
s.context = context
|
||||
s.router = Router(context)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpSuite(c *C) {
|
||||
err := s.setupContext()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownSuite(c *C) {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
s.context.Shutdown()
|
||||
}
|
||||
|
||||
func (s *APISuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *APISuite) HTTPRequest(method string, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGinRunsInReleaseMode(c *C) {
|
||||
c.Check(gin.Mode(), Equals, gin.ReleaseMode)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetVersion(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/version", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Version\":\""+aptly.Version+"\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetReadiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/ready", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is ready\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetHealthiness(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/healthy", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, "{\"Status\":\"Aptly is healthy\"}")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetMetrics(c *C) {
|
||||
response, err := s.HTTPRequest("GET", "/api/metrics", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
b := strings.Replace(response.Body.String(), "\n", "", -1)
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_requests_in_flight gauge.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_requests_total counter.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_request_size_bytes summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_response_size_bytes summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_api_http_request_duration_seconds summary.*")
|
||||
c.Check(b, Matches, ".*# TYPE aptly_build_info gauge.*")
|
||||
c.Check(b, Matches, ".*aptly_build_info.*version=\"testVersion\".*")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestRepoCreate(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
resp, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 201)
|
||||
|
||||
// Clean up: delete the created repo
|
||||
resp, err = s.HTTPRequest("DELETE", "/api/repos/dummy?force=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestTruthy(c *C) {
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
c.Check(truthy("false"), Equals, false)
|
||||
c.Check(truthy("0"), Equals, false)
|
||||
c.Check(truthy(false), Equals, false)
|
||||
c.Check(truthy(0), Equals, false)
|
||||
|
||||
c.Check(truthy("y"), Equals, true)
|
||||
c.Check(truthy("yes"), Equals, true)
|
||||
c.Check(truthy("t"), Equals, true)
|
||||
c.Check(truthy("true"), Equals, true)
|
||||
c.Check(truthy("1"), Equals, true)
|
||||
c.Check(truthy(true), Equals, true)
|
||||
c.Check(truthy(1), Equals, true)
|
||||
|
||||
c.Check(truthy(nil), Equals, false)
|
||||
|
||||
c.Check(truthy("foobar"), Equals, true)
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(gin.H{}), Equals, true)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestDatabaseConnectionFunctions(c *C) {
|
||||
// Test acquire and release database connection
|
||||
err := acquireDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = releaseDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestConcurrentDatabaseRequests(c *C) {
|
||||
// Test concurrent database acquisition
|
||||
done := make(chan bool, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer func() { done <- true }()
|
||||
|
||||
err := acquireDatabaseConnection()
|
||||
if err == nil {
|
||||
_ = releaseDatabaseConnection()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 5; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // If we get here, no deadlock occurred
|
||||
}
|
||||
|
||||
func (s *APISuite) TestMaybeRunTaskInBackground(c *C) {
|
||||
// Test synchronous task execution
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
called := false
|
||||
maybeRunTaskInBackground(ginCtx, "test-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
called = true
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"status": "ok"}}, nil
|
||||
})
|
||||
|
||||
c.Check(called, Equals, true)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestMaybeRunTaskInBackgroundAsync(c *C) {
|
||||
// Test asynchronous task execution
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?_async=true", nil)
|
||||
|
||||
maybeRunTaskInBackground(ginCtx, "test-async-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"status": "ok"}}, nil
|
||||
})
|
||||
|
||||
// For async, should return 202 Accepted
|
||||
c.Check(w.Code, Equals, 202)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAbortWithJSONError(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
|
||||
testErr := fmt.Errorf("test error message")
|
||||
AbortWithJSONError(ginCtx, 400, testErr)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesWithNilList(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
showPackages(ginCtx, nil, s.context.NewCollectionFactory())
|
||||
|
||||
// Should return error when reflist is nil
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAPIVersionConstant(c *C) {
|
||||
// Test that apiVersion struct is properly defined
|
||||
version := aptlyVersion{Version: "test-version"}
|
||||
c.Check(version.Version, Equals, "test-version")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestAPIStatusConstant(c *C) {
|
||||
// Test that aptlyStatus struct is properly defined
|
||||
status := aptlyStatus{Status: "test-status"}
|
||||
c.Check(status.Status, Equals, "test-status")
|
||||
}
|
||||
|
||||
func (s *APISuite) TestRunTaskInBackground(c *C) {
|
||||
// Test running task in background
|
||||
task, err := runTaskInBackground("background-test", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200, Value: gin.H{"done": true}}, nil
|
||||
})
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(task, NotNil)
|
||||
c.Check(task.Name, Equals, "background-test")
|
||||
|
||||
// Wait for task to complete
|
||||
_, _ = s.context.TaskList().WaitForTaskByID(task.ID)
|
||||
|
||||
// Clean up
|
||||
_, _ = s.context.TaskList().DeleteTaskByID(task.ID)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestInitDBRequests(c *C) {
|
||||
// Test that initDBRequests can be called multiple times safely
|
||||
initDBRequests()
|
||||
initDBRequests() // Should not panic
|
||||
|
||||
c.Check(dbRequests, NotNil)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesWithQuery(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test?q=Name&format=details", nil)
|
||||
|
||||
// Create empty reflist
|
||||
reflist := deb.NewPackageRefList()
|
||||
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
// Should succeed with empty list
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []*deb.Package
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestShowPackagesCompactFormat(c *C) {
|
||||
// Test compact format (default)
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
reflist := deb.NewPackageRefList()
|
||||
showPackages(ginCtx, reflist, s.context.NewCollectionFactory())
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestTruthyEdgeCases(c *C) {
|
||||
// Test edge cases for truthy function
|
||||
c.Check(truthy("F"), Equals, false) // capital F
|
||||
c.Check(truthy("FALSE"), Equals, false) // all caps
|
||||
c.Check(truthy("False"), Equals, false) // mixed case
|
||||
c.Check(truthy("NO"), Equals, false) // capital NO
|
||||
c.Check(truthy("Off"), Equals, false) // mixed case off
|
||||
|
||||
// Test empty string
|
||||
c.Check(truthy(""), Equals, true) // empty string is truthy
|
||||
|
||||
// Test other types
|
||||
c.Check(truthy(struct{}{}), Equals, true) // empty struct
|
||||
c.Check(truthy([]int{}), Equals, true) // empty slice
|
||||
c.Check(truthy(map[string]int{}), Equals, true) // empty map
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary DB Cleanup
|
||||
// @Description **Cleanup Aptly DB**
|
||||
// @Description Database cleanup removes information about unreferenced packages and deletes files in the package pool that aren’t used by packages anymore.
|
||||
// @Description It is a good idea to run this command after massive deletion of mirrors, snapshots or local repos.
|
||||
// @Tags Database
|
||||
// @Produce json
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 200 {object} string "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/db/cleanup [post]
|
||||
func apiDBCleanup(c *gin.Context) {
|
||||
resources := []string{string(task.AllResourcesKey)}
|
||||
maybeRunTaskInBackground(c, "Clean up db", resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
var err error
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// collect information about referenced packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
|
||||
out.Printf("Loading mirrors, local repos, snapshots and published repos...")
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
existingPackageRefs = existingPackageRefs.Merge(snapshot.RefList(), false, true)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(published *deb.PublishedRepo) error {
|
||||
if published.SourceKind != deb.SourceLocalRepo {
|
||||
return nil
|
||||
}
|
||||
e := collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
for _, component := range published.Components() {
|
||||
existingPackageRefs = existingPackageRefs.Merge(published.RefList(component), false, true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ... and compare it to the list of all packages
|
||||
out.Printf("Loading list of all packages...")
|
||||
allPackageRefs := collectionFactory.PackageCollection().AllPackageRefs()
|
||||
|
||||
toDelete := allPackageRefs.Subtract(existingPackageRefs)
|
||||
|
||||
// delete packages that are no longer referenced
|
||||
out.Printf("Deleting unreferenced packages (%d)...", toDelete.Len())
|
||||
|
||||
// database can't err as collection factory already constructed
|
||||
db, _ := context.Database()
|
||||
|
||||
if toDelete.Len() > 0 {
|
||||
batch := db.CreateBatch()
|
||||
_ = toDelete.ForEach(func(ref []byte) error {
|
||||
_ = collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
return nil
|
||||
})
|
||||
|
||||
err = batch.Write()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to write to DB: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// now, build a list of files that should be present in Repository (package pool)
|
||||
out.Printf("Building list of files referenced by packages...")
|
||||
referencedFiles := make([]string, 0, existingPackageRefs.Len())
|
||||
|
||||
err = existingPackageRefs.ForEach(func(key []byte) error {
|
||||
pkg, err2 := collectionFactory.PackageCollection().ByKey(key)
|
||||
if err2 != nil {
|
||||
tail := ""
|
||||
return fmt.Errorf("unable to load package %s: %s%s", string(key), err2, tail)
|
||||
}
|
||||
paths, err2 := pkg.FilepathList(context.PackagePool())
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
referencedFiles = append(referencedFiles, paths...)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(referencedFiles)
|
||||
|
||||
// build a list of files in the package pool
|
||||
out.Printf("Building list of files in package pool...")
|
||||
existingFiles, err := context.PackagePool().FilepathList(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to collect file paths: %s", err)
|
||||
}
|
||||
|
||||
// find files which are in the pool but not referenced by packages
|
||||
filesToDelete := utils.StrSlicesSubstract(existingFiles, referencedFiles)
|
||||
|
||||
// delete files that are no longer referenced
|
||||
out.Printf("Deleting unreferenced files (%d)...", len(filesToDelete))
|
||||
|
||||
countFilesToDelete := len(filesToDelete)
|
||||
taskDetail := struct {
|
||||
TotalNumberOfPackagesToDelete int
|
||||
RemainingNumberOfPackagesToDelete int
|
||||
}{
|
||||
countFilesToDelete, countFilesToDelete,
|
||||
}
|
||||
detail.Store(taskDetail)
|
||||
|
||||
if countFilesToDelete > 0 {
|
||||
var size, totalSize int64
|
||||
for _, file := range filesToDelete {
|
||||
size, err = context.PackagePool().Remove(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDetail.RemainingNumberOfPackagesToDelete--
|
||||
detail.Store(taskDetail)
|
||||
totalSize += size
|
||||
}
|
||||
|
||||
out.Printf("Disk space freed: %s...", utils.HumanBytes(totalSize))
|
||||
}
|
||||
|
||||
out.Printf("Compacting database...")
|
||||
return nil, db.CompactDB()
|
||||
})
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type DBTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&DBTestSuite{})
|
||||
|
||||
func (s *DBTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupStructure(c *C) {
|
||||
// Test database cleanup endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with proper context
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithAsync(c *C) {
|
||||
// Test database cleanup with async parameter
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?_async=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return task response when async
|
||||
c.Check(w.Code, Equals, 202)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithDryRun(c *C) {
|
||||
// Test database cleanup with dry run parameter
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?dry-run=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with dry run
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithBothParams(c *C) {
|
||||
// Test database cleanup with both async and dry-run parameters
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup?_async=1&dry-run=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter combination
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupHTTPMethods(c *C) {
|
||||
// Test that only POST method is allowed
|
||||
deniedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupWithRequestBody(c *C) {
|
||||
// Test database cleanup with various request bodies (should be ignored)
|
||||
testBodies := []string{
|
||||
"",
|
||||
"some random text",
|
||||
`{"key": "value"}`,
|
||||
`<xml>data</xml>`,
|
||||
"binary\x00\x01\x02data",
|
||||
}
|
||||
|
||||
for i, body := range testBodies {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle various body content without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Body test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupParameterVariations(c *C) {
|
||||
// Test various parameter value combinations
|
||||
paramTests := []struct {
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{"", "no parameters"},
|
||||
{"_async=0", "async disabled"},
|
||||
{"_async=false", "async false"},
|
||||
{"_async=true", "async true"},
|
||||
{"dry-run=0", "dry-run disabled"},
|
||||
{"dry-run=false", "dry-run false"},
|
||||
{"dry-run=true", "dry-run true"},
|
||||
{"_async=1&dry-run=0", "async on, dry-run off"},
|
||||
{"_async=0&dry-run=1", "async off, dry-run on"},
|
||||
{"_async=true&dry-run=false", "async true, dry-run false"},
|
||||
{"unknown=param", "unknown parameter"},
|
||||
{"_async=invalid", "invalid async value"},
|
||||
{"dry-run=invalid", "invalid dry-run value"},
|
||||
}
|
||||
|
||||
for _, test := range paramTests {
|
||||
path := "/api/db/cleanup"
|
||||
if test.query != "" {
|
||||
path += "?" + test.query
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle all parameter variations without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupContentTypes(c *C) {
|
||||
// Test different content types
|
||||
contentTypes := []string{
|
||||
"",
|
||||
"application/json",
|
||||
"text/plain",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"application/octet-stream",
|
||||
}
|
||||
|
||||
for _, contentType := range contentTypes {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle different content types without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Content-Type: %s", contentType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
path string
|
||||
method string
|
||||
expectError bool
|
||||
}{
|
||||
{"Normal cleanup call", "/api/db/cleanup", "POST", true}, // Expect error due to no context
|
||||
{"Cleanup with extra path", "/api/db/cleanup/extra", "POST", false}, // Route not matched
|
||||
{"Cleanup normal path", "/api/db/cleanup", "POST", true}, // Valid endpoint
|
||||
{"Case sensitive path", "/api/DB/cleanup", "POST", false}, // Route not matched
|
||||
{"Case sensitive path", "/api/db/CLEANUP", "POST", false}, // Route not matched
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupHeaders(c *C) {
|
||||
// Test with various HTTP headers
|
||||
headerTests := []map[string]string{
|
||||
{},
|
||||
{"Accept": "application/json"},
|
||||
{"Accept": "text/plain"},
|
||||
{"Accept": "*/*"},
|
||||
{"User-Agent": "test-agent"},
|
||||
{"Authorization": "Bearer token123"},
|
||||
{"X-Custom-Header": "custom-value"},
|
||||
{"Accept-Encoding": "gzip, deflate"},
|
||||
{"Accept-Language": "en-US,en;q=0.9"},
|
||||
}
|
||||
|
||||
for i, headers := range headerTests {
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle various headers without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Header test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbCleanupResponseFormat(c *C) {
|
||||
// Test response format consistency
|
||||
req, _ := http.NewRequest("POST", "/api/db/cleanup", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should have proper response structure
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
c.Check(w.Header(), NotNil)
|
||||
|
||||
// If there's a response body, it should be valid
|
||||
if w.Body.Len() > 0 {
|
||||
body := w.Body.String()
|
||||
c.Check(len(body), Not(Equals), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbRequestTypes(c *C) {
|
||||
// Test dbRequestKind constants
|
||||
c.Check(acquiredb, Equals, dbRequestKind(0))
|
||||
c.Check(releasedb, Equals, dbRequestKind(1))
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestDbRequestStruct(c *C) {
|
||||
// Test dbRequest struct creation
|
||||
errCh := make(chan error, 1)
|
||||
req := dbRequest{
|
||||
kind: acquiredb,
|
||||
err: errCh,
|
||||
}
|
||||
|
||||
c.Check(req.kind, Equals, acquiredb)
|
||||
c.Check(req.err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestAcquireAndReleaseDatabase(c *C) {
|
||||
// Initialize db requests channel
|
||||
initDBRequests()
|
||||
|
||||
// Test multiple acquire and release cycles
|
||||
for i := 0; i < 3; i++ {
|
||||
err := acquireDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = releaseDatabaseConnection()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestConcurrentDatabaseAccess(c *C) {
|
||||
// Test concurrent database access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
// Acquire and release database connection
|
||||
if err := acquireDatabaseConnection(); err == nil {
|
||||
// Simulate some work
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
_ = releaseDatabaseConnection()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // Test passed without deadlock
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundWithError(c *C) {
|
||||
// Test task that returns an error
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
testErr := gin.Error{Type: gin.ErrorTypePublic, Err: gin.Error{}.Err}
|
||||
maybeRunTaskInBackground(ginCtx, "error-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, testErr
|
||||
})
|
||||
|
||||
// Should return error status
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundConflict(c *C) {
|
||||
// Test task with resource conflict
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
// Create two tasks with same resources to cause conflict
|
||||
resource := "test-resource-" + time.Now().Format("20060102150405")
|
||||
|
||||
// Start first task
|
||||
_, _ = runTaskInBackground("task1", []string{resource}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
time.Sleep(100 * time.Millisecond) // Hold resource
|
||||
return &task.ProcessReturnValue{Code: 200}, nil
|
||||
})
|
||||
|
||||
// Try to start second task with same resource (should conflict)
|
||||
maybeRunTaskInBackground(ginCtx, "task2", []string{resource}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return &task.ProcessReturnValue{Code: 200}, nil
|
||||
})
|
||||
|
||||
// Should return 409 Conflict
|
||||
c.Check(w.Code, Equals, 409)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestRunTaskInBackgroundWithNilReturn(c *C) {
|
||||
// Test task that returns nil ProcessReturnValue
|
||||
task, err := runTaskInBackground("nil-return-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(task, NotNil)
|
||||
|
||||
// Wait and clean up
|
||||
_, _ = s.context.TaskList().WaitForTaskByID(task.ID)
|
||||
_, _ = s.context.TaskList().DeleteTaskByID(task.ID)
|
||||
}
|
||||
|
||||
func (s *DBTestSuite) TestMaybeRunTaskInBackgroundNilReturn(c *C) {
|
||||
// Test synchronous task with nil return value
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/test", nil)
|
||||
|
||||
maybeRunTaskInBackground(ginCtx, "nil-sync-task", []string{}, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
// Should return 200 with nil body
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package api
|
||||
|
||||
type Error struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ErrorTestSuite struct{}
|
||||
|
||||
var _ = Suite(&ErrorTestSuite{})
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStruct(c *C) {
|
||||
// Test Error struct creation and fields
|
||||
err := Error{Error: "test error message"}
|
||||
c.Check(err.Error, Equals, "test error message")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONMarshaling(c *C) {
|
||||
// Test JSON marshaling of Error struct
|
||||
err := Error{Error: "test error message"}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":"test error message"}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONUnmarshaling(c *C) {
|
||||
// Test JSON unmarshaling into Error struct
|
||||
jsonData := `{"error":"test error message"}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "test error message")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorEmptyMessage(c *C) {
|
||||
// Test Error struct with empty message
|
||||
err := Error{Error: ""}
|
||||
c.Check(err.Error, Equals, "")
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":""}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorSpecialCharacters(c *C) {
|
||||
// Test Error struct with special characters
|
||||
specialMessages := []string{
|
||||
"error with \"quotes\"",
|
||||
"error with 'apostrophes'",
|
||||
"error with \n newlines",
|
||||
"error with \t tabs",
|
||||
"error with unicode: 你好",
|
||||
"error with emoji: 🚨❌",
|
||||
"error with backslashes: \\path\\to\\file",
|
||||
"error with json characters: {\"key\": \"value\"}",
|
||||
"error with < > & characters",
|
||||
"error with null \x00 character",
|
||||
}
|
||||
|
||||
for i, message := range specialMessages {
|
||||
err := Error{Error: message}
|
||||
c.Check(err.Error, Equals, message, Commentf("Test case %d", i))
|
||||
|
||||
// Test JSON marshaling works with special characters
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil, Commentf("Marshal failed for case %d: %s", i, message))
|
||||
|
||||
// Test JSON unmarshaling works with special characters
|
||||
var unmarshaled Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil, Commentf("Unmarshal failed for case %d: %s", i, message))
|
||||
c.Check(unmarshaled.Error, Equals, message, Commentf("Round-trip failed for case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorLongMessage(c *C) {
|
||||
// Test Error struct with very long message
|
||||
longMessage := ""
|
||||
for i := 0; i < 1000; i++ {
|
||||
longMessage += "This is a very long error message. "
|
||||
}
|
||||
|
||||
err := Error{Error: longMessage}
|
||||
c.Check(err.Error, Equals, longMessage)
|
||||
|
||||
// Test JSON marshaling/unmarshaling with long message
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(unmarshaled.Error, Equals, longMessage)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONFieldName(c *C) {
|
||||
// Test that the JSON field name is exactly "error"
|
||||
err := Error{Error: "test"}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
// Parse as generic map to check field name
|
||||
var result map[string]interface{}
|
||||
unmarshalErr := json.Unmarshal(jsonData, &result)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
|
||||
// Check that the field is named "error"
|
||||
value, exists := result["error"]
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(value, Equals, "test")
|
||||
|
||||
// Check that no other fields exist
|
||||
c.Check(len(result), Equals, 1)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONWithExtraFields(c *C) {
|
||||
// Test unmarshaling JSON with extra fields (should be ignored)
|
||||
jsonData := `{"error":"test error","extra":"ignored","number":123}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "test error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONMissingField(c *C) {
|
||||
// Test unmarshaling JSON missing the error field
|
||||
jsonData := `{"other":"value"}`
|
||||
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err.Error, Equals, "") // Should be zero value
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorJSONInvalidJSON(c *C) {
|
||||
// Test unmarshaling invalid JSON
|
||||
invalidJSONs := []string{
|
||||
`{"error":}`,
|
||||
`{"error": invalid}`,
|
||||
`{error: "missing quotes"}`,
|
||||
`{"error": "unterminated`,
|
||||
`malformed json`,
|
||||
``,
|
||||
`null`,
|
||||
`[]`,
|
||||
`123`,
|
||||
}
|
||||
|
||||
for i, jsonData := range invalidJSONs {
|
||||
var err Error
|
||||
unmarshalErr := json.Unmarshal([]byte(jsonData), &err)
|
||||
|
||||
// Should either error or handle gracefully
|
||||
if unmarshalErr == nil {
|
||||
// If no error, check the result is reasonable
|
||||
c.Check(err.Error, FitsTypeOf, "", Commentf("Invalid JSON case %d: %s", i, jsonData))
|
||||
} else {
|
||||
// Error is expected for malformed JSON
|
||||
c.Check(unmarshalErr, NotNil, Commentf("Expected error for case %d: %s", i, jsonData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorZeroValue(c *C) {
|
||||
// Test zero value of Error struct
|
||||
var err Error
|
||||
c.Check(err.Error, Equals, "")
|
||||
|
||||
// Test JSON marshaling of zero value
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":""}`)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorPointer(c *C) {
|
||||
// Test Error struct as pointer
|
||||
err := &Error{Error: "pointer error"}
|
||||
c.Check(err.Error, Equals, "pointer error")
|
||||
|
||||
// Test JSON marshaling of pointer
|
||||
jsonData, marshalErr := json.Marshal(err)
|
||||
c.Check(marshalErr, IsNil)
|
||||
c.Check(string(jsonData), Equals, `{"error":"pointer error"}`)
|
||||
|
||||
// Test JSON unmarshaling into pointer
|
||||
var err2 *Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &err2)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(err2, NotNil)
|
||||
c.Check(err2.Error, Equals, "pointer error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructCopy(c *C) {
|
||||
// Test copying Error struct
|
||||
err1 := Error{Error: "original error"}
|
||||
err2 := err1
|
||||
|
||||
c.Check(err2.Error, Equals, "original error")
|
||||
|
||||
// Modify original and ensure copy is independent
|
||||
err1.Error = "modified error"
|
||||
c.Check(err1.Error, Equals, "modified error")
|
||||
c.Check(err2.Error, Equals, "original error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructComparison(c *C) {
|
||||
// Test comparing Error structs
|
||||
err1 := Error{Error: "same message"}
|
||||
err2 := Error{Error: "same message"}
|
||||
err3 := Error{Error: "different message"}
|
||||
|
||||
c.Check(err1 == err2, Equals, true)
|
||||
c.Check(err1 == err3, Equals, false)
|
||||
c.Check(err2 == err3, Equals, false)
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructInSlice(c *C) {
|
||||
// Test Error struct in slice operations
|
||||
errors := []Error{
|
||||
{Error: "first error"},
|
||||
{Error: "second error"},
|
||||
{Error: "third error"},
|
||||
}
|
||||
|
||||
c.Check(len(errors), Equals, 3)
|
||||
c.Check(errors[0].Error, Equals, "first error")
|
||||
c.Check(errors[1].Error, Equals, "second error")
|
||||
c.Check(errors[2].Error, Equals, "third error")
|
||||
|
||||
// Test JSON marshaling of slice
|
||||
jsonData, marshalErr := json.Marshal(errors)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled []Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(len(unmarshaled), Equals, 3)
|
||||
c.Check(unmarshaled[0].Error, Equals, "first error")
|
||||
}
|
||||
|
||||
func (s *ErrorTestSuite) TestErrorStructInMap(c *C) {
|
||||
// Test Error struct in map operations
|
||||
errorMap := map[string]Error{
|
||||
"key1": {Error: "first error"},
|
||||
"key2": {Error: "second error"},
|
||||
}
|
||||
|
||||
c.Check(len(errorMap), Equals, 2)
|
||||
c.Check(errorMap["key1"].Error, Equals, "first error")
|
||||
c.Check(errorMap["key2"].Error, Equals, "second error")
|
||||
|
||||
// Test JSON marshaling of map
|
||||
jsonData, marshalErr := json.Marshal(errorMap)
|
||||
c.Check(marshalErr, IsNil)
|
||||
|
||||
var unmarshaled map[string]Error
|
||||
unmarshalErr := json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(unmarshalErr, IsNil)
|
||||
c.Check(len(unmarshaled), Equals, 2)
|
||||
c.Check(unmarshaled["key1"].Error, Equals, "first error")
|
||||
c.Check(unmarshaled["key2"].Error, Equals, "second error")
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
func verifyPath(path string) bool {
|
||||
path = filepath.Clean(path)
|
||||
for _, part := range strings.Split(path, string(filepath.Separator)) {
|
||||
if part == ".." || part == "." {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func verifyDir(c *gin.Context) bool {
|
||||
if !verifyPath(c.Params.ByName("dir")) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong dir"))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// @Summary List Directories
|
||||
// @Description **Get list of upload directories**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/files
|
||||
// @Description ["aptly-0.9"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of files"
|
||||
// @Router /api/files [get]
|
||||
func apiFilesListDirs(c *gin.Context) {
|
||||
list := []string{}
|
||||
listLock := &sync.Mutex{}
|
||||
|
||||
err := walker.Walk(context.UploadPath(), func(path string, info os.FileInfo) error {
|
||||
if path == context.UploadPath() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
listLock.Lock()
|
||||
defer listLock.Unlock()
|
||||
list = append(list, filepath.Base(path))
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, list)
|
||||
}
|
||||
|
||||
// @Summary Upload Files
|
||||
// @Description **Upload files to a directory**
|
||||
// @Description
|
||||
// @Description - one or more files can be uploaded
|
||||
// @Description - existing uploaded are overwritten
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X POST -F file=@aptly_0.9~dev+217+ge5d646c_i386.deb http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description ["aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Accept multipart/form-data
|
||||
// @Param dir path string true "Directory to upload files to. Created if does not exist"
|
||||
// @Param files formData file true "Files to upload"
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "list of uploaded files"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [post]
|
||||
func apiFilesUpload(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
|
||||
err := os.MkdirAll(path, 0777)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.Request.ParseMultipartForm(10 * 1024 * 1024)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
stored := []string{}
|
||||
|
||||
for _, files := range c.Request.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
destPath := filepath.Join(path, filepath.Base(file.Filename))
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
|
||||
}
|
||||
}
|
||||
|
||||
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
|
||||
c.JSON(200, stored)
|
||||
}
|
||||
|
||||
// @Summary List Files
|
||||
// @Description **Show uploaded files in upload directory**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description ["aptly_0.9~dev+217+ge5d646c_i386.deb"]
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory to list"
|
||||
// @Success 200 {array} string "Files found in directory"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [get]
|
||||
func apiFilesListFiles(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
list := []string{}
|
||||
listLock := &sync.Mutex{}
|
||||
root := filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir")))
|
||||
|
||||
err := filepath.Walk(root, func(path string, _ os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if path == root {
|
||||
return nil
|
||||
}
|
||||
|
||||
listLock.Lock()
|
||||
defer listLock.Unlock()
|
||||
list = append(list, filepath.Base(path))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
} else {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, list)
|
||||
}
|
||||
|
||||
// @Summary Delete Directory
|
||||
// @Description **Delete upload directory and uploaded files within**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9
|
||||
// @Description {}
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir} [delete]
|
||||
func apiFilesDeleteDir(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.RemoveAll(filepath.Join(context.UploadPath(), utils.SanitizePath(c.Params.ByName("dir"))))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// @Summary Delete File
|
||||
// @Description **Delete a uploaded file in upload directory**
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl -X DELETE http://localhost:8080/api/files/aptly-0.9/aptly_0.9~dev+217+ge5d646c_i386.deb
|
||||
// @Description {}
|
||||
// @Description ```
|
||||
// @Tags Files
|
||||
// @Produce json
|
||||
// @Param dir path string true "Directory to delete from"
|
||||
// @Param name path string true "File to delete"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/files/{dir}/{name} [delete]
|
||||
func apiFilesDeleteFile(c *gin.Context) {
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
dir := utils.SanitizePath(c.Params.ByName("dir"))
|
||||
name := utils.SanitizePath(c.Params.ByName("name"))
|
||||
if !verifyPath(name) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
|
||||
return
|
||||
}
|
||||
|
||||
err := os.Remove(filepath.Join(context.UploadPath(), dir, name))
|
||||
if err != nil {
|
||||
if err1, ok := err.(*os.PathError); !ok || !os.IsNotExist(err1.Err) {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func TestFiles(t *testing.T) { TestingT(t) }
|
||||
|
||||
type FilesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&FilesSuite{})
|
||||
|
||||
func (s *FilesSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TearDownTest(c *C) {
|
||||
// Clean up any test files
|
||||
if s.context != nil {
|
||||
uploadPath := s.context.UploadPath()
|
||||
if uploadPath != "" {
|
||||
os.RemoveAll(uploadPath)
|
||||
}
|
||||
}
|
||||
s.APISuite.TearDownTest(c)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyPath(c *C) {
|
||||
// Valid paths
|
||||
c.Check(verifyPath("valid-dir"), Equals, true)
|
||||
c.Check(verifyPath("valid/sub/dir"), Equals, true)
|
||||
c.Check(verifyPath("valid/../other"), Equals, true) // filepath.Clean normalizes to "other"
|
||||
|
||||
// Invalid paths
|
||||
c.Check(verifyPath(""), Equals, false) // Empty path becomes "."
|
||||
c.Check(verifyPath("../invalid"), Equals, false) // Contains ".."
|
||||
c.Check(verifyPath(".."), Equals, false) // Is ".."
|
||||
c.Check(verifyPath("."), Equals, false) // Is "."
|
||||
c.Check(verifyPath("./"), Equals, false) // Contains "."
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyDirValid(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Params = gin.Params{
|
||||
{Key: "dir", Value: "valid-dir"},
|
||||
}
|
||||
|
||||
result := verifyDir(ctx)
|
||||
c.Check(result, Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestVerifyDirInvalid(c *C) {
|
||||
// Create a test gin context
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Params = gin.Params{
|
||||
{Key: "dir", Value: "../invalid"},
|
||||
}
|
||||
|
||||
result := verifyDir(ctx)
|
||||
c.Check(result, Equals, false)
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesListDirs(c *C) {
|
||||
// Create upload directory for testing
|
||||
uploadPath := s.context.UploadPath()
|
||||
err := os.MkdirAll(filepath.Join(uploadPath, "test-dir"), 0755)
|
||||
c.Assert(err, IsNil)
|
||||
defer os.RemoveAll(uploadPath)
|
||||
|
||||
// Create test file
|
||||
f, err := os.Create(filepath.Join(uploadPath, "test-file.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/files", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
var result []string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(result), Equals, 1)
|
||||
c.Check(result[0], Equals, "test-dir")
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesUpload(c *C) {
|
||||
// Create multipart form data
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "test.txt")
|
||||
c.Assert(err, IsNil)
|
||||
part.Write([]byte("test content"))
|
||||
writer.Close()
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/files/testdir", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify file was uploaded
|
||||
uploadPath := filepath.Join(s.context.UploadPath(), "testdir", "test.txt")
|
||||
_, err = os.Stat(uploadPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(filepath.Join(s.context.UploadPath(), "testdir"))
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesListFiles(c *C) {
|
||||
// Create test directory and files
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Create test files
|
||||
for i := 0; i < 3; i++ {
|
||||
f, err := os.Create(filepath.Join(testDir, fmt.Sprintf("test%d.txt", i)))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/files/testdir", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
var result []string
|
||||
err = json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(result), Equals, 3)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(testDir)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteDir(c *C) {
|
||||
// Create test directory
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Create test file in directory
|
||||
f, err := os.Create(filepath.Join(testDir, "test.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
f.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify directory was deleted
|
||||
_, err = os.Stat(testDir)
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteFile(c *C) {
|
||||
// Create test directory and file
|
||||
testDir := filepath.Join(s.context.UploadPath(), "testdir")
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
testFile := filepath.Join(testDir, "test.txt")
|
||||
f, err := os.Create(testFile)
|
||||
c.Assert(err, IsNil)
|
||||
f.Write([]byte("test content"))
|
||||
f.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir/test.txt", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Check response
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Verify file was deleted
|
||||
_, err = os.Stat(testFile)
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
|
||||
// Clean up
|
||||
os.RemoveAll(testDir)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiFilesDeleteFileInvalidPath(c *C) {
|
||||
// Create test request with invalid path
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/api/files/testdir/../invalid", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should reject with 404 (not found) or 400 (bad request)
|
||||
c.Check(w.Code == 400 || w.Code == 404, Equals, true)
|
||||
}
|
||||
|
||||
// Custom checker for file existence
|
||||
var testFileExists Checker = &fileExistsChecker{
|
||||
CheckerInfo: &CheckerInfo{Name: "testFileExists", Params: []string{"filename"}},
|
||||
}
|
||||
|
||||
type fileExistsChecker struct {
|
||||
*CheckerInfo
|
||||
}
|
||||
|
||||
func (checker *fileExistsChecker) Check(params []interface{}, names []string) (result bool, error string) {
|
||||
filename, ok := params[0].(string)
|
||||
if !ok {
|
||||
return false, "filename must be a string"
|
||||
}
|
||||
|
||||
_, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, ""
|
||||
}
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// Test core API functions
|
||||
func (s *FilesSuite) TestApiVersion(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/version", nil)
|
||||
|
||||
apiVersion(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Version":.*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiHealthy(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/healthy", nil)
|
||||
|
||||
apiHealthy(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is healthy".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWhenReady(c *C) {
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(true)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(isReady)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is ready".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWhenNotReady(c *C) {
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(isReady)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 503)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is unavailable".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestApiReadyWithNil(c *C) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = httptest.NewRequest("GET", "/api/ready", nil)
|
||||
|
||||
apiReady(nil)(ctx)
|
||||
|
||||
c.Check(w.Code, Equals, 503)
|
||||
c.Check(w.Body.String(), Matches, `.*"Status":"Aptly is unavailable".*`)
|
||||
}
|
||||
|
||||
func (s *FilesSuite) TestTruthy(c *C) {
|
||||
// Test string values
|
||||
c.Check(truthy("yes"), Equals, true)
|
||||
c.Check(truthy("true"), Equals, true)
|
||||
c.Check(truthy("1"), Equals, true)
|
||||
c.Check(truthy("on"), Equals, true)
|
||||
c.Check(truthy("anything"), Equals, true)
|
||||
c.Check(truthy("n"), Equals, false)
|
||||
c.Check(truthy("no"), Equals, false)
|
||||
c.Check(truthy("f"), Equals, false)
|
||||
c.Check(truthy("false"), Equals, false)
|
||||
c.Check(truthy("0"), Equals, false)
|
||||
c.Check(truthy("off"), Equals, false)
|
||||
c.Check(truthy("NO"), Equals, false) // case insensitive
|
||||
c.Check(truthy("FALSE"), Equals, false) // case insensitive
|
||||
|
||||
// Test int values
|
||||
c.Check(truthy(1), Equals, true)
|
||||
c.Check(truthy(42), Equals, true)
|
||||
c.Check(truthy(-1), Equals, true)
|
||||
c.Check(truthy(0), Equals, false)
|
||||
|
||||
// Test bool values
|
||||
c.Check(truthy(true), Equals, true)
|
||||
c.Check(truthy(false), Equals, false)
|
||||
|
||||
// Test nil
|
||||
c.Check(truthy(nil), Equals, false)
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type gpgAddKeyParams struct {
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Add ASCII armored gpg public key, do not download from keyserver
|
||||
GpgKeyArmor string `json:"GpgKeyArmor" example:""`
|
||||
|
||||
// Keyserver to download keys provided in `GpgKeyID`
|
||||
Keyserver string `json:"Keyserver" example:"hkp://keyserver.ubuntu.com:80"`
|
||||
// Keys do download from `Keyserver`, separated by space
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500 8B48AD6246925553"`
|
||||
}
|
||||
|
||||
// @Summary Add GPG Keys
|
||||
// @Description **Adds GPG keys to aptly keyring**
|
||||
// @Description
|
||||
// @Description Add GPG public keys for veryfing remote repositories for mirroring.
|
||||
// @Description
|
||||
// @Description Keys can be added in two ways:
|
||||
// @Description * By providing the ASCII armord key in `GpgKeyArmor` (leave Keyserver and GpgKeyID empty)
|
||||
// @Description * By providing a `Keyserver` and one or more key IDs in `GpgKeyID`, separated by space (leave GpgKeyArmor empty)
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgAddKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [post]
|
||||
func apiGPGAddKey(c *gin.Context) {
|
||||
b := gpgAddKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
b.Keyserver = utils.SanitizePath(b.Keyserver)
|
||||
b.GpgKeyID = utils.SanitizePath(b.GpgKeyID)
|
||||
b.GpgKeyArmor = utils.SanitizePath(b.GpgKeyArmor)
|
||||
// b.Keyring can be an absolute path
|
||||
|
||||
var err error
|
||||
args := []string{"--no-default-keyring", "--allow-non-selfsigned-uid"}
|
||||
keyring := "trustedkeys.gpg"
|
||||
if len(b.Keyring) > 0 {
|
||||
keyring = b.Keyring
|
||||
}
|
||||
args = append(args, "--keyring", keyring)
|
||||
if len(b.Keyserver) > 0 {
|
||||
args = append(args, "--keyserver", b.Keyserver)
|
||||
}
|
||||
if len(b.GpgKeyArmor) > 0 {
|
||||
var tempdir string
|
||||
tempdir, err = os.MkdirTemp(os.TempDir(), "aptly")
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(tempdir) }()
|
||||
|
||||
keypath := filepath.Join(tempdir, "key")
|
||||
keyfile, e := os.Create(keypath)
|
||||
if e != nil {
|
||||
AbortWithJSONError(c, 400, e)
|
||||
return
|
||||
}
|
||||
if _, e = keyfile.WriteString(b.GpgKeyArmor); e != nil {
|
||||
AbortWithJSONError(c, 400, e)
|
||||
}
|
||||
args = append(args, "--import", keypath)
|
||||
|
||||
}
|
||||
if len(b.GpgKeyID) > 0 {
|
||||
keys := strings.Fields(b.GpgKeyID)
|
||||
args = append(args, "--recv-keys")
|
||||
args = append(args, keys...)
|
||||
}
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
// it might happened that we have a situation with an erroneous
|
||||
// gpg command (e.g. when GpgKeyID and GpgKeyArmor is set).
|
||||
// there is no error handling for such as gpg will do this for us
|
||||
cmd := exec.Command(gpg, args...)
|
||||
fmt.Printf("running %s %s\n", gpg, strings.Join(args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
c.JSON(400, string(out))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GPGTestSuite struct {
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
var _ = Suite(&GPGTestSuite{})
|
||||
|
||||
func (s *GPGTestSuite) SetUpTest(c *C) {
|
||||
s.router = gin.New()
|
||||
s.router.POST("/api/gpg/key", apiGPGAddKey)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyStructure(c *C) {
|
||||
// Test GPG key add endpoint structure with sample key data
|
||||
keyData := `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQINBFKuaIQBEAC+JC5od6Vw1tz2SEfBE7tBLQhNy3z2SIu7iNC3Bi/W6xUy5YKw
|
||||
sample key data for testing
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or invalid key, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyEmptyBody(c *C) {
|
||||
// Test GPG key add with empty body
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(""))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle empty body gracefully
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyInvalidData(c *C) {
|
||||
// Test GPG key add with invalid key data
|
||||
invalidKeys := []string{
|
||||
"not a pgp key",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\ninvalid\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"random text data",
|
||||
"<xml>not a key</xml>",
|
||||
"-----BEGIN CERTIFICATE-----\ninvalid cert\n-----END CERTIFICATE-----",
|
||||
}
|
||||
|
||||
for _, keyData := range invalidKeys {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle invalid key data gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Key data: %s", keyData[:min(len(keyData), 50)]))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyHTTPMethods(c *C) {
|
||||
// Test that only POST method is allowed
|
||||
deniedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/gpg/key", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyContentTypes(c *C) {
|
||||
// Test different content types
|
||||
contentTypes := []string{
|
||||
"application/pgp-keys",
|
||||
"text/plain",
|
||||
"application/x-pgp-message",
|
||||
"application/octet-stream",
|
||||
"",
|
||||
}
|
||||
|
||||
keyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\nsample\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
for _, contentType := range contentTypes {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle different content types without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Content-Type: %s", contentType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyLargePayload(c *C) {
|
||||
// Test with large payload (simulate large key file)
|
||||
largeKeyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
for i := 0; i < 1000; i++ {
|
||||
largeKeyData += "large key data line " + string(rune(i)) + "\n"
|
||||
}
|
||||
largeKeyData += "-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(largeKeyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle large payloads without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyBinaryData(c *C) {
|
||||
// Test with binary data
|
||||
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD}
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBuffer(binaryData))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle binary data without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeySpecialCharacters(c *C) {
|
||||
// Test with special characters and encoding
|
||||
specialKeys := []string{
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\nключ с русскими символами\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n中文字符测试\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n🔑 emoji key 🔐\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\"quotes\" and 'apostrophes'\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n<>&\"'`\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
}
|
||||
|
||||
for i, keyData := range specialKeys {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys; charset=utf-8")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle special characters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special key test #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
data string
|
||||
contentType string
|
||||
expectError bool
|
||||
}{
|
||||
{"Empty key", "", "application/pgp-keys", true},
|
||||
{"Malformed header", "-----BEGIN WRONG BLOCK-----\ndata\n-----END WRONG BLOCK-----", "application/pgp-keys", true},
|
||||
{"Missing end", "-----BEGIN PGP PUBLIC KEY BLOCK-----\ndata", "application/pgp-keys", true},
|
||||
{"Missing begin", "data\n-----END PGP PUBLIC KEY BLOCK-----", "application/pgp-keys", true},
|
||||
{"Only whitespace", " \n\t\r\n ", "application/pgp-keys", true},
|
||||
{"JSON data", `{"key": "value"}`, "application/json", true},
|
||||
{"XML data", `<key>value</key>`, "application/xml", true},
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(test.data))
|
||||
req.Header.Set("Content-Type", test.contentType)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle errors gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GPGTestSuite) TestGPGAddKeyReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
keyData := "-----BEGIN PGP PUBLIC KEY BLOCK-----\ntest key data\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("POST", "/api/gpg/key", bytes.NewBufferString(keyData))
|
||||
req.Header.Set("Content-Type", "application/pgp-keys")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary Graph Output
|
||||
// @Description **Generate dependency graph**
|
||||
// @Description
|
||||
// @Description Command graph generates graph of dependencies:
|
||||
// @Description
|
||||
// @Description * between snapshots and mirrors (what mirror was used to create each snapshot)
|
||||
// @Description * between snapshots and local repos (what local repo was used to create snapshot)
|
||||
// @Description * between snapshots (pulling, merging, etc.)
|
||||
// @Description * between snapshots, local repos and published repositories (how snapshots were published).
|
||||
// @Description
|
||||
// @Description Graph is rendered to PNG file using graphviz package.
|
||||
// @Description
|
||||
// @Description Example URL: `http://localhost:8080/api/graph.svg?layout=vertical`
|
||||
// @Tags Status
|
||||
// @Produce image/png, image/svg+xml
|
||||
// @Param ext path string true "ext specifies desired file extension, e.g. .png, .svg."
|
||||
// @Param layout query string false "Change between a `horizontal` (default) and a `vertical` graph layout."
|
||||
// @Success 200 {object} []byte "Output"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/graph.{ext} [get]
|
||||
func apiGraph(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
output []byte
|
||||
)
|
||||
|
||||
ext := c.Params.ByName("ext")
|
||||
layout := c.Request.URL.Query().Get("layout")
|
||||
factory := context.NewCollectionFactory()
|
||||
|
||||
graph, err := deb.BuildGraph(factory, layout)
|
||||
if err != nil {
|
||||
c.JSON(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString(graph.String())
|
||||
|
||||
if ext == "dot" || ext == "gv" {
|
||||
// If the raw dot data is requested, return it as string.
|
||||
// This allows client-side rendering rather than server-side.
|
||||
c.String(200, buf.String())
|
||||
return
|
||||
}
|
||||
|
||||
command := exec.Command("dot", "-T"+ext)
|
||||
command.Stderr = os.Stderr
|
||||
|
||||
stdin, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(stdin, buf)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = stdin.Close()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err = command.Output()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to execute dot: %s (is graphviz package installed?)", err))
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := mime.TypeByExtension("." + ext)
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
c.Data(200, mimeType, output)
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GraphTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&GraphTestSuite{})
|
||||
|
||||
func (s *GraphTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphDotFormat(c *C) {
|
||||
// Test requesting raw DOT format
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with context and return DOT format
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "text/plain; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphGvFormat(c *C) {
|
||||
// Test requesting GV format (alias for DOT)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.gv", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed with context and return DOT format (gv is alias)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "text/plain; charset=utf-8")
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphSvgFormat(c *C) {
|
||||
// Test requesting SVG format (requires graphviz)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or missing graphviz
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphPngFormat(c *C) {
|
||||
// Test requesting PNG format (requires graphviz)
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context or missing graphviz
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithHorizontalLayout(c *C) {
|
||||
// Test with horizontal layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?layout=horizontal", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context, but should parse layout parameter
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error due to missing context
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithVerticalLayout(c *C) {
|
||||
// Test with vertical layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png?layout=vertical", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will likely error due to no context, but should parse layout parameter
|
||||
c.Check(w.Code, Not(Equals), 200) // Expect error due to missing context
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithInvalidLayout(c *C) {
|
||||
// Test with invalid layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot?layout=invalid", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed - invalid layout is ignored
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithEmptyLayout(c *C) {
|
||||
// Test with empty layout parameter
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?layout=", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail because SVG requires graphviz which is not installed
|
||||
c.Check(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphWithMultipleParams(c *C) {
|
||||
// Test with multiple query parameters
|
||||
req, _ := http.NewRequest("GET", "/api/graph.png?layout=vertical&extra=param&another=value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail because PNG requires graphviz which is not installed
|
||||
c.Check(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphParameterHandling(c *C) {
|
||||
// Test parameter extraction and validation
|
||||
testCases := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/graph.dot", "DOT format"},
|
||||
{"/api/graph.gv", "GV format"},
|
||||
{"/api/graph.svg", "SVG format"},
|
||||
{"/api/graph.png", "PNG format"},
|
||||
{"/api/graph.pdf", "PDF format"},
|
||||
{"/api/graph.ps", "PostScript format"},
|
||||
{"/api/graph.jpg", "JPEG format"},
|
||||
{"/api/graph.gif", "GIF format"},
|
||||
{"/api/graph.unknown", "Unknown format"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
req, _ := http.NewRequest("GET", tc.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test case: %s", tc.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphMimeTypeHandling(c *C) {
|
||||
// Test MIME type detection for different extensions
|
||||
extensions := map[string]string{
|
||||
"svg": "image/svg+xml",
|
||||
"png": "image/png",
|
||||
"pdf": "application/pdf",
|
||||
"ps": "application/postscript",
|
||||
"jpg": "image/jpeg",
|
||||
"gif": "image/gif",
|
||||
}
|
||||
|
||||
for ext, expectedMime := range extensions {
|
||||
actualMime := mime.TypeByExtension("." + ext)
|
||||
if actualMime != "" {
|
||||
// Just check that the actual MIME type starts with expected
|
||||
c.Check(strings.HasPrefix(actualMime, expectedMime), Equals, true,
|
||||
Commentf("MIME type mismatch for extension: %s", ext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphHTTPMethods(c *C) {
|
||||
// Test that only GET method is allowed
|
||||
deniedMethods := []string{"POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphPathValidation(c *C) {
|
||||
// Test path validation and parameter extraction
|
||||
validPaths := []string{
|
||||
"/api/graph.dot",
|
||||
"/api/graph.svg",
|
||||
"/api/graph.png",
|
||||
"/api/graph.pdf",
|
||||
}
|
||||
|
||||
invalidPaths := []string{
|
||||
"/api/graph", // Missing extension
|
||||
"/api/graph.", // Empty extension
|
||||
"/api/graphs.svg", // Wrong endpoint name
|
||||
}
|
||||
|
||||
for _, path := range validPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should match route (even if it errors due to missing context)
|
||||
c.Check(w.Code, Not(Equals), 404, Commentf("Valid path should match route: %s", path))
|
||||
}
|
||||
|
||||
for _, path := range invalidPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not match route
|
||||
c.Check(w.Code, Equals, 404, Commentf("Invalid path should not match route: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphExtensionExtraction(c *C) {
|
||||
// Test that extension is properly extracted from path
|
||||
testPaths := []string{
|
||||
"/api/graph.dot",
|
||||
"/api/graph.svg",
|
||||
"/api/graph.png",
|
||||
"/api/graph.pdf",
|
||||
"/api/graph.ps",
|
||||
"/api/graph.jpg",
|
||||
"/api/graph.gif",
|
||||
"/api/graph.unknown",
|
||||
}
|
||||
|
||||
for _, path := range testPaths {
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle extension extraction without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Extension extraction failed for: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphQueryParameterHandling(c *C) {
|
||||
// Test various query parameter combinations
|
||||
queryTests := []struct {
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{"", "no parameters"},
|
||||
{"layout=horizontal", "horizontal layout"},
|
||||
{"layout=vertical", "vertical layout"},
|
||||
{"layout=invalid", "invalid layout"},
|
||||
{"layout=", "empty layout"},
|
||||
{"layout=horizontal&extra=param", "multiple parameters"},
|
||||
{"unknown=param", "unknown parameter"},
|
||||
{"layout=horizontal&layout=vertical", "duplicate parameters"},
|
||||
}
|
||||
|
||||
for _, test := range queryTests {
|
||||
path := "/api/graph.svg"
|
||||
if test.query != "" {
|
||||
path += "?" + test.query
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle query parameters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Query parameter test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphErrorHandling(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/graph.svg", "missing database context"},
|
||||
{"/api/graph.png", "missing graphviz"},
|
||||
{"/api/graph.unknown", "unknown format"},
|
||||
{"/api/graph.dot", "raw DOT format"},
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest("GET", test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle errors gracefully without panicking
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Error test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphContentTypeHeaders(c *C) {
|
||||
// Test that appropriate content types are set for different formats
|
||||
formatTests := []struct {
|
||||
ext string
|
||||
expectJSON bool
|
||||
expectImage bool
|
||||
}{
|
||||
{"dot", false, false}, // Should return text
|
||||
{"gv", false, false}, // Should return text
|
||||
{"svg", false, true}, // Should return image/svg+xml (if successful)
|
||||
{"png", false, true}, // Should return image/png (if successful)
|
||||
{"pdf", false, false}, // Should return application/pdf (if successful)
|
||||
}
|
||||
|
||||
for _, test := range formatTests {
|
||||
req, _ := http.NewRequest("GET", "/api/graph."+test.ext, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
|
||||
if test.expectJSON {
|
||||
c.Check(strings.Contains(contentType, "application/json"), Equals, true,
|
||||
Commentf("Expected JSON content type for .%s, got: %s", test.ext, contentType))
|
||||
}
|
||||
|
||||
// Note: Image content types will only be set if graphviz is available and context exists
|
||||
c.Check(contentType, Not(Equals), "", Commentf("Content type should be set for .%s", test.ext))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphSpecialCharacters(c *C) {
|
||||
// Test handling of special characters in query parameters
|
||||
specialQueries := []string{
|
||||
"layout=horizontal%20with%20spaces",
|
||||
"layout=vertical¶m=value%20with%20spaces",
|
||||
"layout=test%26special%3Dchars",
|
||||
"layout=unicode%E2%9C%93",
|
||||
"param=%3Cscript%3Ealert%28%29%3C%2Fscript%3E", // XSS attempt
|
||||
}
|
||||
|
||||
for _, query := range specialQueries {
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg?"+query, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle special characters without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special character test failed for: %s", query))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphLargeExtensions(c *C) {
|
||||
// Test with very long extensions
|
||||
longExt := strings.Repeat("x", 1000)
|
||||
req, _ := http.NewRequest("GET", "/api/graph."+longExt, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle long extensions without crashing
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphReliability(c *C) {
|
||||
// Test multiple sequential calls for reliability
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("GET", "/api/graph.dot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should be consistent across multiple calls
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Call #%d", i+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GraphTestSuite) TestGraphConcurrency(c *C) {
|
||||
// Test concurrent requests to ensure thread safety
|
||||
done := make(chan bool, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/graph.svg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle concurrent requests without issues
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all requests to complete
|
||||
for i := 0; i < 5; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
c.Check(true, Equals, true) // Test completed without deadlocks
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
apiRequestsInFlightGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_api_http_requests_in_flight",
|
||||
Help: "Number of concurrent HTTP api requests currently handled.",
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
apiRequestsTotalCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "aptly_api_http_requests_total",
|
||||
Help: "Total number of api requests.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiRequestSizeSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_request_size_bytes",
|
||||
Help: "Api HTTP request size in bytes.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiResponseSizeSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_response_size_bytes",
|
||||
Help: "Api HTTP response size in bytes.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiRequestsDurationSummary = promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "aptly_api_http_request_duration_seconds",
|
||||
Help: "Duration of api requests in seconds.",
|
||||
},
|
||||
[]string{"code", "method", "path"},
|
||||
)
|
||||
apiVersionGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_build_info",
|
||||
Help: "Metric with a constant '1' value labeled by version and goversion from which aptly was built.",
|
||||
},
|
||||
[]string{"version", "goversion"},
|
||||
)
|
||||
apiFilesUploadedCounter = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "aptly_api_files_uploaded_total",
|
||||
Help: "Total number of uploaded files labeled by upload directory.",
|
||||
},
|
||||
[]string{"directory"},
|
||||
)
|
||||
apiReposPackageCountGauge = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "aptly_repos_package_count",
|
||||
Help: "Current number of published packages labeled by source, distribution and component.",
|
||||
},
|
||||
[]string{"source", "distribution", "component"},
|
||||
)
|
||||
)
|
||||
|
||||
type metricsCollectorRegistrar struct {
|
||||
hasRegistered bool
|
||||
}
|
||||
|
||||
func (r *metricsCollectorRegistrar) Register(router *gin.Engine) {
|
||||
if !r.hasRegistered {
|
||||
apiVersionGauge.WithLabelValues(aptly.Version, runtime.Version()).Set(1)
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
r.hasRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
var MetricsCollectorRegistrar = metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
func countPackagesByRepos() {
|
||||
err := context.NewCollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
err := context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf(
|
||||
"Error %s found while determining package count for metrics endpoint (prefix:%s / distribution:%s / component:%s\n).",
|
||||
err, repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
log.Warn().Msg(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
components := repo.Components()
|
||||
for _, c := range components {
|
||||
count := float64(len(repo.RefList(c).Refs))
|
||||
apiReposPackageCountGauge.WithLabelValues(fmt.Sprintf("%s", (repo.SourceNames())), repo.Distribution, c).Set(count)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error %s found while listing published repos for metrics endpoint", err)
|
||||
log.Warn().Msg(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MetricsTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MetricsTestSuite{})
|
||||
|
||||
func (s *MetricsTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
// Reset metrics registrar state for each test
|
||||
MetricsCollectorRegistrar.hasRegistered = false
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsCollectorRegistrarRegisterOnce(c *C) {
|
||||
// Test that metrics are only registered once
|
||||
registrar := &metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
// First registration should work
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
c.Check(registrar.hasRegistered, Equals, true)
|
||||
|
||||
// Second registration should be skipped
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
c.Check(registrar.hasRegistered, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsCollectorRegistrarVersionGauge(c *C) {
|
||||
// Test that version gauge is set correctly
|
||||
registrar := &metricsCollectorRegistrar{hasRegistered: false}
|
||||
|
||||
// Register metrics
|
||||
registrar.Register(s.router.(*gin.Engine))
|
||||
|
||||
// Check that version gauge was set
|
||||
expectedLabels := prometheus.Labels{
|
||||
"version": aptly.Version,
|
||||
"goversion": runtime.Version(),
|
||||
}
|
||||
|
||||
gauge := apiVersionGauge.With(expectedLabels)
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Verify the gauge value is 1
|
||||
metric := &dto.Metric{}
|
||||
gauge.(prometheus.Gauge).Write(metric)
|
||||
c.Check(metric.GetGauge().GetValue(), Equals, float64(1))
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsInFlightGauge(c *C) {
|
||||
// Test that in-flight requests gauge works
|
||||
c.Check(apiRequestsInFlightGauge, NotNil)
|
||||
|
||||
// Test that we can create labels for the gauge
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/test")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test incrementing and decrementing
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsTotalCounter(c *C) {
|
||||
// Test that total requests counter works
|
||||
c.Check(apiRequestsTotalCounter, NotNil)
|
||||
|
||||
// Test that we can create labels for the counter
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test incrementing
|
||||
counter.Inc()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestSizeSummary(c *C) {
|
||||
// Test that request size summary works
|
||||
c.Check(apiRequestSizeSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing values
|
||||
summary.Observe(1024.0)
|
||||
summary.Observe(512.0)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiResponseSizeSummary(c *C) {
|
||||
// Test that response size summary works
|
||||
c.Check(apiResponseSizeSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiResponseSizeSummary.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing values
|
||||
summary.Observe(2048.0)
|
||||
summary.Observe(1024.0)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiRequestsDurationSummary(c *C) {
|
||||
// Test that request duration summary works
|
||||
c.Check(apiRequestsDurationSummary, NotNil)
|
||||
|
||||
// Test that we can create labels for the summary
|
||||
summary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(summary, NotNil)
|
||||
|
||||
// Test observing duration values
|
||||
summary.Observe(0.1) // 100ms
|
||||
summary.Observe(0.05) // 50ms
|
||||
summary.Observe(1.0) // 1s
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiFilesUploadedCounter(c *C) {
|
||||
// Test that files uploaded counter works
|
||||
c.Check(apiFilesUploadedCounter, NotNil)
|
||||
|
||||
// Test that we can create labels for the counter
|
||||
counter := apiFilesUploadedCounter.WithLabelValues("uploads")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test incrementing
|
||||
counter.Inc()
|
||||
counter.Add(5)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestApiReposPackageCountGauge(c *C) {
|
||||
// Test that repos package count gauge works
|
||||
c.Check(apiReposPackageCountGauge, NotNil)
|
||||
|
||||
// Test that we can create labels for the gauge
|
||||
gauge := apiReposPackageCountGauge.WithLabelValues("source", "stable", "main")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test setting values
|
||||
gauge.Set(100)
|
||||
gauge.Set(150)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsPrometheusIntegration(c *C) {
|
||||
// Test integration with Prometheus client library
|
||||
|
||||
// Test that metrics are properly registered with default registry
|
||||
metricNames := []string{
|
||||
"aptly_api_http_requests_in_flight",
|
||||
"aptly_api_http_requests_total",
|
||||
"aptly_api_http_request_size_bytes",
|
||||
"aptly_api_http_response_size_bytes",
|
||||
"aptly_api_http_request_duration_seconds",
|
||||
"aptly_build_info",
|
||||
"aptly_api_files_uploaded_total",
|
||||
"aptly_repos_package_count",
|
||||
}
|
||||
|
||||
for _, metricName := range metricNames {
|
||||
// Try to gather metrics to ensure they're registered
|
||||
gathered, err := prometheus.DefaultGatherer.Gather()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, metricFamily := range gathered {
|
||||
if metricFamily.GetName() == metricName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Check(found, Equals, true, Commentf("Metric %s not found", metricName))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsLabels(c *C) {
|
||||
// Test that metrics have expected labels
|
||||
|
||||
// Test in-flight gauge labels
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/test")
|
||||
c.Check(gauge, NotNil)
|
||||
|
||||
// Test total counter labels
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/test")
|
||||
c.Check(counter, NotNil)
|
||||
|
||||
// Test request size summary labels
|
||||
requestSummary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/upload")
|
||||
c.Check(requestSummary, NotNil)
|
||||
|
||||
// Test response size summary labels
|
||||
responseSummary := apiResponseSizeSummary.WithLabelValues("404", "GET", "/api/missing")
|
||||
c.Check(responseSummary, NotNil)
|
||||
|
||||
// Test duration summary labels
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues("500", "POST", "/api/error")
|
||||
c.Check(durationSummary, NotNil)
|
||||
|
||||
// Test version gauge labels
|
||||
versionGauge := apiVersionGauge.WithLabelValues("1.0.0", "go1.19")
|
||||
c.Check(versionGauge, NotNil)
|
||||
|
||||
// Test files uploaded counter labels
|
||||
filesCounter := apiFilesUploadedCounter.WithLabelValues("temp-uploads")
|
||||
c.Check(filesCounter, NotNil)
|
||||
|
||||
// Test repos package count gauge labels
|
||||
reposGauge := apiReposPackageCountGauge.WithLabelValues("snapshot:test", "testing", "contrib")
|
||||
c.Check(reposGauge, NotNil)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentHTTPCodes(c *C) {
|
||||
// Test metrics with various HTTP status codes
|
||||
httpCodes := []string{"200", "201", "400", "401", "403", "404", "409", "500", "502", "503"}
|
||||
|
||||
for _, code := range httpCodes {
|
||||
// Test that metrics work with different status codes
|
||||
counter := apiRequestsTotalCounter.WithLabelValues(code, "GET", "/api/test")
|
||||
counter.Inc()
|
||||
|
||||
requestSummary := apiRequestSizeSummary.WithLabelValues(code, "POST", "/api/test")
|
||||
requestSummary.Observe(100)
|
||||
|
||||
responseSummary := apiResponseSizeSummary.WithLabelValues(code, "GET", "/api/test")
|
||||
responseSummary.Observe(200)
|
||||
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues(code, "PUT", "/api/test")
|
||||
durationSummary.Observe(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentHTTPMethods(c *C) {
|
||||
// Test metrics with various HTTP methods
|
||||
httpMethods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range httpMethods {
|
||||
// Test that metrics work with different HTTP methods
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues(method, "/api/test")
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", method, "/api/test")
|
||||
counter.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithDifferentPaths(c *C) {
|
||||
// Test metrics with various API paths
|
||||
apiPaths := []string{
|
||||
"/api/repos",
|
||||
"/api/repos/test",
|
||||
"/api/snapshots",
|
||||
"/api/publish",
|
||||
"/api/files",
|
||||
"/api/files/upload",
|
||||
"/api/mirrors",
|
||||
"/api/tasks",
|
||||
"/api/version",
|
||||
}
|
||||
|
||||
for _, path := range apiPaths {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", path)
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", path)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsThreadSafety(c *C) {
|
||||
// Test that metrics are thread-safe by simulating concurrent access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
// Simulate concurrent metric updates
|
||||
for j := 0; j < 100; j++ {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", "/api/concurrent")
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", "/api/concurrent")
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
summary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/concurrent")
|
||||
summary.Observe(0.01)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify metrics were updated (exact count doesn't matter due to concurrency)
|
||||
c.Check(true, Equals, true) // Test completed without race conditions
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsMetadata(c *C) {
|
||||
// Test that metrics have proper metadata (help text, names)
|
||||
|
||||
// Gather all metrics
|
||||
gathered, err := prometheus.DefaultGatherer.Gather()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
expectedMetrics := map[string]string{
|
||||
"aptly_api_http_requests_in_flight": "Number of concurrent HTTP api requests currently handled.",
|
||||
"aptly_api_http_requests_total": "Total number of api requests.",
|
||||
"aptly_api_http_request_size_bytes": "Api HTTP request size in bytes.",
|
||||
"aptly_api_http_response_size_bytes": "Api HTTP response size in bytes.",
|
||||
"aptly_api_http_request_duration_seconds": "Duration of api requests in seconds.",
|
||||
"aptly_build_info": "Metric with a constant '1' value labeled by version and goversion from which aptly was built.",
|
||||
"aptly_api_files_uploaded_total": "Total number of uploaded files labeled by upload directory.",
|
||||
"aptly_repos_package_count": "Current number of published packages labeled by source, distribution and component.",
|
||||
}
|
||||
|
||||
for _, metricFamily := range gathered {
|
||||
metricName := metricFamily.GetName()
|
||||
if expectedHelp, exists := expectedMetrics[metricName]; exists {
|
||||
c.Check(metricFamily.GetHelp(), Equals, expectedHelp,
|
||||
Commentf("Help text mismatch for metric: %s", metricName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestCountPackagesByRepos(c *C) {
|
||||
// Test countPackagesByRepos function structure
|
||||
// Note: This function requires database context which we don't have in tests,
|
||||
// but we can test that it doesn't crash when called
|
||||
|
||||
// This will likely error due to no context, but should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.Fatalf("countPackagesByRepos panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
countPackagesByRepos()
|
||||
|
||||
// If we get here, the function didn't panic
|
||||
c.Check(true, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestGetBasePath(c *C) {
|
||||
// Test getBasePath function
|
||||
w := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(w)
|
||||
|
||||
// Test with simple path (only returns first two segments)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/version", nil)
|
||||
basePath := getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/version")
|
||||
|
||||
// Test with path containing more segments (still returns first two)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/repos/test-repo", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/repos")
|
||||
|
||||
// Test with nested parameters (still returns first two)
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api/repos/repo1/packages", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api/repos")
|
||||
|
||||
// Test with root path
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/")
|
||||
|
||||
// Test with single segment
|
||||
ginCtx.Request = httptest.NewRequest("GET", "/api", nil)
|
||||
basePath = getBasePath(ginCtx)
|
||||
c.Check(basePath, Equals, "/api")
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestGetURLSegment(c *C) {
|
||||
// Test getURLSegment function
|
||||
|
||||
// Test valid segments
|
||||
segment, err := getURLSegment("/api/repos/test", 0)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
segment, err = getURLSegment("/api/repos/test", 1)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/repos")
|
||||
|
||||
segment, err = getURLSegment("/api/repos/test", 2)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(*segment, Equals, "/test")
|
||||
|
||||
// Test out of range
|
||||
_, err = getURLSegment("/api/repos", 3)
|
||||
c.Check(err, NotNil)
|
||||
|
||||
// Test root path
|
||||
segment, err = getURLSegment("/", 0)
|
||||
c.Check(err, NotNil) // No segments after removing empty string
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerInFlight(c *C) {
|
||||
// Test instrumentHandlerInFlight middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerCounter(c *C) {
|
||||
// Test instrumentHandlerCounter middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerRequestSize(c *C) {
|
||||
// Test instrumentHandlerRequestSize middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.POST("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request with body
|
||||
req := httptest.NewRequest("POST", "/api/test", strings.NewReader("test body"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerResponseSize(c *C) {
|
||||
// Test instrumentHandlerResponseSize middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"data": strings.Repeat("x", 1000)})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestInstrumentHandlerDuration(c *C) {
|
||||
// Test instrumentHandlerDuration middleware
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create test gin context
|
||||
router := gin.New()
|
||||
|
||||
// Add instrumentation middleware
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsRegistration(c *C) {
|
||||
// Test that metrics registration works correctly with gin router
|
||||
MetricsCollectorRegistrar.Register(s.router.(*gin.Engine))
|
||||
|
||||
// Create a test request to trigger middleware
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Add a test handler
|
||||
s.router.(*gin.Engine).GET("/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"test": "response"})
|
||||
})
|
||||
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(MetricsCollectorRegistrar.hasRegistered, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsErrorConditions(c *C) {
|
||||
// Test error handling in metrics collection
|
||||
|
||||
// Test with invalid label values (should not crash)
|
||||
invalidLabels := []string{"", "very_long_label_" + strings.Repeat("x", 1000), "label\nwith\nnewlines"}
|
||||
|
||||
for _, label := range invalidLabels {
|
||||
// These should not crash, even with invalid labels
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", label)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", label)
|
||||
counter.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsValueRanges(c *C) {
|
||||
// Test metrics with various value ranges
|
||||
|
||||
// Test large values
|
||||
summary := apiRequestSizeSummary.WithLabelValues("200", "POST", "/api/large")
|
||||
summary.Observe(1e9) // 1GB
|
||||
summary.Observe(1e12) // 1TB
|
||||
|
||||
// Test very small values
|
||||
durationSummary := apiRequestsDurationSummary.WithLabelValues("200", "GET", "/api/fast")
|
||||
durationSummary.Observe(1e-9) // 1 nanosecond
|
||||
durationSummary.Observe(1e-6) // 1 microsecond
|
||||
|
||||
// Test zero values
|
||||
gauge := apiReposPackageCountGauge.WithLabelValues("empty", "dist", "comp")
|
||||
gauge.Set(0)
|
||||
|
||||
// Test negative values (should be handled gracefully)
|
||||
gauge.Set(-1) // May or may not be allowed by Prometheus, but shouldn't crash
|
||||
}
|
||||
|
||||
func (s *MetricsTestSuite) TestMetricsWithSpecialCharacters(c *C) {
|
||||
// Test metrics with special characters in labels
|
||||
specialPaths := []string{
|
||||
"/api/repos/repo-with-dashes",
|
||||
"/api/repos/repo_with_underscores",
|
||||
"/api/repos/repo.with.dots",
|
||||
"/api/repos/repo+with+plus",
|
||||
"/api/repos/repo%20with%20encoded",
|
||||
}
|
||||
|
||||
for _, path := range specialPaths {
|
||||
counter := apiRequestsTotalCounter.WithLabelValues("200", "GET", path)
|
||||
counter.Inc()
|
||||
|
||||
gauge := apiRequestsInFlightGauge.WithLabelValues("GET", path)
|
||||
gauge.Inc()
|
||||
gauge.Dec()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Only use base path as label value (e.g.: /api/repos) because of time series cardinality
|
||||
// See https://prometheus.io/docs/practices/naming/#labels
|
||||
func getBasePath(c *gin.Context) string {
|
||||
segment0, err := getURLSegment(c.Request.URL.Path, 0)
|
||||
if err != nil {
|
||||
return "/"
|
||||
}
|
||||
segment1, err := getURLSegment(c.Request.URL.Path, 1)
|
||||
if err != nil {
|
||||
return *segment0
|
||||
}
|
||||
|
||||
return *segment0 + *segment1
|
||||
}
|
||||
|
||||
func getURLSegment(url string, idx int) (*string, error) {
|
||||
urlSegments := strings.Split(url, "/")
|
||||
// Remove segment at index 0 because it's an empty string
|
||||
urlSegments = urlSegments[1:cap(urlSegments)]
|
||||
|
||||
if len(urlSegments) <= idx {
|
||||
return nil, fmt.Errorf("index %d out of range, only has %d url segments", idx, len(urlSegments))
|
||||
}
|
||||
|
||||
segmentAtIndex := urlSegments[idx]
|
||||
s := fmt.Sprintf("/%s", segmentAtIndex)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func instrumentHandlerInFlight(g *prometheus.GaugeVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
g.WithLabelValues(c.Request.Method, pathFunc(c)).Inc()
|
||||
defer g.WithLabelValues(c.Request.Method, pathFunc(c)).Dec()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerCounter(counter *prometheus.CounterVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
counter.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerRequestSize(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(float64(c.Request.ContentLength))
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerResponseSize(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
var responseSize = math.Max(float64(c.Writer.Size()), 0)
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(responseSize)
|
||||
}
|
||||
}
|
||||
|
||||
func instrumentHandlerDuration(obs prometheus.ObserverVec, pathFunc func(*gin.Context) string) func(*gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
now := time.Now()
|
||||
c.Next()
|
||||
obs.WithLabelValues(strconv.Itoa(c.Writer.Status()), c.Request.Method, pathFunc(c)).Observe(time.Since(now).Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
// JSONLogger is a gin middleware that takes an instance of Logger and uses it for writing access
|
||||
// logs that include error messages if there are any.
|
||||
func JSONLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
ts := time.Now()
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
errorMessage := strings.TrimSuffix(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
|
||||
l := log.With().Str("remote", c.ClientIP()).Logger().
|
||||
With().Str("method", c.Request.Method).Logger().
|
||||
With().Str("path", path).Logger().
|
||||
With().Str("protocol", c.Request.Proto).Logger().
|
||||
With().Str("code", fmt.Sprint(c.Writer.Status())).Logger().
|
||||
With().Str("latency", ts.Sub(start).String()).Logger().
|
||||
With().Str("agent", c.Request.UserAgent()).Logger()
|
||||
|
||||
if c.Writer.Status() >= 400 && c.Writer.Status() < 500 {
|
||||
l.Warn().Msg(errorMessage)
|
||||
} else if c.Writer.Status() >= 500 {
|
||||
l.Error().Msg(errorMessage)
|
||||
} else {
|
||||
l.Info().Msg(errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MiddlewareSuite struct {
|
||||
router http.Handler
|
||||
context *gin.Context
|
||||
logReader *os.File
|
||||
logWriter *os.File
|
||||
}
|
||||
|
||||
var _ = Suite(&MiddlewareSuite{})
|
||||
|
||||
func (s *MiddlewareSuite) SetUpTest(c *C) {
|
||||
r, w, err := os.Pipe()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
utils.SetupJSONLogger("debug", w)
|
||||
mw := JSONLogger()
|
||||
|
||||
router := gin.New()
|
||||
router.UseRawPath = true
|
||||
router.Use(mw)
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
root := router.Group("/api")
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
root.GET("/ready", apiReady(isReady))
|
||||
root.GET("/healthy", apiHealthy)
|
||||
|
||||
s.router = router
|
||||
s.logReader = r
|
||||
s.logWriter = w
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TearDownTest(c *C) {
|
||||
s.router = nil
|
||||
s.context = nil
|
||||
s.logReader = nil
|
||||
s.logWriter = nil
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) HTTPRequest(method string, url string, body io.Reader) {
|
||||
recorder := httptest.NewRecorder()
|
||||
s.context, _ = gin.CreateTestContext(recorder)
|
||||
req, _ := http.NewRequestWithContext(s.context, method, url, body)
|
||||
s.context.Request = req
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(httptest.NewRecorder(), req)
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware4xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "warn")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["method"]; ok {
|
||||
c.Check(val, Equals, "GET")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'method' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["path"]; ok {
|
||||
c.Check(val, Equals, "/")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'path' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["protocol"]; ok {
|
||||
c.Check(val, Equals, "HTTP/1.1")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'protocol' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if val, ok := jsonMap["code"]; ok {
|
||||
c.Check(val, Equals, "404")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'code' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["remote"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'remote' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["latency"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'latency' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["agent"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'agent' key, obtained %s", capturedOutput)
|
||||
}
|
||||
|
||||
if _, ok := jsonMap["time"]; !ok {
|
||||
c.Errorf("Log message didn't have a 'time' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware2xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddleware5xx(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/ready", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "error")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestJSONMiddlewareRaw(c *C) {
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, s.logReader)
|
||||
fmt.Println(buf.String())
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/healthy?test=raw", nil)
|
||||
_ = s.logWriter.Close()
|
||||
capturedOutput := <-outC
|
||||
|
||||
var jsonMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(capturedOutput), &jsonMap)
|
||||
|
||||
fmt.Println(capturedOutput)
|
||||
|
||||
if val, ok := jsonMap["level"]; ok {
|
||||
c.Check(val, Equals, "info")
|
||||
} else {
|
||||
c.Errorf("Log message didn't have a 'level' key, obtained %s", capturedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestGetBasePath(c *C) {
|
||||
s.HTTPRequest(http.MethodGet, "", nil)
|
||||
path := getBasePath(s.context)
|
||||
c.Check(path, Equals, "/")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/api")
|
||||
|
||||
s.HTTPRequest(http.MethodGet, "/api/repos/testRepo", nil)
|
||||
path = getBasePath(s.context)
|
||||
c.Check(path, Equals, "/api/repos")
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestGetURLSegment(c *C) {
|
||||
url := "/"
|
||||
segment, err := getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/")
|
||||
|
||||
_, err = getURLSegment(url, 1)
|
||||
if err == nil {
|
||||
c.Error("Invalid return value")
|
||||
}
|
||||
|
||||
url = "/api"
|
||||
segment, err = getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
_, err = getURLSegment(url, 1)
|
||||
if err == nil {
|
||||
c.Error("Invalid return value")
|
||||
}
|
||||
|
||||
url = "/api/repos/testRepo"
|
||||
segment, err = getURLSegment(url, 0)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/api")
|
||||
|
||||
segment, err = getURLSegment(url, 1)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Check(*segment, Equals, "/repos")
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestInstrumentationMiddleware(c *C) {
|
||||
// Test instrumentation middleware functions
|
||||
router := gin.New()
|
||||
|
||||
// Add all instrumentation middleware
|
||||
router.Use(instrumentHandlerInFlight(apiRequestsInFlightGauge, getBasePath))
|
||||
router.Use(instrumentHandlerCounter(apiRequestsTotalCounter, getBasePath))
|
||||
router.Use(instrumentHandlerRequestSize(apiRequestSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerResponseSize(apiResponseSizeSummary, getBasePath))
|
||||
router.Use(instrumentHandlerDuration(apiRequestsDurationSummary, getBasePath))
|
||||
|
||||
// Add test handler
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Make request
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/test", nil)
|
||||
req.ContentLength = 42
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func getVerifier(keyRings []string) (pgp.Verifier, error) {
|
||||
verifier := context.GetVerifier()
|
||||
for _, keyRing := range keyRings {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
// @Summary List Mirrors
|
||||
// @Description **Show list of currently available mirrors**
|
||||
// @Description Each mirror is returned as in “show” API.
|
||||
// @Tags Mirrors
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.RemoteRepo
|
||||
// @Router /api/mirrors [get]
|
||||
func apiMirrorsList(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
result := []*deb.RemoteRepo{}
|
||||
_ = collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
result = append(result, repo)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
type mirrorCreateParams struct {
|
||||
// Name of mirror to be created
|
||||
Name string `binding:"required" json:"Name" example:"mirror2"`
|
||||
// Url of the archive to mirror
|
||||
ArchiveURL string `binding:"required" json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Distribution name to mirror
|
||||
Distribution string ` json:"Distribution" example:"'buster', for flat repositories use './'"`
|
||||
// Package query that is applied to mirror packages
|
||||
Filter string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Components to mirror, if not specified aptly would fetch all components
|
||||
Components []string ` json:"Components" example:"main"`
|
||||
// Limit mirror to those architectures, if not specified aptly would fetch all architectures
|
||||
Architectures []string ` json:"Architectures" example:"amd64"`
|
||||
// Gpg keyring(s) for verifying Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller bool ` json:"DownloadInstaller"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to skip if the given components are in the Release file
|
||||
SkipComponentCheck bool ` json:"SkipComponentCheck"`
|
||||
// Set "true" to skip the verification of architectures
|
||||
SkipArchitectureCheck bool ` json:"SkipArchitectureCheck"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures bool ` json:"IgnoreSignatures"`
|
||||
}
|
||||
|
||||
// @Summary Create Mirror
|
||||
// @Description **Create a mirror of a remote repository**
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body mirrorCreateParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/mirrors [post]
|
||||
func apiMirrorsCreate(c *gin.Context) {
|
||||
var err error
|
||||
var b mirrorCreateParams
|
||||
|
||||
b.DownloadSources = context.Config().DownloadSourcePackages
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
b.Architectures = context.ArchitecturesList()
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
if strings.HasPrefix(b.ArchiveURL, "ppa:") {
|
||||
b.ArchiveURL, b.Distribution, b.Components, err = deb.ParsePPA(b.ArchiveURL, context.Config())
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.Filter != "" {
|
||||
_, err = query.Parse(b.Filter)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(b.Name, b.ArchiveURL, b.Distribution, b.Components, b.Architectures,
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
repo.Filter = b.Filter
|
||||
repo.FilterWithDeps = b.FilterWithDeps
|
||||
repo.SkipComponentCheck = b.SkipComponentCheck
|
||||
repo.SkipArchitectureCheck = b.SkipArchitectureCheck
|
||||
repo.DownloadSources = b.DownloadSources
|
||||
repo.DownloadUdebs = b.DownloadUdebs
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
downloader := context.NewDownloader(nil)
|
||||
err = repo.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to fetch mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.Add(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to add mirror: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(201, repo)
|
||||
}
|
||||
|
||||
// @Summary Delete Mirror
|
||||
// @Description **Delete a mirror**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Param force query int true "force: 1 to enable"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 403 {object} Error "Unable to delete mirror with snapshots"
|
||||
// @Failure 500 {object} Error "Unable to delete"
|
||||
// @Router /api/mirrors/{name} [delete]
|
||||
func apiMirrorsDrop(c *gin.Context) {
|
||||
name := c.Params.ByName("name")
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
mirrorCollection := collectionFactory.RemoteRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
repo, err := mirrorCollection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to drop: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete mirror %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := repo.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.ByRemoteRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusForbidden, Value: nil}, fmt.Errorf("won't delete mirror with snapshots, use 'force=1' to override")
|
||||
}
|
||||
}
|
||||
|
||||
err = mirrorCollection.Drop(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to drop: %v", err)
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusNoContent, Value: nil}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get Mirror Info
|
||||
// @Description **Get mirror information by name**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [get]
|
||||
func apiMirrorsShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
// @Summary List Mirror Packages
|
||||
// @Description **Get a list of packages from a mirror**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name"
|
||||
// @Param q query string false "search query"
|
||||
// @Param format query string false "format: `details` for more detailed information"
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.Package "List of Packages"
|
||||
// @Failure 400 {object} Error "Unable to determine list of architectures"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name}/packages [get]
|
||||
func apiMirrorsPackages(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
}
|
||||
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to show package list, mirror hasn't been downloaded yet"))
|
||||
return
|
||||
}
|
||||
|
||||
reflist := repo.RefList()
|
||||
result := []*deb.Package{}
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
queryS := c.Request.URL.Query().Get("q")
|
||||
if queryS != "" {
|
||||
q, err := query.Parse(c.Request.URL.Query().Get("q"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
withDeps := c.Request.URL.Query().Get("withDeps") == "1"
|
||||
architecturesList := []string{}
|
||||
|
||||
if withDeps {
|
||||
if len(context.ArchitecturesList()) > 0 {
|
||||
architecturesList = context.ArchitecturesList()
|
||||
} else {
|
||||
architecturesList = list.Architectures(false)
|
||||
}
|
||||
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to determine list of architectures, please specify explicitly"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
|
||||
list, err = list.Filter(deb.FilterOptions{
|
||||
Queries: []deb.PackageQuery{q},
|
||||
WithDependencies: withDeps,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to search: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Request.URL.Query().Get("format") == "details" {
|
||||
_ = list.ForEach(func(p *deb.Package) error {
|
||||
result = append(result, p)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
} else {
|
||||
c.JSON(200, list.Strings())
|
||||
}
|
||||
}
|
||||
|
||||
type mirrorUpdateParams struct {
|
||||
// Change mirror name to `Name`
|
||||
Name string ` json:"Name" example:"mirror1"`
|
||||
// Url of the archive to mirror
|
||||
ArchiveURL string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Package query that is applied to mirror packages
|
||||
Filter string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Limit mirror to those architectures, if not specified aptly would fetch all architectures
|
||||
Architectures []string ` json:"Architectures" example:"amd64"`
|
||||
// Components to mirror, if not specified aptly would fetch all components
|
||||
Components []string ` json:"Components" example:"main"`
|
||||
// Gpg keyring(s) for verifing Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to skip checking if the given components are in the Release file
|
||||
SkipComponentCheck bool ` json:"SkipComponentCheck"`
|
||||
// Set "true" to skip checking if the given architectures are in the Release file
|
||||
SkipArchitectureCheck bool ` json:"SkipArchitectureCheck"`
|
||||
// Set "true" to ignore checksum errors
|
||||
IgnoreChecksums bool ` json:"IgnoreChecksums"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures bool ` json:"IgnoreSignatures"`
|
||||
// Set "true" to force a mirror update even if another process is already updating the mirror (use with caution!)
|
||||
ForceUpdate bool ` json:"ForceUpdate"`
|
||||
// Set "true" to skip downloading already downloaded packages
|
||||
SkipExistingPackages bool ` json:"SkipExistingPackages"`
|
||||
}
|
||||
|
||||
// @Summary Update Mirror
|
||||
// @Description **Update Mirror and download packages**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name to update"
|
||||
// @Consume json
|
||||
// @Param request body mirrorUpdateParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "Mirror was updated successfully"
|
||||
// @Success 202 {object} task.Task "Mirror is being updated"
|
||||
// @Failure 400 {object} Error "Unable to determine list of architectures"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [put]
|
||||
func apiMirrorsUpdate(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
remote *deb.RemoteRepo
|
||||
b mirrorUpdateParams
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
remote, err = collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
b.Name = remote.Name
|
||||
b.DownloadUdebs = remote.DownloadUdebs
|
||||
b.DownloadSources = remote.DownloadSources
|
||||
b.SkipComponentCheck = remote.SkipComponentCheck
|
||||
b.SkipArchitectureCheck = remote.SkipArchitectureCheck
|
||||
b.FilterWithDeps = remote.FilterWithDeps
|
||||
b.Filter = remote.Filter
|
||||
b.Architectures = remote.Architectures
|
||||
b.Components = remote.Components
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
|
||||
log.Info().Msgf("%s: Starting mirror update", b.Name)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != remote.Name {
|
||||
_, err = collection.ByName(b.Name)
|
||||
if err == nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to rename: mirror %s already exists", b.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.DownloadUdebs != remote.DownloadUdebs {
|
||||
if remote.IsFlat() && b.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to update: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.ArchiveURL != "" {
|
||||
remote.SetArchiveRoot(b.ArchiveURL)
|
||||
}
|
||||
|
||||
remote.Name = b.Name
|
||||
remote.DownloadUdebs = b.DownloadUdebs
|
||||
remote.DownloadSources = b.DownloadSources
|
||||
remote.SkipComponentCheck = b.SkipComponentCheck
|
||||
remote.SkipArchitectureCheck = b.SkipArchitectureCheck
|
||||
remote.FilterWithDeps = b.FilterWithDeps
|
||||
remote.Filter = b.Filter
|
||||
remote.Architectures = b.Architectures
|
||||
remote.Components = b.Components
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(remote.Key())}
|
||||
maybeRunTaskInBackground(c, "Update mirror "+b.Name, resources, func(out aptly.Progress, detail *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
|
||||
downloader := context.NewDownloader(out)
|
||||
err := remote.Fetch(downloader, verifier, b.IgnoreSignatures)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if !b.ForceUpdate {
|
||||
err = remote.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, b.SkipComponentCheck)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if remote.Filter != "" {
|
||||
var filterQuery deb.PackageQuery
|
||||
|
||||
filterQuery, err = query.Parse(remote.Filter)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
_, _, err = remote.ApplyFilter(context.DependencyOptions(), filterQuery, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// on any interruption, unlock the mirror
|
||||
e := context.ReOpenDatabase()
|
||||
if e == nil {
|
||||
remote.MarkAsIdle()
|
||||
_ = collection.Update(remote)
|
||||
}
|
||||
}()
|
||||
|
||||
remote.MarkAsUpdating()
|
||||
err = collection.Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.GoContextHandleSignals()
|
||||
|
||||
count := len(queue)
|
||||
taskDetail := struct {
|
||||
TotalDownloadSize int64
|
||||
RemainingDownloadSize int64
|
||||
TotalNumberOfPackages int
|
||||
RemainingNumberOfPackages int
|
||||
}{
|
||||
downloadSize, downloadSize, count, count,
|
||||
}
|
||||
detail.Store(taskDetail)
|
||||
|
||||
downloadQueue := make(chan int)
|
||||
taskFinished := make(chan *deb.PackageDownloadTask)
|
||||
|
||||
var (
|
||||
errors []string
|
||||
errLock sync.Mutex
|
||||
)
|
||||
|
||||
pushError := func(err error) {
|
||||
errLock.Lock()
|
||||
errors = append(errors, err.Error())
|
||||
errLock.Unlock()
|
||||
}
|
||||
|
||||
go func() {
|
||||
for idx := range queue {
|
||||
select {
|
||||
case downloadQueue <- idx:
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
close(downloadQueue)
|
||||
}()
|
||||
|
||||
// update of task details need to be done in order
|
||||
go func() {
|
||||
for {
|
||||
task, ok := <-taskFinished
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
taskDetail.RemainingDownloadSize -= task.File.Checksums.Size
|
||||
taskDetail.RemainingNumberOfPackages--
|
||||
detail.Store(taskDetail)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msgf("%s: Spawning background processes...", b.Name)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < context.Config().DownloadConcurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case idx, ok := <-downloadQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
task := &queue[idx]
|
||||
|
||||
var e error
|
||||
|
||||
// provision download location
|
||||
if pp, ok := context.PackagePool().(aptly.LocalPackagePool); ok {
|
||||
task.TempDownPath, e = pp.GenerateTempPath(task.File.Filename)
|
||||
} else {
|
||||
var file *os.File
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
// download file...
|
||||
e = context.Downloader().DownloadWithChecksum(
|
||||
context,
|
||||
remote.PackageURL(task.File.DownloadURL()).String(),
|
||||
task.TempDownPath,
|
||||
&task.File.Checksums,
|
||||
b.IgnoreChecksums)
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
// and import it back to the pool
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, collectionFactory.ChecksumCollection(nil))
|
||||
if err != nil {
|
||||
//return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to import file: %s", err)
|
||||
pushError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// update "attached" files if any
|
||||
for _, additionalAtask := range task.Additional {
|
||||
additionalAtask.File.PoolPath = task.File.PoolPath
|
||||
additionalAtask.File.Checksums = task.File.Checksums
|
||||
}
|
||||
|
||||
task.Done = true
|
||||
taskFinished <- task
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all download goroutines to finish
|
||||
log.Info().Msgf("%s: Waiting for background processes to finish...", b.Name)
|
||||
wg.Wait()
|
||||
log.Info().Msgf("%s: Background processes finished", b.Name)
|
||||
close(taskFinished)
|
||||
|
||||
defer func() {
|
||||
for _, task := range queue {
|
||||
if task.TempDownPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(task.TempDownPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Failed to delete %s: %v\n", task.TempDownPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-context.Done():
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: interrupted")
|
||||
default:
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
log.Info().Msgf("%s: Unable to update because of previous errors", b.Name)
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Finalizing download...", b.Name)
|
||||
_ = remote.FinalizeDownload(collectionFactory, out)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(remote)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
log.Info().Msgf("%s: Mirror updated successfully", b.Name)
|
||||
return &task.ProcessReturnValue{Code: http.StatusNoContent, Value: nil}, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type MirrorSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&MirrorSuite{})
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrors(c *C) {
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
response, _ := s.HTTPRequest("DELETE", "/api/mirrors/does-not-exist", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
c.Check(response.Body.String(), Equals, "{\"error\":\"unable to drop: mirror with name does-not-exist not found\"}")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.ExpectFailure("Need to mock downloads")
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "dummy",
|
||||
"ArchiveURL": "foobar",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
response, err := s.HTTPRequest("POST", "/api/mirrors", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Equals, "")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorShow(c *C) {
|
||||
// Test showing a specific mirror
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors/test-mirror", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorUpdate(c *C) {
|
||||
// Test updating a mirror
|
||||
body, _ := json.Marshal(gin.H{
|
||||
"ArchiveURL": "http://new.archive.url/debian",
|
||||
})
|
||||
response, _ := s.HTTPRequest("PUT", "/api/mirrors/test-mirror", bytes.NewReader(body))
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorPackages(c *C) {
|
||||
// Test listing packages in a mirror
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors/test-mirror/packages", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestMirrorUpdateRun(c *C) {
|
||||
// Test running mirror update
|
||||
response, _ := s.HTTPRequest("PUT", "/api/mirrors/test-mirror/update", nil)
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "github.com/aptly-dev/aptly/deb" // for swagger
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary Get Package Info
|
||||
// @Description **Show information about package by package key**
|
||||
// @Description Package keys could be obtained from various GET .../packages APIs.
|
||||
// @Tags Packages
|
||||
// @Produce json
|
||||
// @Param key path string true "package key (unique package identifier)"
|
||||
// @Success 200 {object} deb.Package "OK"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/packages/{key} [get]
|
||||
func apiPackagesShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
p, err := collectionFactory.PackageCollection().ByKey([]byte(c.Params.ByName("key")))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, p)
|
||||
}
|
||||
|
||||
// @Summary List Packages
|
||||
// @Description **Get list of packages**
|
||||
// @Tags Packages
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Param q query string false "search query"
|
||||
// @Param format query string false "format: `details` for more detailed information"
|
||||
// @Success 200 {array} string "List of packages"
|
||||
// @Router /api/packages [get]
|
||||
func apiPackages(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.PackageCollection()
|
||||
showPackages(c, collection.AllPackageRefs(), collectionFactory)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PackagesSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagesSuite{})
|
||||
|
||||
func (s *PackagesSuite) TestPackageShow(c *C) {
|
||||
// Test showing a specific package
|
||||
response, _ := s.HTTPRequest("GET", "/api/packages/Pamd64%20test%201.0%20abc123", nil)
|
||||
// Will return 404 as the package doesn't exist
|
||||
c.Check(response.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PackagesSuite) TestPackagesList(c *C) {
|
||||
// Test listing all packages
|
||||
response, _ := s.HTTPRequest("GET", "/api/packages", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result []interface{}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, NotNil)
|
||||
}
|
||||
|
||||
func (s *PackagesSuite) TestPackagesGetMaximumVersion(c *C) {
|
||||
// Create dummy repo first
|
||||
body, _ := json.Marshal(gin.H{"Name": "dummy"})
|
||||
resp, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 201)
|
||||
|
||||
// Now test packages with maximumVersion
|
||||
response, err := s.HTTPRequest("GET", "/api/repos/dummy/packages?maximumVersion=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
|
||||
// Clean up
|
||||
resp, err = s.HTTPRequest("DELETE", "/api/repos/dummy?force=1", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(resp.Code, Equals, 200)
|
||||
}
|
||||
+1084
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,675 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PublishAPITestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishAPITestSuite{})
|
||||
|
||||
func (s *PublishAPITestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishList(c *C) {
|
||||
// Test listing published repositories
|
||||
req, _ := http.NewRequest("GET", "/api/publish", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
var result []*deb.PublishedRepo
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishShow(c *C) {
|
||||
// Test showing a specific published repository
|
||||
// First, we need to create a snapshot and publish it
|
||||
// For now, test the endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test/bookworm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishUpdate(c *C) {
|
||||
// Test updating a published repository
|
||||
params := struct {
|
||||
Signing signingParams `json:"Signing"`
|
||||
}{
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishDrop(c *C) {
|
||||
// Test dropping a published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishListChanges(c *C) {
|
||||
// Test listing changes in a published repository
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test/bookworm/sources", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishAddSource(c *C) {
|
||||
// Test adding a source to published repository
|
||||
params := sourceParams{
|
||||
Component: "contrib",
|
||||
Name: "test-snap2",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test/bookworm/sources", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishUpdateSource(c *C) {
|
||||
// Test updating a source in published repository
|
||||
params := sourceParams{
|
||||
Component: "main",
|
||||
Name: "updated-snap",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm/sources/main", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishRemoveSource(c *C) {
|
||||
// Test removing a source from published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm/sources/contrib", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishSetSources(c *C) {
|
||||
// Test setting sources for published repository
|
||||
params := struct {
|
||||
Sources []sourceParams `json:"Sources"`
|
||||
}{
|
||||
Sources: []sourceParams{
|
||||
{Component: "main", Name: "snap1"},
|
||||
{Component: "contrib", Name: "snap2"},
|
||||
},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/publish/test/bookworm/sources", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishDropChanges(c *C) {
|
||||
// Test dropping changes from published repository
|
||||
req, _ := http.NewRequest("DELETE", "/api/publish/test/bookworm/sources", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the publish doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSigner(c *C) {
|
||||
// Test getSigner function
|
||||
// Test with Skip = true
|
||||
skipParams := &signingParams{Skip: true}
|
||||
signer, err := getSigner(skipParams)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, IsNil) // Should return nil when Skip is true
|
||||
|
||||
// Test with Skip = false - will use context signer
|
||||
params := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
Keyring: "trustedkeys.gpg",
|
||||
SecretKeyring: "secretkeys.gpg",
|
||||
Passphrase: "test",
|
||||
PassphraseFile: "/tmp/passphrase",
|
||||
}
|
||||
signer, err = getSigner(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, NotNil)
|
||||
}
|
||||
|
||||
|
||||
func (s *PublishAPITestSuite) TestSigningParamsStruct(c *C) {
|
||||
// Test signingParams struct and JSON marshaling/unmarshaling
|
||||
params := signingParams{
|
||||
Skip: true,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
Keyring: "trustedkeys.gpg",
|
||||
SecretKeyring: "secretkeys.gpg",
|
||||
Passphrase: "verysecure",
|
||||
PassphraseFile: "/etc/aptly.passphrase",
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*Skip.*true.*")
|
||||
c.Check(string(jsonData), Matches, ".*GpgKey.*A0546A43624A8331.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled signingParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.Skip, Equals, true)
|
||||
c.Check(unmarshaled.GpgKey, Equals, "A0546A43624A8331")
|
||||
c.Check(unmarshaled.Keyring, Equals, "trustedkeys.gpg")
|
||||
c.Check(unmarshaled.SecretKeyring, Equals, "secretkeys.gpg")
|
||||
c.Check(unmarshaled.Passphrase, Equals, "verysecure")
|
||||
c.Check(unmarshaled.PassphraseFile, Equals, "/etc/aptly.passphrase")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSourceParamsStruct(c *C) {
|
||||
// Test sourceParams struct and JSON marshaling/unmarshaling
|
||||
params := sourceParams{
|
||||
Component: "main",
|
||||
Name: "snap1",
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*Component.*main.*")
|
||||
c.Check(string(jsonData), Matches, ".*Name.*snap1.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled sourceParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.Component, Equals, "main")
|
||||
c.Check(unmarshaled.Name, Equals, "snap1")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerSkip(c *C) {
|
||||
// Test getSigner with Skip=true
|
||||
options := &signingParams{
|
||||
Skip: true,
|
||||
}
|
||||
|
||||
signer, err := getSigner(options)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(signer, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscape(c *C) {
|
||||
// Test slashEscape function
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", "."},
|
||||
{"test_path", "test/path"},
|
||||
{"test__path", "test_path"},
|
||||
{"test_path_file", "test/path/file"},
|
||||
{"test__test__test", "test_test_test"},
|
||||
{"_test_", "test/"},
|
||||
{"__", "_"},
|
||||
{"test_path__with__underscores", "test/path_with_underscores"},
|
||||
{"complex_path__example_test", "complex/path_example/test"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Input: %s", tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscapeEdgeCases(c *C) {
|
||||
// Test edge cases for slashEscape
|
||||
edgeCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"no_underscores_here", "no/underscores/here"},
|
||||
{"double__only", "double_only"},
|
||||
{"_", "."},
|
||||
{"__only", "_only"},
|
||||
{"only_", "only/"},
|
||||
{"mixed_case__Test_Path", "mixed/case_Test/Path"},
|
||||
{"numbers_123__test", "numbers/123_test"},
|
||||
{"special-chars.test_path", "special-chars.test/path"},
|
||||
}
|
||||
|
||||
for _, tc := range edgeCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Input: '%s'", tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishListBasic(c *C) {
|
||||
// Test basic API publish list endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/publish", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Now context is set up properly through APISuite
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Should return OK with empty list
|
||||
c.Check(w.Code, Equals, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishShowBasic(c *C) {
|
||||
// Test basic API publish show endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test-prefix/test-dist", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// This will fail because context is not set up properly
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Expect some kind of error due to missing context
|
||||
c.Check(w.Code, Not(Equals), http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishShowWithSlashEscape(c *C) {
|
||||
// Test API publish show with slash escape characters
|
||||
req, _ := http.NewRequest("GET", "/api/publish/test__prefix/test_dist", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Should attempt to process the escaped path
|
||||
c.Check(w.Code, Not(Equals), http.StatusOK) // Expected to fail due to missing context
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishedRepoCreateParamsStruct(c *C) {
|
||||
// Test publishedRepoCreateParams struct
|
||||
skipContents := true
|
||||
skipCleanup := false
|
||||
skipBz2 := true
|
||||
acquireByHash := false
|
||||
multiDist := true
|
||||
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "snapshot",
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-snap"}},
|
||||
Distribution: "bookworm",
|
||||
Label: "Test Label",
|
||||
Origin: "Test Origin",
|
||||
ForceOverwrite: true,
|
||||
Architectures: []string{"amd64", "armhf"},
|
||||
Signing: signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "A0546A43624A8331",
|
||||
},
|
||||
NotAutomatic: "yes",
|
||||
ButAutomaticUpgrades: "yes",
|
||||
SkipContents: &skipContents,
|
||||
SkipCleanup: &skipCleanup,
|
||||
SkipBz2: &skipBz2,
|
||||
AcquireByHash: &acquireByHash,
|
||||
MultiDist: &multiDist,
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*SourceKind.*snapshot.*")
|
||||
c.Check(string(jsonData), Matches, ".*Distribution.*bookworm.*")
|
||||
c.Check(string(jsonData), Matches, ".*Label.*Test Label.*")
|
||||
c.Check(string(jsonData), Matches, ".*Origin.*Test Origin.*")
|
||||
c.Check(string(jsonData), Matches, ".*ForceOverwrite.*true.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled publishedRepoCreateParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.SourceKind, Equals, "snapshot")
|
||||
c.Check(unmarshaled.Distribution, Equals, "bookworm")
|
||||
c.Check(unmarshaled.Label, Equals, "Test Label")
|
||||
c.Check(unmarshaled.Origin, Equals, "Test Origin")
|
||||
c.Check(unmarshaled.ForceOverwrite, Equals, true)
|
||||
c.Check(len(unmarshaled.Sources), Equals, 1)
|
||||
c.Check(unmarshaled.Sources[0].Component, Equals, "main")
|
||||
c.Check(unmarshaled.Sources[0].Name, Equals, "test-snap")
|
||||
c.Check(len(unmarshaled.Architectures), Equals, 2)
|
||||
c.Check(unmarshaled.Architectures[0], Equals, "amd64")
|
||||
c.Check(unmarshaled.Architectures[1], Equals, "armhf")
|
||||
c.Check(*unmarshaled.SkipContents, Equals, true)
|
||||
c.Check(*unmarshaled.SkipCleanup, Equals, false)
|
||||
c.Check(*unmarshaled.SkipBz2, Equals, true)
|
||||
c.Check(*unmarshaled.AcquireByHash, Equals, false)
|
||||
c.Check(*unmarshaled.MultiDist, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestPublishedRepoUpdateSwitchParamsStruct(c *C) {
|
||||
// Test publishedRepoUpdateSwitchParams struct
|
||||
skipContents := false
|
||||
skipBz2 := true
|
||||
skipCleanup := true
|
||||
acquireByHash := true
|
||||
multiDist := false
|
||||
|
||||
params := publishedRepoUpdateSwitchParams{
|
||||
ForceOverwrite: true,
|
||||
Signing: signingParams{
|
||||
Skip: true,
|
||||
GpgKey: "testkey",
|
||||
Keyring: "test.gpg",
|
||||
},
|
||||
SkipContents: &skipContents,
|
||||
SkipBz2: &skipBz2,
|
||||
SkipCleanup: &skipCleanup,
|
||||
Snapshots: []sourceParams{{Component: "main", Name: "snap1"}, {Component: "contrib", Name: "snap2"}},
|
||||
AcquireByHash: &acquireByHash,
|
||||
MultiDist: &multiDist,
|
||||
}
|
||||
|
||||
// Test JSON marshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(jsonData), Matches, ".*ForceOverwrite.*true.*")
|
||||
c.Check(string(jsonData), Matches, ".*SkipContents.*false.*")
|
||||
c.Check(string(jsonData), Matches, ".*SkipBz2.*true.*")
|
||||
|
||||
// Test JSON unmarshaling
|
||||
var unmarshaled publishedRepoUpdateSwitchParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(unmarshaled.ForceOverwrite, Equals, true)
|
||||
c.Check(unmarshaled.Signing.Skip, Equals, true)
|
||||
c.Check(unmarshaled.Signing.GpgKey, Equals, "testkey")
|
||||
c.Check(unmarshaled.Signing.Keyring, Equals, "test.gpg")
|
||||
c.Check(*unmarshaled.SkipContents, Equals, false)
|
||||
c.Check(*unmarshaled.SkipBz2, Equals, true)
|
||||
c.Check(*unmarshaled.SkipCleanup, Equals, true)
|
||||
c.Check(*unmarshaled.AcquireByHash, Equals, true)
|
||||
c.Check(*unmarshaled.MultiDist, Equals, false)
|
||||
c.Check(len(unmarshaled.Snapshots), Equals, 2)
|
||||
c.Check(unmarshaled.Snapshots[0].Component, Equals, "main")
|
||||
c.Check(unmarshaled.Snapshots[0].Name, Equals, "snap1")
|
||||
c.Check(unmarshaled.Snapshots[1].Component, Equals, "contrib")
|
||||
c.Check(unmarshaled.Snapshots[1].Name, Equals, "snap2")
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotInvalidJSON(c *C) {
|
||||
// Test POST endpoint with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotEmptySources(c *C) {
|
||||
// Test POST endpoint with empty sources
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "snapshot",
|
||||
Sources: []sourceParams{}, // Empty sources
|
||||
Distribution: "test",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 400 due to empty sources
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotUnknownSourceKind(c *C) {
|
||||
// Test POST endpoint with unknown source kind
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: "unknown",
|
||||
Sources: []sourceParams{{Component: "main", Name: "test"}},
|
||||
Distribution: "test",
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 400 due to unknown source kind
|
||||
c.Check(w.Code, Equals, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotValidRequest(c *C) {
|
||||
// Test POST endpoint with valid request (will fail due to missing context)
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: deb.SourceSnapshot,
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-snap"}},
|
||||
Distribution: "test-dist",
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail due to missing context but should get past basic validation
|
||||
c.Check(w.Code, Not(Equals), http.StatusBadRequest) // Should not be a 400 error
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestApiPublishRepoOrSnapshotLocalRepoSourceKind(c *C) {
|
||||
// Test POST endpoint with local repo source kind
|
||||
params := publishedRepoCreateParams{
|
||||
SourceKind: deb.SourceLocalRepo,
|
||||
Sources: []sourceParams{{Component: "main", Name: "test-repo"}},
|
||||
Distribution: "test-dist",
|
||||
Signing: signingParams{Skip: true},
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/publish/test-prefix", bytes.NewReader(jsonData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will fail due to missing context but should get past basic validation
|
||||
c.Check(w.Code, Not(Equals), http.StatusBadRequest) // Should not be a 400 error
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSigningParamsEdgeCases(c *C) {
|
||||
// Test signingParams with edge cases
|
||||
testCases := []signingParams{
|
||||
{Skip: true}, // Minimal case
|
||||
{Skip: false, GpgKey: "", Keyring: "", SecretKeyring: "", Passphrase: "", PassphraseFile: ""}, // Empty strings
|
||||
{Skip: false, GpgKey: "very-long-key-id-123456789012345678901234567890", Keyring: "very-long-keyring-name.gpg"}, // Long values
|
||||
{Skip: false, Passphrase: "password with spaces and special chars !@#$%^&*()"}, // Special characters
|
||||
{Skip: false, PassphraseFile: "/very/long/path/to/passphrase/file/that/might/not/exist.txt"}, // Long file path
|
||||
}
|
||||
|
||||
for i, params := range testCases {
|
||||
// Test JSON marshaling/unmarshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
|
||||
var unmarshaled signingParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Skip, Equals, params.Skip, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.GpgKey, Equals, params.GpgKey, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Keyring, Equals, params.Keyring, Commentf("Test case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSourceParamsEdgeCases(c *C) {
|
||||
// Test sourceParams with edge cases
|
||||
testCases := []sourceParams{
|
||||
{Component: "", Name: ""}, // Empty strings
|
||||
{Component: "very-long-component-name-with-dashes-and-numbers-123", Name: "very-long-name-456"}, // Long values
|
||||
{Component: "comp.with.dots", Name: "name_with_underscores"}, // Special characters
|
||||
{Component: "UPPERCASE", Name: "MixedCase"}, // Case variations
|
||||
{Component: "123numeric", Name: "456numbers"}, // Numeric values
|
||||
}
|
||||
|
||||
for i, params := range testCases {
|
||||
// Test JSON marshaling/unmarshaling
|
||||
jsonData, err := json.Marshal(params)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
|
||||
var unmarshaled sourceParams
|
||||
err = json.Unmarshal(jsonData, &unmarshaled)
|
||||
c.Check(err, IsNil, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Component, Equals, params.Component, Commentf("Test case %d", i))
|
||||
c.Check(unmarshaled.Name, Equals, params.Name, Commentf("Test case %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestSlashEscapeComprehensive(c *C) {
|
||||
// Comprehensive test of slashEscape function
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
description string
|
||||
}{
|
||||
{"", ".", "empty string"},
|
||||
{"simple", "simple", "no underscores"},
|
||||
{"one_underscore", "one/underscore", "single underscore"},
|
||||
{"two__underscores", "two_underscores", "double underscore"},
|
||||
{"_leading", "leading", "leading underscore"},
|
||||
{"trailing_", "trailing/", "trailing underscore"},
|
||||
{"_both_", "both/", "both leading and trailing"},
|
||||
{"__double_leading", "_double/leading", "double leading underscore"},
|
||||
{"trailing_double__", "trailing/double_", "double trailing underscore"},
|
||||
{"mixed_single__double_combo", "mixed/single_double/combo", "mixed single and double"},
|
||||
{"complex_path__with_multiple__sections", "complex/path_with/multiple_sections", "complex path"},
|
||||
{"a_b_c_d_e", "a/b/c/d/e", "multiple single underscores"},
|
||||
{"a__b__c__d__e", "a_b_c_d_e", "multiple double underscores"},
|
||||
{"_a__b_c__d_", "a_b/c_d/", "mixed pattern"},
|
||||
{"test___triple", "test_/triple", "triple underscore"},
|
||||
{"test____quad", "test__quad", "quadruple underscore"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := slashEscape(tc.input)
|
||||
c.Check(result, Equals, tc.expected, Commentf("Test case: %s (input: '%s')", tc.description, tc.input))
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementations for testing context dependencies
|
||||
type MockSigner struct {
|
||||
initError error
|
||||
key string
|
||||
keyring string
|
||||
secretKeyring string
|
||||
passphrase string
|
||||
passphraseFile string
|
||||
batch bool
|
||||
}
|
||||
|
||||
func (m *MockSigner) SetKey(key string) { m.key = key }
|
||||
func (m *MockSigner) SetKeyRing(keyring, secretKeyring string) {
|
||||
m.keyring = keyring
|
||||
m.secretKeyring = secretKeyring
|
||||
}
|
||||
func (m *MockSigner) SetPassphrase(passphrase, passphraseFile string) {
|
||||
m.passphrase = passphrase
|
||||
m.passphraseFile = passphraseFile
|
||||
}
|
||||
func (m *MockSigner) SetBatch(batch bool) { m.batch = batch }
|
||||
func (m *MockSigner) Init() error { return m.initError }
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerMockSuccess(c *C) {
|
||||
// Test getSigner logic with mock (can't test actual getSigner due to context dependencies)
|
||||
options := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "testkey",
|
||||
Keyring: "test.gpg",
|
||||
SecretKeyring: "secret.gpg",
|
||||
Passphrase: "testpass",
|
||||
PassphraseFile: "/tmp/passfile",
|
||||
}
|
||||
|
||||
// Mock the signer behavior
|
||||
mockSigner := &MockSigner{initError: nil}
|
||||
|
||||
// Simulate what getSigner would do
|
||||
mockSigner.SetKey(options.GpgKey)
|
||||
mockSigner.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
mockSigner.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
mockSigner.SetBatch(true)
|
||||
err := mockSigner.Init()
|
||||
|
||||
c.Check(err, IsNil)
|
||||
c.Check(mockSigner.key, Equals, "testkey")
|
||||
c.Check(mockSigner.keyring, Equals, "test.gpg")
|
||||
c.Check(mockSigner.secretKeyring, Equals, "secret.gpg")
|
||||
c.Check(mockSigner.passphrase, Equals, "testpass")
|
||||
c.Check(mockSigner.passphraseFile, Equals, "/tmp/passfile")
|
||||
c.Check(mockSigner.batch, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishAPITestSuite) TestGetSignerMockError(c *C) {
|
||||
// Test getSigner logic with mock error
|
||||
options := &signingParams{
|
||||
Skip: false,
|
||||
GpgKey: "invalidkey",
|
||||
}
|
||||
|
||||
// Mock the signer behavior with error
|
||||
mockSigner := &MockSigner{initError: fmt.Errorf("mock init error")}
|
||||
|
||||
mockSigner.SetKey(options.GpgKey)
|
||||
mockSigner.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
mockSigner.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
mockSigner.SetBatch(true)
|
||||
err := mockSigner.Init()
|
||||
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "mock init error")
|
||||
}
|
||||
+910
@@ -0,0 +1,910 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary Serve HTML Listing
|
||||
// @Description If ServeInAPIMode is enabled in aptly config,
|
||||
// @Description this endpoint is enabled which returns an HTML listing of each repo that can be browsed
|
||||
// @Tags Repos
|
||||
// @Produce html
|
||||
// @Success 200 {object} string "HTML"
|
||||
// @Router /api/repos [get]
|
||||
func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
c.Writer.Flush()
|
||||
_, _ = c.Writer.WriteString("<pre>\n")
|
||||
if len(localRepos) == 0 {
|
||||
_, _ = c.Writer.WriteString("<a href=\"-/\">default</a>\n")
|
||||
}
|
||||
for publishPrefix := range localRepos {
|
||||
_, _ = c.Writer.WriteString(fmt.Sprintf("<a href=\"%[1]s/\">%[1]s</a>\n", publishPrefix))
|
||||
}
|
||||
_, _ = c.Writer.WriteString("</pre>")
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Serve Packages
|
||||
// @Description If ServeInAPIMode is enabled in aptly config,
|
||||
// @Description this api serves a specified package from storage
|
||||
// @Tags Repos
|
||||
// @Param storage path string true "Storage"
|
||||
// @Param pkgPath path string true "Package Path" allowReserved=true
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/{storage}/{pkgPath} [get]
|
||||
func reposServeInAPIMode(c *gin.Context) {
|
||||
pkgpath := c.Param("pkgPath")
|
||||
|
||||
storage := c.Param("storage")
|
||||
if storage == "-" {
|
||||
storage = ""
|
||||
} else {
|
||||
storage = "filesystem:" + storage
|
||||
}
|
||||
|
||||
publicPath := context.GetPublishedStorage(storage).(aptly.FileSystemPublishedStorage).PublicPath()
|
||||
c.FileFromFS(pkgpath, http.Dir(publicPath))
|
||||
}
|
||||
|
||||
// @Summary List Repositories
|
||||
// @Description **Get list of available repos**
|
||||
// @Description Each repo is returned as in “show” API.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.LocalRepo
|
||||
// @Router /api/repos [get]
|
||||
func apiReposList(c *gin.Context) {
|
||||
result := []*deb.LocalRepo{}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
_ = collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
result = append(result, r)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
type repoCreateParams struct {
|
||||
// Name of repository to create
|
||||
Name string `binding:"required" json:"Name" example:"repo1"`
|
||||
// Text describing the repository (optional)
|
||||
Comment string ` json:"Comment" example:"this is a repo"`
|
||||
// Default distribution when publishing from this local repo
|
||||
DefaultDistribution string ` json:"DefaultDistribution" example:"stable"`
|
||||
// Default component when publishing from this local repo
|
||||
DefaultComponent string ` json:"DefaultComponent" example:"main"`
|
||||
// Snapshot name to create repoitory from (optional)
|
||||
FromSnapshot string ` json:"FromSnapshot" example:""`
|
||||
}
|
||||
|
||||
// @Summary Create Repository
|
||||
// @Description **Create a local repository**
|
||||
// @Description
|
||||
// @Description Distribution and component are used as defaults when publishing repo either directly or via snapshot.
|
||||
// @Description
|
||||
// @Description ```
|
||||
// @Description $ curl -X POST -H 'Content-Type: application/json' --data '{"Name": "aptly-repo"}' http://localhost:8080/api/repos
|
||||
// @Description {"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Consume json
|
||||
// @Param request body repoCreateParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Source snapshot not found"
|
||||
// @Failure 409 {object} Error "Local repo already exists"
|
||||
// @Failure 500 {object} Error "Internal error"
|
||||
// @Router /api/repos [post]
|
||||
func apiReposCreate(c *gin.Context) {
|
||||
var b repoCreateParams
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
repo := deb.NewLocalRepo(b.Name, b.Comment)
|
||||
repo.DefaultComponent = b.DefaultComponent
|
||||
repo.DefaultDistribution = b.DefaultDistribution
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
if b.FromSnapshot != "" {
|
||||
var snapshot *deb.Snapshot
|
||||
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
snapshot, err := snapshotCollection.ByName(b.FromSnapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, fmt.Errorf("source snapshot not found: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = snapshotCollection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, fmt.Errorf("unable to load source snapshot: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
repo.UpdateRefList(snapshot.RefList())
|
||||
}
|
||||
|
||||
localRepoCollection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
if _, err := localRepoCollection.ByName(b.Name); err == nil {
|
||||
AbortWithJSONError(c, http.StatusConflict, fmt.Errorf("local repo with name %s already exists", b.Name))
|
||||
return
|
||||
}
|
||||
|
||||
err := localRepoCollection.Add(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, repo)
|
||||
}
|
||||
|
||||
type reposEditParams struct {
|
||||
// Name of repository to modify
|
||||
Name *string `binding:"required" json:"Name" example:"repo1"`
|
||||
// Change Comment of repository
|
||||
Comment *string ` json:"Comment" example:"example repo"`
|
||||
// Change Default Distribution for publishing
|
||||
DefaultDistribution *string ` json:"DefaultDistribution" example:""`
|
||||
// Change Devault Component for publishing
|
||||
DefaultComponent *string ` json:"DefaultComponent" example:""`
|
||||
}
|
||||
|
||||
// @Summary Update Repository
|
||||
// @Description **Update local repository meta information**
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body reposEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.LocalRepo "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/repos/{name} [put]
|
||||
func apiReposEdit(c *gin.Context) {
|
||||
var b reposEditParams
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != nil {
|
||||
_, err := collection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
// already exists
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
repo.Name = *b.Name
|
||||
}
|
||||
if b.Comment != nil {
|
||||
repo.Comment = *b.Comment
|
||||
}
|
||||
if b.DefaultDistribution != nil {
|
||||
repo.DefaultDistribution = *b.DefaultDistribution
|
||||
}
|
||||
if b.DefaultComponent != nil {
|
||||
repo.DefaultComponent = *b.DefaultComponent
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
// GET /api/repos/:name
|
||||
// @Summary Get Repository Info
|
||||
// @Description Returns basic information about local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.LocalRepo
|
||||
// @Failure 404 {object} Error "Repository not found"
|
||||
// @Router /api/repos/{name} [get]
|
||||
func apiReposShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
// @Summary Delete Repository
|
||||
// @Description Drop/delete a repo
|
||||
// @Description Cannot drop repos that are published.
|
||||
// @Description Needs force=1 to drop repos used as source by other repos.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Param force query int false "force: 1 to enable"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "Repo object"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Repo Conflict"
|
||||
// @Router /api/repos/{name} [delete]
|
||||
func apiReposDrop(c *gin.Context) {
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
publishedCollection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
taskName := fmt.Sprintf("Delete repo %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
published := publishedCollection.ByLocalRepo(repo)
|
||||
if len(published) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop, local repo is published")
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.ByLocalRepoSource(repo)
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop, local repo has snapshots, use ?force=1 to override")
|
||||
}
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, collection.Drop(repo)
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary List Repo Packages
|
||||
// @Description **Return a list of packages present in the repo**
|
||||
// @Description
|
||||
// @Description If `q` query parameter is missing, return all packages, otherwise return packages that match q
|
||||
// @Description
|
||||
// @Description **Example:**
|
||||
// @Description ```
|
||||
// @Description $ curl http://localhost:8080/api/repos/aptly-repo/packages
|
||||
// @Description ["Pi386 aptly 0.8 966561016b44ed80"]
|
||||
// @Description ```
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param q query string true "Package query (e.g Name%20(~%20matlab))"
|
||||
// @Param withDeps query string true "Set to 1 to include dependencies when evaluating package query"
|
||||
// @Param format query string true "Set to 'details' to return extra info about each package"
|
||||
// @Param maximumVersion query string true "Set to 1 to only return the highest version for each package name"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 404 {object} Error "Internal Server Error"
|
||||
// @Router /api/repos/{name}/packages [get]
|
||||
func apiReposPackagesShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
showPackages(c, repo.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
type reposPackagesAddDeleteParams struct {
|
||||
// Package Refs
|
||||
PackageRefs []string `binding:"required" json:"PackageRefs" example:""`
|
||||
}
|
||||
|
||||
// Handler for both add and delete
|
||||
func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(list *deb.PackageList, p *deb.Package, out aptly.Progress) error) {
|
||||
var b reposPackagesAddDeleteParams
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
|
||||
maybeRunTaskInBackground(c, taskNamePrefix+repo.Name, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
out.Printf("Loading packages...\n")
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// verify package refs and build package list
|
||||
for _, ref := range b.PackageRefs {
|
||||
var p *deb.Package
|
||||
|
||||
p, err = collectionFactory.PackageCollection().ByKey([]byte(ref))
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("packages %s: %s", ref, err)
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = cb(list, p, out)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
}
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: repo}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Add Packages by Key
|
||||
// @Description **Add packages to local repository by package keys.**
|
||||
// @Description
|
||||
// @Description Any package can be added that is present in the aptly database (from any mirror, snapshot, local repository). This API combined with package list (search) APIs allows one to implement importing, copying, moving packages around.
|
||||
// @Description
|
||||
// @Description API verifies that packages actually exist in aptly database and checks constraint that conflicting packages can’t be part of the same local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 400 {object} Error "Internal Server Error"
|
||||
// @Router /api/repos/{name}/packages [post]
|
||||
func apiReposPackagesAdd(c *gin.Context) {
|
||||
apiReposPackagesAddDelete(c, "Add packages to repo ", func(list *deb.PackageList, p *deb.Package, out aptly.Progress) error {
|
||||
out.Printf("Adding package %s\n", p.Name)
|
||||
return list.Add(p)
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Delete Packages by Key
|
||||
// @Description **Remove packages from local repository by package keys.**
|
||||
// @Description
|
||||
// @Description Any package(s) can be removed from a local repository. Package references from a local repository can be retrieved with GET /api/repos/:name/packages.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Consume json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 400 {object} Error "Internal Server Error"
|
||||
// @Router /api/repos/{name}/packages [delete]
|
||||
func apiReposPackagesDelete(c *gin.Context) {
|
||||
apiReposPackagesAddDelete(c, "Delete packages from repo ", func(list *deb.PackageList, p *deb.Package, out aptly.Progress) error {
|
||||
out.Printf("Removing package %s\n", p.Name)
|
||||
list.Remove(p)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Add Uploaded File
|
||||
// @Description Import packages from files (uploaded using File Upload API) to the local repository. If directory specified, aptly would discover package files automatically.
|
||||
// @Description Adding same package to local repository is not an error.
|
||||
// @Description By default aptly would try to remove every successfully processed file and directory `dir` (if it becomes empty after import).
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string false "Filename (optional)"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Failure 400 {object} Error "wrong file"
|
||||
// @Failure 404 {object} Error "Repository not found"
|
||||
// @Failure 500 {object} Error "Error adding files"
|
||||
// @Router /api/repos/{name}/file/{dir}/{file} [post]
|
||||
func apiReposPackageFromFile(c *gin.Context) {
|
||||
// redirect all work to dir method
|
||||
apiReposPackageFromDir(c)
|
||||
}
|
||||
|
||||
// @Summary Add Uploaded Directory
|
||||
// @Description Import packages from files (uploaded using File Upload API) to the local repository. If directory specified, aptly would discover package files automatically.
|
||||
// @Description Adding same package to local repository is not an error.
|
||||
// @Description By default aptly would try to remove every successfully processed file and directory `dir` (if it becomes empty after import).
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory to add"
|
||||
// @Consume json
|
||||
// @Param noRemove query string false "when value is set to 1, don’t remove any files"
|
||||
// @Param forceReplace query string false "when value is set to 1, remove packages conflicting with package being added (in local repository)"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Failure 400 {object} Error "wrong file"
|
||||
// @Failure 404 {object} Error "Repository not found"
|
||||
// @Failure 500 {object} Error "Error adding files"
|
||||
// @Router /api/repos/{name}/file/{dir} [post]
|
||||
func apiReposPackageFromDir(c *gin.Context) {
|
||||
forceReplace := c.Request.URL.Query().Get("forceReplace") == "1"
|
||||
noRemove := c.Request.URL.Query().Get("noRemove") == "1"
|
||||
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
dirParam := utils.SanitizePath(c.Params.ByName("dir"))
|
||||
fileParam := utils.SanitizePath(c.Params.ByName("file"))
|
||||
if fileParam != "" && !verifyPath(fileParam) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
var taskName string
|
||||
var sources []string
|
||||
if fileParam == "" {
|
||||
taskName = fmt.Sprintf("Add packages from dir %s to repo %s", dirParam, name)
|
||||
sources = []string{filepath.Join(context.UploadPath(), dirParam)}
|
||||
} else {
|
||||
sources = []string{filepath.Join(context.UploadPath(), dirParam, fileParam)}
|
||||
taskName = fmt.Sprintf("Add package %s from dir %s to repo %s", fileParam, dirParam, name)
|
||||
}
|
||||
|
||||
resources := []string{string(repo.Key())}
|
||||
resources = append(resources, sources...)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
verifier := context.GetVerifier()
|
||||
|
||||
var (
|
||||
packageFiles, failedFiles []string
|
||||
otherFiles []string
|
||||
processedFiles, failedFiles2 []string
|
||||
reporter = &aptly.RecordingResultReporter{
|
||||
Warnings: []string{},
|
||||
AddedLines: []string{},
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
list *deb.PackageList
|
||||
)
|
||||
|
||||
packageFiles, otherFiles, failedFiles = deb.CollectPackageFiles(sources, reporter)
|
||||
|
||||
list, err := deb.NewPackageListFromRefList(repo.RefList(), collectionFactory.PackageCollection(), nil)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
processedFiles, failedFiles2, err = deb.ImportPackageFiles(list, packageFiles, forceReplace, verifier, context.PackagePool(),
|
||||
collectionFactory.PackageCollection(), reporter, nil, collectionFactory.ChecksumCollection)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
processedFiles = append(processedFiles, otherFiles...)
|
||||
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to import package files: %s", err)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(deb.NewPackageRefListFromPackageList(list))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
|
||||
if !noRemove {
|
||||
processedFiles = utils.StrSliceDeduplicate(processedFiles)
|
||||
|
||||
for _, file := range processedFiles {
|
||||
err := os.Remove(file)
|
||||
if err != nil {
|
||||
reporter.Warning("unable to remove file %s: %s", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
failedFiles = []string{}
|
||||
}
|
||||
|
||||
if len(reporter.AddedLines) > 0 {
|
||||
out.Printf("Added: %s\n", strings.Join(reporter.AddedLines, ", "))
|
||||
}
|
||||
if len(reporter.RemovedLines) > 0 {
|
||||
out.Printf("Removed: %s\n", strings.Join(reporter.RemovedLines, ", "))
|
||||
}
|
||||
if len(reporter.Warnings) > 0 {
|
||||
out.Printf("Warnings: %s\n", strings.Join(reporter.Warnings, ", "))
|
||||
}
|
||||
if len(failedFiles) > 0 {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{
|
||||
"Report": reporter,
|
||||
"FailedFiles": failedFiles,
|
||||
}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type reposCopyPackageParams struct {
|
||||
// Copy also dependencies
|
||||
WithDeps bool `json:"with-deps,omitempty"`
|
||||
// Do not perform operations
|
||||
DryRun bool `json:"dry-run,omitempty"`
|
||||
}
|
||||
|
||||
// @Summary Copy Package
|
||||
// @Description Copies a package from a source to destination repository
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Destination repo"
|
||||
// @Param src path string true "Source repo"
|
||||
// @Param file path string true "File/packages to copy"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 200 {object} task.ProcessReturnValue "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 422 {object} Error "Unprocessable Entity"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/repos/{name}/copy/{src}/{file} [post]
|
||||
func apiReposCopyPackage(c *gin.Context) {
|
||||
dstRepoName := c.Params.ByName("name")
|
||||
srcRepoName := c.Params.ByName("src")
|
||||
fileName := c.Params.ByName("file")
|
||||
|
||||
jsonBody := reposCopyPackageParams{
|
||||
WithDeps: false,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
err := c.Bind(&jsonBody)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
dstRepo, err := collectionFactory.LocalRepoCollection().ByName(dstRepoName)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("dest repo error: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
var srcRepo *deb.LocalRepo
|
||||
srcRepo, err = collectionFactory.LocalRepoCollection().ByName(srcRepoName)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("src repo error: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if srcRepo.UUID == dstRepo.UUID {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("dest and source are identical"))
|
||||
return
|
||||
}
|
||||
|
||||
taskName := fmt.Sprintf("Copy packages from repo %s to repo %s", srcRepoName, dstRepoName)
|
||||
resources := []string{string(dstRepo.Key()), string(srcRepo.Key())}
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(dstRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("dest repo error: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(srcRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("src repo error: %s", err)
|
||||
}
|
||||
|
||||
srcRefList := srcRepo.RefList()
|
||||
|
||||
reporter := &aptly.RecordingResultReporter{
|
||||
Warnings: []string{},
|
||||
AddedLines: []string{},
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
|
||||
dstList, err := deb.NewPackageListFromRefList(dstRepo.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in dest: %s", err)
|
||||
}
|
||||
|
||||
srcList, err := deb.NewPackageListFromRefList(srcRefList, collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to load packages in src: %s", err)
|
||||
}
|
||||
|
||||
srcList.PrepareIndex()
|
||||
|
||||
var architecturesList []string
|
||||
|
||||
if jsonBody.WithDeps {
|
||||
dstList.PrepareIndex()
|
||||
|
||||
// Calculate architectures
|
||||
if len(context.ArchitecturesList()) > 0 {
|
||||
architecturesList = context.ArchitecturesList()
|
||||
} else {
|
||||
architecturesList = dstList.Architectures(false)
|
||||
}
|
||||
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusUnprocessableEntity, Value: nil}, fmt.Errorf("unable to determine list of architectures, please specify explicitly")
|
||||
}
|
||||
}
|
||||
|
||||
// srcList.Filter|FilterWithProgress only accept query list
|
||||
queries := make([]deb.PackageQuery, 1)
|
||||
queries[0], err = query.Parse(fileName)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusUnprocessableEntity, Value: nil}, fmt.Errorf("unable to parse query '%s': %s", fileName, err)
|
||||
}
|
||||
|
||||
toProcess, err := srcList.Filter(deb.FilterOptions{
|
||||
Queries: queries,
|
||||
WithDependencies: jsonBody.WithDeps,
|
||||
Source: dstList,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
Progress: context.Progress(),
|
||||
})
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("filter error: %s", err)
|
||||
}
|
||||
|
||||
if toProcess.Len() == 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusUnprocessableEntity, Value: nil}, fmt.Errorf("no package found for filter: '%s'", fileName)
|
||||
}
|
||||
|
||||
err = toProcess.ForEach(func(p *deb.Package) error {
|
||||
err = dstList.Add(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("added %s-%s(%s)", p.Name, p.Version, p.Architecture)
|
||||
reporter.AddedLines = append(reporter.AddedLines, name)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("error processing dest add: %s", err)
|
||||
}
|
||||
|
||||
if jsonBody.DryRun {
|
||||
reporter.Warning("Changes not saved, as dry run has been requested")
|
||||
} else {
|
||||
dstRepo.UpdateRefList(deb.NewPackageRefListFromPackageList(dstList))
|
||||
|
||||
err = collectionFactory.LocalRepoCollection().Update(dstRepo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to save: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{
|
||||
"Report": reporter,
|
||||
}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Include File from Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string true "File/packages to include"
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir}/{file} [post]
|
||||
func apiReposIncludePackageFromFile(c *gin.Context) {
|
||||
// redirect all work to dir method
|
||||
apiReposIncludePackageFromDir(c)
|
||||
}
|
||||
|
||||
type reposIncludePackageFromDirResponse struct {
|
||||
Report *aptly.RecordingResultReporter
|
||||
FailedFiles []string
|
||||
}
|
||||
|
||||
// @Summary Include Directory
|
||||
// @Description Allows automatic processing of .changes file controlling package upload (uploaded using File Upload API) to the local repository. i.e. Exposes repo include command in api.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param forceReplace query int false "when value is set to 1, when adding package that conflicts with existing package, remove existing package"
|
||||
// @Param noRemoveFiles query int false "when value is set to 1, don’t remove files that have been imported successfully into repository"
|
||||
// @Param acceptUnsigned query int false "when value is set to 1, accept unsigned .changes files"
|
||||
// @Param ignoreSignature query int false "when value is set to 1 disable verification of .changes file signature"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} reposIncludePackageFromDirResponse "Response"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/repos/{name}/include/{dir} [post]
|
||||
func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
forceReplace := c.Request.URL.Query().Get("forceReplace") == "1"
|
||||
noRemoveFiles := c.Request.URL.Query().Get("noRemoveFiles") == "1"
|
||||
acceptUnsigned := c.Request.URL.Query().Get("acceptUnsigned") == "1"
|
||||
ignoreSignature := c.Request.URL.Query().Get("ignoreSignature") == "1"
|
||||
|
||||
repoTemplateString := c.Params.ByName("name")
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
if !verifyDir(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []string
|
||||
var taskName string
|
||||
dirParam := utils.SanitizePath(c.Params.ByName("dir"))
|
||||
fileParam := utils.SanitizePath(c.Params.ByName("file"))
|
||||
if fileParam != "" && !verifyPath(fileParam) {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("wrong file"))
|
||||
return
|
||||
}
|
||||
|
||||
if fileParam == "" {
|
||||
taskName = fmt.Sprintf("Include packages from changes files in dir %s to repo matching template %s", dirParam, repoTemplateString)
|
||||
sources = []string{filepath.Join(context.UploadPath(), dirParam)}
|
||||
} else {
|
||||
taskName = fmt.Sprintf("Include packages from changes file %s from dir %s to repo matching template %s", fileParam, dirParam, repoTemplateString)
|
||||
sources = []string{filepath.Join(context.UploadPath(), dirParam, fileParam)}
|
||||
}
|
||||
|
||||
repoTemplate, err := template.New("repo").Parse(repoTemplateString)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("error parsing repo template: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
var resources []string
|
||||
if len(repoTemplate.Root.Nodes) > 1 {
|
||||
resources = append(resources, task.AllLocalReposResourcesKey)
|
||||
} else {
|
||||
// repo template string is simple text so only use resource key of specific repository
|
||||
repo, err := collectionFactory.LocalRepoCollection().ByName(repoTemplateString)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, string(repo.Key()))
|
||||
}
|
||||
resources = append(resources, sources...)
|
||||
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
var (
|
||||
err error
|
||||
verifier = context.GetVerifier()
|
||||
changesFiles []string
|
||||
failedFiles, failedFiles2 []string
|
||||
reporter = &aptly.RecordingResultReporter{
|
||||
Warnings: []string{},
|
||||
AddedLines: []string{},
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
)
|
||||
|
||||
changesFiles, failedFiles = deb.CollectChangesFiles(sources, reporter)
|
||||
_, failedFiles2, err = deb.ImportChangesFiles(
|
||||
changesFiles, reporter, acceptUnsigned, ignoreSignature, forceReplace, noRemoveFiles, verifier,
|
||||
repoTemplate, context.Progress(), collectionFactory.LocalRepoCollection(), collectionFactory.PackageCollection(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection, nil, query.Parse)
|
||||
failedFiles = append(failedFiles, failedFiles2...)
|
||||
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to import changes files: %s", err)
|
||||
}
|
||||
|
||||
if !noRemoveFiles {
|
||||
// atempt to remove dir, if it fails, that's fine: probably it's not empty
|
||||
_ = os.Remove(filepath.Join(context.UploadPath(), dirParam))
|
||||
}
|
||||
|
||||
if failedFiles == nil {
|
||||
failedFiles = []string{}
|
||||
}
|
||||
|
||||
if len(reporter.AddedLines) > 0 {
|
||||
out.Printf("Added: %s\n", strings.Join(reporter.AddedLines, ", "))
|
||||
}
|
||||
if len(reporter.RemovedLines) > 0 {
|
||||
out.Printf("Removed: %s\n", strings.Join(reporter.RemovedLines, ", "))
|
||||
}
|
||||
if len(reporter.Warnings) > 0 {
|
||||
out.Printf("Warnings: %s\n", strings.Join(reporter.Warnings, ", "))
|
||||
}
|
||||
if len(failedFiles) > 0 {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
Report: reporter,
|
||||
FailedFiles: failedFiles,
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: ret}, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ReposTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ReposTestSuite{})
|
||||
|
||||
func (s *ReposTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposListEmpty(c *C) {
|
||||
// Test listing repos when none exist
|
||||
req, _ := http.NewRequest("GET", "/api/repos", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
var result []*deb.LocalRepo
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(result), Equals, 0)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateBasic(c *C) {
|
||||
// Test creating a basic repository
|
||||
params := repoCreateParams{
|
||||
Name: "test-repo",
|
||||
Comment: "Test repository",
|
||||
DefaultDistribution: "stable",
|
||||
DefaultComponent: "main",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Now context is properly set up, should create successfully
|
||||
c.Check(w.Code, Equals, 201) // Expect successful creation
|
||||
|
||||
// Clean up: delete the created repo
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEdit(c *C) {
|
||||
// First create a repo
|
||||
params := repoCreateParams{
|
||||
Name: "edit-test-repo",
|
||||
Comment: "Original comment",
|
||||
}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Now edit it
|
||||
editParams := reposEditParams{
|
||||
Comment: stringPtr("Updated comment"),
|
||||
}
|
||||
body, _ = json.Marshal(editParams)
|
||||
req, _ = http.NewRequest("PUT", "/api/repos/edit-test-repo", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/edit-test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddDelete(c *C) {
|
||||
// First create a repo
|
||||
params := repoCreateParams{
|
||||
Name: "pkg-test-repo",
|
||||
}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Test adding packages (will fail without actual packages)
|
||||
addParams := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
body, _ = json.Marshal(addParams)
|
||||
req, _ = http.NewRequest("POST", "/api/repos/pkg-test-repo/packages", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Will fail as package doesn't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/pkg-test-repo?force=1", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackage(c *C) {
|
||||
// Create source and destination repos
|
||||
params := repoCreateParams{Name: "src-repo"}
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
params = repoCreateParams{Name: "dst-repo"}
|
||||
body, _ = json.Marshal(params)
|
||||
req, _ = http.NewRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 201)
|
||||
|
||||
// Test copy (will fail without packages)
|
||||
copyParams := reposCopyPackageParams{
|
||||
WithDeps: true,
|
||||
DryRun: true,
|
||||
}
|
||||
body, _ = json.Marshal(copyParams)
|
||||
req, _ = http.NewRequest("POST", "/api/repos/dst-repo/copy/src-repo/test", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
// Will return empty result as no packages match
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Clean up
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/src-repo?force=1", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
req, _ = http.NewRequest("DELETE", "/api/repos/dst-repo?force=1", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateInvalidJSON(c *C) {
|
||||
// Test creating repository with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBufferString("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateMissingName(c *C) {
|
||||
// Test creating repository without required name
|
||||
params := repoCreateParams{
|
||||
Comment: "Test repository",
|
||||
DefaultDistribution: "stable",
|
||||
DefaultComponent: "main",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposShowNotFound(c *C) {
|
||||
// Test showing non-existent repository
|
||||
req, _ := http.NewRequest("GET", "/api/repos/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests endpoint structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEditStructure(c *C) {
|
||||
// Test repository edit endpoint structure
|
||||
params := reposEditParams{
|
||||
Name: stringPtr("new-name"),
|
||||
Comment: stringPtr("Updated comment"),
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/repos/test-repo", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposEditInvalidJSON(c *C) {
|
||||
// Test edit with invalid JSON
|
||||
req, _ := http.NewRequest("PUT", "/api/repos/test-repo", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropStructure(c *C) {
|
||||
// Test repository drop endpoint structure
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 404 as test-repo doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropWithForce(c *C) {
|
||||
// Test repository drop with force parameter
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo?force=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesShowStructure(c *C) {
|
||||
// Test packages show endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/repos/test-repo/packages", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesShowWithQuery(c *C) {
|
||||
// Test packages show with query parameters
|
||||
req, _ := http.NewRequest("GET", "/api/repos/test-repo/packages?q=Name%20(~%20test)", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests query parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddStructure(c *C) {
|
||||
// Test packages add endpoint structure
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAddInvalidJSON(c *C) {
|
||||
// Test packages add with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesDeleteStructure(c *C) {
|
||||
// Test packages delete endpoint structure
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo/packages", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadStructure(c *C) {
|
||||
// Test file upload endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadWithParameters(c *C) {
|
||||
// Test file upload with query parameters
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir?noRemove=1&forceReplace=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposFileUploadSpecificFile(c *C) {
|
||||
// Test specific file upload endpoint
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/file/upload-dir/package.deb", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackageStructure(c *C) {
|
||||
// Test copy package endpoint structure
|
||||
params := reposCopyPackageParams{
|
||||
WithDeps: true,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyPackageInvalidJSON(c *C) {
|
||||
// Test copy package with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query", bytes.NewBufferString("invalid"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludePackageStructure(c *C) {
|
||||
// Test include package endpoint structure
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludePackageWithParameters(c *C) {
|
||||
// Test include package with query parameters
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir?forceReplace=1&noRemoveFiles=1&acceptUnsigned=1&ignoreSignature=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposIncludeSpecificFile(c *C) {
|
||||
// Test include specific file endpoint
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/include/upload-dir/package.changes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposParameterValidation(c *C) {
|
||||
// Test parameter validation and structure
|
||||
testCases := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
wantCode int
|
||||
}{
|
||||
{"invalid repo name chars", "GET", "/api/repos/invalid/name", "", 404}, // route doesn't match
|
||||
{"empty repo name", "GET", "/api/repos", "", 200}, // list repos endpoint
|
||||
{"invalid method", "PATCH", "/api/repos/test", "", 404},
|
||||
{"malformed JSON in create", "POST", "/api/repos", `{"Name":}`, 400},
|
||||
{"malformed JSON in edit", "PUT", "/api/repos/test", `{"Name":}`, 400},
|
||||
{"malformed JSON in packages", "POST", "/api/repos/test/packages", `{"PackageRefs":}`, 400},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
var req *http.Request
|
||||
if tc.body != "" {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, _ = http.NewRequest(tc.method, tc.path, nil)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, tc.wantCode, Commentf("Test case: %s", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposListInAPIModeStructure(c *C) {
|
||||
// Test reposListInAPIMode function structure
|
||||
localRepos := map[string]utils.FileSystemPublishRoot{
|
||||
"repo1": {},
|
||||
"repo2": {},
|
||||
}
|
||||
|
||||
handler := reposListInAPIMode(localRepos)
|
||||
c.Check(handler, NotNil)
|
||||
|
||||
// Test with empty repos map
|
||||
emptyHandler := reposListInAPIMode(map[string]utils.FileSystemPublishRoot{})
|
||||
c.Check(emptyHandler, NotNil)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposServeInAPIModeStructure(c *C) {
|
||||
// Test reposServeInAPIMode function structure by simulating call
|
||||
s.router.(*gin.Engine).GET("/api/:storage/*pkgPath", reposServeInAPIMode)
|
||||
|
||||
// Test with default storage
|
||||
req, _ := http.NewRequest("GET", "/api/-/some/package/path", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
|
||||
// Test with specific storage
|
||||
req, _ = http.NewRequest("GET", "/api/storage1/some/package/path", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCreateFromSnapshot(c *C) {
|
||||
// Test creating repository from snapshot
|
||||
params := repoCreateParams{
|
||||
Name: "test-repo-from-snapshot",
|
||||
Comment: "Test repository from snapshot",
|
||||
FromSnapshot: "test-snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context/snapshot, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPackagesAsyncOperations(c *C) {
|
||||
// Test async operations with _async parameter
|
||||
params := reposPackagesAddDeleteParams{
|
||||
PackageRefs: []string{"Pamd64 test 1.0 abc123"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/packages?_async=1", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests async parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposDropAsyncOperation(c *C) {
|
||||
// Test async repository drop
|
||||
req, _ := http.NewRequest("DELETE", "/api/repos/test-repo?_async=1&force=1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests async parameter parsing
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposCopyAsyncOperation(c *C) {
|
||||
// Test async copy operation
|
||||
params := reposCopyPackageParams{
|
||||
WithDeps: false,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/dest-repo/copy/src-repo/package-query?_async=1", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
// Helper function to create string pointer
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposPathSanitization(c *C) {
|
||||
// Test path sanitization in file operations
|
||||
testPaths := []string{
|
||||
"../../../etc/passwd",
|
||||
"normal-dir",
|
||||
"dir with spaces",
|
||||
".hidden-dir",
|
||||
"",
|
||||
}
|
||||
|
||||
for _, path := range testPaths {
|
||||
// Test sanitization doesn't cause crashes
|
||||
sanitized := utils.SanitizePath(path)
|
||||
c.Check(sanitized, NotNil)
|
||||
|
||||
// Test with file upload endpoints
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/repos/test-repo/file/%s", path), nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not crash, even if it errors due to missing context
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ReposTestSuite) TestReposErrorHandling(c *C) {
|
||||
// Test various error conditions and edge cases
|
||||
errorTests := []struct {
|
||||
description string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
expectedErr bool
|
||||
}{
|
||||
{"Missing required fields", "POST", "/api/repos", `{}`, true},
|
||||
{"Invalid package refs", "POST", "/api/repos/test/packages", `{"PackageRefs":[]}`, true},
|
||||
{"Invalid query format", "GET", "/api/repos/test/packages?q=invalid[query", "", false}, // Query validation happens deeper
|
||||
{"Copy to same repo", "POST", "/api/repos/test/copy/test/pkg", `{}`, false}, // Error happens in business logic
|
||||
{"File upload endpoint", "POST", "/api/repos/test/file/upload-dir", "", false}, // Valid endpoint
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
var req *http.Request
|
||||
if test.body != "" {
|
||||
req, _ = http.NewRequest(test.method, test.path, strings.NewReader(test.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req, _ = http.NewRequest(test.method, test.path, nil)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/aptly-dev/aptly/docs"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
var context *ctx.AptlyContext
|
||||
|
||||
func apiMetricsGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
countPackagesByRepos()
|
||||
promhttp.Handler().ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func redirectSwagger(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/docs/index.html" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
if c.Request.URL.Path == "/docs/" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
if c.Request.URL.Path == "/docs" {
|
||||
c.Redirect(http.StatusMovedPermanently, "/docs.html")
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
// Router returns prebuilt with routes http.Handler
|
||||
func Router(c *ctx.AptlyContext) http.Handler {
|
||||
if aptly.EnableDebug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
context = c
|
||||
|
||||
router.UseRawPath = true
|
||||
|
||||
if c.Config().LogFormat == "json" {
|
||||
gin.DefaultWriter = utils.LogWriter{Logger: log.Logger}
|
||||
router.Use(JSONLogger())
|
||||
} else {
|
||||
router.Use(gin.Logger())
|
||||
}
|
||||
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
if c.Config().EnableSwaggerEndpoint {
|
||||
router.GET("docs.html", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
})
|
||||
router.Use(redirectSwagger)
|
||||
url := ginSwagger.URL("/docs/doc.json")
|
||||
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
MetricsCollectorRegistrar.Register(router)
|
||||
}
|
||||
|
||||
if c.Config().ServeInAPIMode {
|
||||
router.GET("/repos/", reposListInAPIMode(c.Config().GetFileSystemPublishRoots()))
|
||||
router.GET("/repos/:storage/*pkgPath", reposServeInAPIMode)
|
||||
}
|
||||
|
||||
api := router.Group("/api")
|
||||
if context.Flags().Lookup("no-lock").Value.Get().(bool) {
|
||||
// We use a goroutine to count the number of
|
||||
// concurrent requests. When no more requests are
|
||||
// running, we close the database to free the lock.
|
||||
initDBRequests()
|
||||
|
||||
api.Use(func(c *gin.Context) {
|
||||
err := acquireDatabaseConnection()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := releaseDatabaseConnection()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
api.GET("/metrics", apiMetricsGet())
|
||||
}
|
||||
api.GET("/version", apiVersion)
|
||||
api.GET("/storage", apiDiskFree)
|
||||
|
||||
isReady := &atomic.Value{}
|
||||
isReady.Store(false)
|
||||
defer isReady.Store(true)
|
||||
api.GET("/ready", apiReady(isReady))
|
||||
api.GET("/healthy", apiHealthy)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/repos", apiReposList)
|
||||
api.POST("/repos", apiReposCreate)
|
||||
api.GET("/repos/:name", apiReposShow)
|
||||
api.PUT("/repos/:name", apiReposEdit)
|
||||
api.DELETE("/repos/:name", apiReposDrop)
|
||||
|
||||
api.GET("/repos/:name/packages", apiReposPackagesShow)
|
||||
api.POST("/repos/:name/packages", apiReposPackagesAdd)
|
||||
api.DELETE("/repos/:name/packages", apiReposPackagesDelete)
|
||||
|
||||
api.POST("/repos/:name/file/:dir/:file", apiReposPackageFromFile)
|
||||
api.POST("/repos/:name/file/:dir", apiReposPackageFromDir)
|
||||
api.POST("/repos/:name/copy/:src/:file", apiReposCopyPackage)
|
||||
|
||||
api.POST("/repos/:name/include/:dir/:file", apiReposIncludePackageFromFile)
|
||||
api.POST("/repos/:name/include/:dir", apiReposIncludePackageFromDir)
|
||||
|
||||
api.POST("/repos/:name/snapshots", apiSnapshotsCreateFromRepository)
|
||||
}
|
||||
|
||||
{
|
||||
api.POST("/mirrors/:name/snapshots", apiSnapshotsCreateFromMirror)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/mirrors", apiMirrorsList)
|
||||
api.GET("/mirrors/:name", apiMirrorsShow)
|
||||
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
api.POST("/mirrors", apiMirrorsCreate)
|
||||
api.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
api.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
}
|
||||
|
||||
{
|
||||
api.POST("/gpg/key", apiGPGAddKey)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/s3", apiS3List)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/files", apiFilesListDirs)
|
||||
api.POST("/files/:dir", apiFilesUpload)
|
||||
api.GET("/files/:dir", apiFilesListFiles)
|
||||
api.DELETE("/files/:dir", apiFilesDeleteDir)
|
||||
api.DELETE("/files/:dir/:name", apiFilesDeleteFile)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/publish", apiPublishList)
|
||||
api.GET("/publish/:prefix/:distribution", apiPublishShow)
|
||||
api.POST("/publish", apiPublishRepoOrSnapshot)
|
||||
api.POST("/publish/:prefix", apiPublishRepoOrSnapshot)
|
||||
api.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch)
|
||||
api.DELETE("/publish/:prefix/:distribution", apiPublishDrop)
|
||||
api.POST("/publish/:prefix/:distribution/sources", apiPublishAddSource)
|
||||
api.GET("/publish/:prefix/:distribution/sources", apiPublishListChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources", apiPublishSetSources)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources", apiPublishDropChanges)
|
||||
api.PUT("/publish/:prefix/:distribution/sources/:component", apiPublishUpdateSource)
|
||||
api.DELETE("/publish/:prefix/:distribution/sources/:component", apiPublishRemoveSource)
|
||||
api.POST("/publish/:prefix/:distribution/update", apiPublishUpdate)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/snapshots", apiSnapshotsList)
|
||||
api.POST("/snapshots", apiSnapshotsCreate)
|
||||
api.PUT("/snapshots/:name", apiSnapshotsUpdate)
|
||||
api.GET("/snapshots/:name", apiSnapshotsShow)
|
||||
api.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
|
||||
api.DELETE("/snapshots/:name", apiSnapshotsDrop)
|
||||
api.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
|
||||
api.POST("/snapshots/:name/merge", apiSnapshotsMerge)
|
||||
api.POST("/snapshots/:name/pull", apiSnapshotsPull)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/packages/:key", apiPackagesShow)
|
||||
api.GET("/packages", apiPackages)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/graph.:ext", apiGraph)
|
||||
}
|
||||
{
|
||||
api.POST("/db/cleanup", apiDBCleanup)
|
||||
}
|
||||
{
|
||||
api.GET("/tasks", apiTasksList)
|
||||
api.POST("/tasks-clear", apiTasksClear)
|
||||
api.GET("/tasks-wait", apiTasksWait)
|
||||
api.GET("/tasks/:id/wait", apiTasksWaitForTaskByID)
|
||||
api.GET("/tasks/:id/output", apiTasksOutputShow)
|
||||
api.GET("/tasks/:id/detail", apiTasksDetailShow)
|
||||
api.GET("/tasks/:id/return_value", apiTasksReturnValueShow)
|
||||
api.GET("/tasks/:id", apiTasksShow)
|
||||
api.DELETE("/tasks/:id", apiTasksDelete)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type RouterSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&RouterSuite{})
|
||||
|
||||
func (s *RouterSuite) TestRedirectSwagger(c *C) {
|
||||
// Test redirect from /docs to /docs/index.html
|
||||
response, _ := s.HTTPRequest("GET", "/docs", nil)
|
||||
c.Check(response.Code, Equals, 301)
|
||||
c.Check(response.Header().Get("Location"), Equals, "/docs/")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary S3 buckets
|
||||
// @Description **Get list of S3 buckets**
|
||||
// @Description
|
||||
// @Description List configured S3 buckets.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {array} string "List of S3 buckets"
|
||||
// @Router /api/s3 [get]
|
||||
func apiS3List(c *gin.Context) {
|
||||
keys := []string{}
|
||||
// Use safe accessor to get a copy of the map
|
||||
s3Roots := context.Config().GetS3PublishRoots()
|
||||
for k := range s3Roots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
c.JSON(200, keys)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type S3Suite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&S3Suite{})
|
||||
|
||||
func (s *S3Suite) TestS3List(c *C) {
|
||||
// Test listing S3 endpoints
|
||||
response, _ := s.HTTPRequest("GET", "/api/s3", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
+817
@@ -0,0 +1,817 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary List Snapshots
|
||||
// @Description **Get list of snapshots**
|
||||
// @Description
|
||||
// @Description Each snapshot is returned as in “show” API.
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.Snapshot
|
||||
// @Router /api/snapshots [get]
|
||||
func apiSnapshotsList(c *gin.Context) {
|
||||
SortMethodString := c.Request.URL.Query().Get("sort")
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
if SortMethodString == "" {
|
||||
SortMethodString = "name"
|
||||
}
|
||||
|
||||
result := []*deb.Snapshot{}
|
||||
_ = collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
result = append(result, snapshot)
|
||||
return nil
|
||||
})
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
type snapshotsCreateFromMirrorParams struct {
|
||||
// Name of snapshot to create
|
||||
Name string `binding:"required" json:"Name" example:"snap1"`
|
||||
// Description of snapshot
|
||||
Description string ` json:"Description"`
|
||||
}
|
||||
|
||||
// @Summary Snapshot Mirror
|
||||
// @Description **Create a snapshot of a mirror**
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Param request body snapshotsCreateFromMirrorParams true "Parameters"
|
||||
// @Param name path string true "Mirror name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 201 {object} deb.Snapshot "Created Snapshot"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Mirror Not Found"
|
||||
// @Failure 409 {object} Error "Conflicting snapshot"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/mirrors/{name}/snapshots [post]
|
||||
func apiSnapshotsCreateFromMirror(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
repo *deb.RemoteRepo
|
||||
snapshot *deb.Snapshot
|
||||
b snapshotsCreateFromMirrorParams
|
||||
)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
repo, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
// including snapshot resource key
|
||||
resources := []string{string(repo.Key()), "S" + b.Name}
|
||||
taskName := fmt.Sprintf("Create snapshot of mirror %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := repo.CheckLock()
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, err
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
snapshot, err = deb.NewSnapshotFromRepository(b.Name, repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
|
||||
if b.Description != "" {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: snapshot}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type snapshotsCreateParams struct {
|
||||
// Name of snapshot to create
|
||||
Name string `binding:"required" json:"Name" example:"snap2"`
|
||||
// Description of snapshot
|
||||
Description string ` json:"Description"`
|
||||
// List of source snapshots
|
||||
SourceSnapshots []string ` json:"SourceSnapshots" example:"snap1"`
|
||||
// List of package refs
|
||||
PackageRefs []string ` json:"PackageRefs" example:""`
|
||||
}
|
||||
|
||||
// @Summary Snapshot Packages
|
||||
// @Description **Create a snapshot from package refs**
|
||||
// @Description
|
||||
// @Description Refs can be obtained from snapshots, local repos, or mirrors
|
||||
// @Tags Snapshots
|
||||
// @Param request body snapshotsCreateParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.Snapshot "Created snapshot"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Source snapshot or package refs not found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots [post]
|
||||
func apiSnapshotsCreate(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
snapshot *deb.Snapshot
|
||||
b snapshotsCreateParams
|
||||
)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Description == "" {
|
||||
if len(b.SourceSnapshots)+len(b.PackageRefs) == 0 {
|
||||
b.Description = "Created as empty"
|
||||
}
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
var resources []string
|
||||
|
||||
sources := make([]*deb.Snapshot, len(b.SourceSnapshots))
|
||||
|
||||
for i := range b.SourceSnapshots {
|
||||
sources[i], err = snapshotCollection.ByName(b.SourceSnapshots[i])
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, string(sources[i].ResourceKey()))
|
||||
}
|
||||
|
||||
maybeRunTaskInBackground(c, "Create snapshot "+b.Name, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
for i := range sources {
|
||||
err = snapshotCollection.LoadComplete(sources[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
}
|
||||
|
||||
list := deb.NewPackageList()
|
||||
|
||||
// verify package refs and build package list
|
||||
for _, ref := range b.PackageRefs {
|
||||
p, err := collectionFactory.PackageCollection().ByKey([]byte(ref))
|
||||
if err != nil {
|
||||
if err == database.ErrNotFound {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, fmt.Errorf("package %s: %s", ref, err)
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = list.Add(p)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
}
|
||||
|
||||
snapshot = deb.NewSnapshotFromRefList(b.Name, sources, deb.NewPackageRefListFromPackageList(list), b.Description)
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: snapshot}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type snapshotsCreateFromRepositoryParams struct {
|
||||
// Name of snapshot to create
|
||||
Name string `binding:"required" json:"Name" example:"snap1"`
|
||||
// Description of snapshot
|
||||
Description string ` json:"Description"`
|
||||
}
|
||||
|
||||
// @Summary Snapshot Repository
|
||||
// @Description **Create a snapshot of a repository by name**
|
||||
// @Tags Snapshots
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body snapshotsCreateFromRepositoryParams true "Parameters"
|
||||
// @Param name path string true "Name of the snapshot"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.Snapshot "Created snapshot object"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Failure 404 {object} Error "Repo Not Found"
|
||||
// @Router /api/repos/{name}/snapshots [post]
|
||||
func apiSnapshotsCreateFromRepository(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
repo *deb.LocalRepo
|
||||
snapshot *deb.Snapshot
|
||||
b snapshotsCreateFromRepositoryParams
|
||||
)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
repo, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
// including snapshot resource key
|
||||
resources := []string{string(repo.Key()), "S" + b.Name}
|
||||
taskName := fmt.Sprintf("Create snapshot of repo %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err := collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
snapshot, err = deb.NewSnapshotFromLocalRepo(b.Name, repo)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusNotFound, Value: nil}, err
|
||||
}
|
||||
|
||||
if b.Description != "" {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = snapshotCollection.Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: snapshot}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type snapshotsUpdateParams struct {
|
||||
// Change Name of snapshot
|
||||
Name string ` json:"Name" example:"snap2"`
|
||||
// Change Description of snapshot
|
||||
Description string `json:"Description"`
|
||||
}
|
||||
|
||||
// @Summary Update Snapshot
|
||||
// @Description **Update snapshot metadata (Name, Description)**
|
||||
// @Tags Snapshots
|
||||
// @Param request body snapshotsUpdateParams true "Parameters"
|
||||
// @Param name path string true "Snapshot name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.Snapshot "Updated snapshot object"
|
||||
// @Failure 404 {object} Error "Snapshot Not Found"
|
||||
// @Failure 409 {object} Error "Conflicting snapshot"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots/{name} [put]
|
||||
func apiSnapshotsUpdate(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
snapshot *deb.Snapshot
|
||||
b snapshotsUpdateParams
|
||||
)
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
snapshot, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(snapshot.ResourceKey()), "S" + b.Name}
|
||||
taskName := fmt.Sprintf("Update snapshot %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
_, err := collection.ByName(b.Name)
|
||||
if err == nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to rename: snapshot %s already exists", b.Name)
|
||||
}
|
||||
|
||||
if b.Name != "" {
|
||||
snapshot.Name = b.Name
|
||||
}
|
||||
|
||||
if b.Description != "" {
|
||||
snapshot.Description = b.Description
|
||||
}
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Update(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: snapshot}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get Snapshot Info
|
||||
// @Description **Query detailed information about a snapshot by name**
|
||||
// @Tags Snapshots
|
||||
// @Param name path string true "Name of the snapshot"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.Snapshot "msg"
|
||||
// @Failure 404 {object} Error "Snapshot Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots/{name} [get]
|
||||
func apiSnapshotsShow(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
snapshot, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, snapshot)
|
||||
}
|
||||
|
||||
// @Summary Delete Snapshot
|
||||
// @Description **Delete snapshot by name**
|
||||
// @Description Cannot drop snapshots that are published.
|
||||
// @Description Needs force=1 to drop snapshots used as source by other snapshots.
|
||||
// @Tags Snapshots
|
||||
// @Param name path string true "Snapshot name"
|
||||
// @Param force query string false "Force operation"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Failure 404 {object} Error "Snapshot Not Found"
|
||||
// @Failure 409 {object} Error "Snapshot in use"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots/{name} [delete]
|
||||
func apiSnapshotsDrop(c *gin.Context) {
|
||||
name := c.Params.ByName("name")
|
||||
force := c.Request.URL.Query().Get("force") == "1"
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
publishedCollection := collectionFactory.PublishedRepoCollection()
|
||||
|
||||
snapshot, err := snapshotCollection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(snapshot.ResourceKey())}
|
||||
taskName := fmt.Sprintf("Delete snapshot %s", name)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
published := publishedCollection.BySnapshot(snapshot)
|
||||
|
||||
if len(published) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to drop: snapshot is published")
|
||||
}
|
||||
|
||||
if !force {
|
||||
snapshots := snapshotCollection.BySnapshotSource(snapshot)
|
||||
if len(snapshots) > 0 {
|
||||
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("won't delete snapshot that was used as source for other snapshots, use ?force=1 to override")
|
||||
}
|
||||
}
|
||||
|
||||
err = snapshotCollection.Drop(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: gin.H{}}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Snapshot diff
|
||||
// @Description **Return the diff between two snapshots (name & withSnapshot)**
|
||||
// @Description Provide `onlyMatching=1` to return only packages present in both snapshots.
|
||||
// @Description Otherwise, returns a `left` and `right` result providing packages only in the first and second snapshots
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Param name path string true "Snapshot name"
|
||||
// @Param withSnapshot path string true "Snapshot name to diff against"
|
||||
// @Param onlyMatching query string false "Only return packages present in both snapshots"
|
||||
// @Success 200 {array} deb.PackageDiff "Package Diff"
|
||||
// @Failure 404 {object} Error "Snapshot Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots/{name}/diff/{withSnapshot} [get]
|
||||
func apiSnapshotsDiff(c *gin.Context) {
|
||||
onlyMatching := c.Request.URL.Query().Get("onlyMatching") == "1"
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
snapshotA, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
snapshotB, err := collection.ByName(c.Params.ByName("withSnapshot"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(snapshotA)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(snapshotB)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate diff
|
||||
diff, err := snapshotA.RefList().Diff(snapshotB.RefList(), collectionFactory.PackageCollection())
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
result := []deb.PackageDiff{}
|
||||
|
||||
for _, pdiff := range diff {
|
||||
if onlyMatching && (pdiff.Left == nil || pdiff.Right == nil) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, pdiff)
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
// @Summary List Snapshot Packages
|
||||
// @Description **List all packages in snapshot or perform search on snapshot contents and return results**
|
||||
// @Description If `q` query parameter is missing, return all packages, otherwise return packages that match q
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Param name path string true "Snapshot to search"
|
||||
// @Param q query string false "Package query (e.g Name%20(~%20matlab))"
|
||||
// @Param withDeps query string false "Set to 1 to include dependencies when evaluating package query"
|
||||
// @Param format query string false "Set to 'details' to return extra info about each package"
|
||||
// @Param maximumVersion query string false "Set to 1 to only return the highest version for each package name"
|
||||
// @Success 200 {array} string "Package info"
|
||||
// @Failure 404 {object} Error "Snapshot Not Found"
|
||||
// @Failure 500 {object} Error "Internal Server Error"
|
||||
// @Router /api/snapshots/{name}/packages [get]
|
||||
func apiSnapshotsSearchPackages(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.SnapshotCollection()
|
||||
|
||||
snapshot, err := collection.ByName(c.Params.ByName("name"))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = collection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
showPackages(c, snapshot.RefList(), collectionFactory)
|
||||
}
|
||||
|
||||
type snapshotsMergeParams struct {
|
||||
// List of snapshot names to be merged
|
||||
Sources []string `binding:"required" json:"Sources" example:"snapshot1"`
|
||||
}
|
||||
|
||||
// @Summary Snapshot Merge
|
||||
// @Description **Merge several source snapshots into a new snapshot**
|
||||
// @Description
|
||||
// @Description Merge happens from left to right. By default, packages with the same name-architecture pair are replaced during merge (package from latest snapshot on the list wins).
|
||||
// @Description
|
||||
// @Description If only one snapshot is specified, merge copies source into destination.
|
||||
// @Tags Snapshots
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Param name path string true "Name of the snapshot to be created"
|
||||
// @Param latest query int false "merge only the latest version of each package"
|
||||
// @Param no-remove query int false "all versions of packages are preserved during merge"
|
||||
// @Param request body snapshotsMergeParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Success 201 {object} deb.Snapshot "Resulting snapshot object"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/snapshots/{name}/merge [post]
|
||||
func apiSnapshotsMerge(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
snapshot *deb.Snapshot
|
||||
body snapshotsMergeParams
|
||||
)
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
if c.Bind(&body) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(body.Sources) < 1 {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("minimum one source snapshot is required"))
|
||||
return
|
||||
}
|
||||
|
||||
latest := c.Request.URL.Query().Get("latest") == "1"
|
||||
noRemove := c.Request.URL.Query().Get("no-remove") == "1"
|
||||
overrideMatching := !latest && !noRemove
|
||||
|
||||
if noRemove && latest {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, fmt.Errorf("no-remove and latest are mutually exclusive"))
|
||||
return
|
||||
}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
snapshotCollection := collectionFactory.SnapshotCollection()
|
||||
|
||||
sources := make([]*deb.Snapshot, len(body.Sources))
|
||||
resources := make([]string, len(sources))
|
||||
for i := range body.Sources {
|
||||
sources[i], err = snapshotCollection.ByName(body.Sources[i])
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources[i] = string(sources[i].ResourceKey())
|
||||
}
|
||||
|
||||
maybeRunTaskInBackground(c, "Merge snapshot "+name, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = snapshotCollection.LoadComplete(sources[0])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
result := sources[0].RefList()
|
||||
for i := 1; i < len(sources); i++ {
|
||||
err = snapshotCollection.LoadComplete(sources[i])
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
result = result.Merge(sources[i].RefList(), overrideMatching, false)
|
||||
}
|
||||
|
||||
if latest {
|
||||
result.FilterLatestRefs()
|
||||
}
|
||||
|
||||
sourceDescription := make([]string, len(sources))
|
||||
for i, s := range sources {
|
||||
sourceDescription[i] = fmt.Sprintf("'%s'", s.Name)
|
||||
}
|
||||
|
||||
snapshot = deb.NewSnapshotFromRefList(name, sources, result,
|
||||
fmt.Sprintf("Merged from sources: %s", strings.Join(sourceDescription, ", ")))
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Add(snapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to create snapshot: %s", err)
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: snapshot}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type snapshotsPullParams struct {
|
||||
// Source name to be searched for packages and dependencies
|
||||
Source string `binding:"required" json:"Source" example:"source-snapshot"`
|
||||
// Name of the snapshot to be created
|
||||
Destination string `binding:"required" json:"Destination" example:"idestination-snapshot"`
|
||||
// List of package queries (i.e. name of package to be pulled from `Source`)
|
||||
Queries []string `binding:"required" json:"Queries" example:"xserver-xorg"`
|
||||
// List of architectures (optional)
|
||||
Architectures []string ` json:"Architectures" example:"amd64, armhf"`
|
||||
}
|
||||
|
||||
// @Summary Snapshot Pull
|
||||
// @Description **Pulls new packages and dependencies from a source snapshot into a new snapshot**
|
||||
// @Description
|
||||
// @Description May also upgrade package versions if name snapshot already contains packages being pulled. New snapshot `Destination` is created as result of this process.
|
||||
// @Description If architectures are limited (with config architectures or parameter `Architectures`, only mentioned architectures are processed, otherwise aptly will process all architectures in the snapshot.
|
||||
// @Description If following dependencies by source is enabled (using dependencyFollowSource config), pulling binary packages would also pull corresponding source packages as well.
|
||||
// @Description By default aptly would remove packages matching name and architecture while importing: e.g. when importing software_1.3_amd64, package software_1.2.9_amd64 would be removed.
|
||||
// @Description
|
||||
// @Description With flag `no-remove` both package versions would stay in the snapshot.
|
||||
// @Description
|
||||
// @Description Aptly pulls first package matching each of package queries, but with flag -all-matches all matching packages would be pulled.
|
||||
// @Tags Snapshots
|
||||
// @Param request body snapshotsPullParams true "Parameters"
|
||||
// @Param name path string true "Name of the snapshot to be created"
|
||||
// @Param all-matches query int false "pull all the packages that satisfy the dependency version requirements (default is to pull first matching package): 1 to enable"
|
||||
// @Param dry-run query int false "don’t create destination snapshot, just show what would be pulled: 1 to enable"
|
||||
// @Param no-deps query int false "don’t process dependencies, just pull listed packages: 1 to enable"
|
||||
// @Param no-remove query int false "don’t remove other package versions when pulling package: 1 to enable"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Consume json
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.Snapshot "Resulting Snapshot object"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/snapshots/{name}/pull [post]
|
||||
func apiSnapshotsPull(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
destinationSnapshot *deb.Snapshot
|
||||
body snapshotsPullParams
|
||||
)
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
|
||||
if err = c.BindJSON(&body); err != nil {
|
||||
AbortWithJSONError(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
allMatches := c.Request.URL.Query().Get("all-matches") == "1"
|
||||
dryRun := c.Request.URL.Query().Get("dry-run") == "1"
|
||||
noDeps := c.Request.URL.Query().Get("no-deps") == "1"
|
||||
noRemove := c.Request.URL.Query().Get("no-remove") == "1"
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// Load <name> snapshot
|
||||
toSnapshot, err := collectionFactory.SnapshotCollection().ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Load <Source> snapshot
|
||||
sourceSnapshot, err := collectionFactory.SnapshotCollection().ByName(body.Source)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
resources := []string{string(sourceSnapshot.ResourceKey()), string(toSnapshot.ResourceKey())}
|
||||
taskName := fmt.Sprintf("Pull snapshot %s into %s and save as %s", body.Source, name, body.Destination)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(toSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(sourceSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// convert snapshots to package list
|
||||
toPackageList, err := deb.NewPackageListFromRefList(toSnapshot.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
sourcePackageList, err := deb.NewPackageListFromRefList(sourceSnapshot.RefList(), collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
toPackageList.PrepareIndex()
|
||||
sourcePackageList.PrepareIndex()
|
||||
|
||||
var architecturesList []string
|
||||
|
||||
if len(context.ArchitecturesList()) > 0 {
|
||||
architecturesList = context.ArchitecturesList()
|
||||
} else {
|
||||
architecturesList = toPackageList.Architectures(false)
|
||||
}
|
||||
|
||||
architecturesList = append(architecturesList, body.Architectures...)
|
||||
sort.Strings(architecturesList)
|
||||
|
||||
if len(architecturesList) == 0 {
|
||||
err := fmt.Errorf("unable to determine list of architectures, please specify explicitly")
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
// Build architecture query: (arch == "i386" | arch == "amd64" | ...)
|
||||
var archQuery deb.PackageQuery = &deb.FieldQuery{Field: "$Architecture", Relation: deb.VersionEqual, Value: ""}
|
||||
for _, arch := range architecturesList {
|
||||
archQuery = &deb.OrQuery{L: &deb.FieldQuery{Field: "$Architecture", Relation: deb.VersionEqual, Value: arch}, R: archQuery}
|
||||
}
|
||||
|
||||
queries := make([]deb.PackageQuery, len(body.Queries))
|
||||
for i, q := range body.Queries {
|
||||
queries[i], err = query.Parse(q)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
// Add architecture filter
|
||||
queries[i] = &deb.AndQuery{L: queries[i], R: archQuery}
|
||||
}
|
||||
|
||||
// Filter with dependencies as requested
|
||||
destinationPackageList, err := sourcePackageList.Filter(deb.FilterOptions{
|
||||
Queries: queries,
|
||||
WithDependencies: !noDeps,
|
||||
Source: toPackageList,
|
||||
DependencyOptions: context.DependencyOptions(),
|
||||
Architectures: architecturesList,
|
||||
Progress: context.Progress(),
|
||||
})
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
destinationPackageList.PrepareIndex()
|
||||
|
||||
removedPackages := []string{}
|
||||
addedPackages := []string{}
|
||||
alreadySeen := map[string]bool{}
|
||||
|
||||
_ = destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
|
||||
key := pkg.Architecture + "_" + pkg.Name
|
||||
_, seen := alreadySeen[key]
|
||||
|
||||
// If we haven't seen such name-architecture pair and were instructed to remove, remove it
|
||||
if !noRemove && !seen {
|
||||
// Remove all packages with the same name and architecture
|
||||
packageSearchResults := toPackageList.Search(deb.Dependency{Architecture: pkg.Architecture, Pkg: pkg.Name}, true, false)
|
||||
for _, p := range packageSearchResults {
|
||||
toPackageList.Remove(p)
|
||||
removedPackages = append(removedPackages, p.String())
|
||||
}
|
||||
}
|
||||
|
||||
// If !allMatches, add only first matching name-arch package
|
||||
if !seen || allMatches {
|
||||
_ = toPackageList.Add(pkg)
|
||||
addedPackages = append(addedPackages, pkg.String())
|
||||
}
|
||||
|
||||
alreadySeen[key] = true
|
||||
|
||||
return nil
|
||||
})
|
||||
alreadySeen = nil
|
||||
|
||||
if dryRun {
|
||||
response := struct {
|
||||
AddedPackages []string `json:"added_packages"`
|
||||
RemovedPackages []string `json:"removed_packages"`
|
||||
}{
|
||||
AddedPackages: addedPackages,
|
||||
RemovedPackages: removedPackages,
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: response}, nil
|
||||
}
|
||||
|
||||
// Create <destination> snapshot
|
||||
destinationSnapshot = deb.NewSnapshotFromPackageList(body.Destination, []*deb.Snapshot{toSnapshot, sourceSnapshot}, toPackageList,
|
||||
fmt.Sprintf("Pulled into '%s' with '%s' as source, pull request was: '%s'", toSnapshot.Name, sourceSnapshot.Name, strings.Join(body.Queries, ", ")))
|
||||
|
||||
err = collectionFactory.SnapshotCollection().Add(destinationSnapshot)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
|
||||
}
|
||||
|
||||
return &task.ProcessReturnValue{Code: http.StatusCreated, Value: destinationSnapshot}, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type SnapshotAPITestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&SnapshotAPITestSuite{})
|
||||
|
||||
func (s *SnapshotAPITestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotShow(c *C) {
|
||||
// Test showing a specific snapshot
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/test-snapshot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotUpdate(c *C) {
|
||||
// Test updating a snapshot
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "updated-snapshot",
|
||||
Description: "Updated description",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("PUT", "/api/snapshots/test-snapshot", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotDrop(c *C) {
|
||||
// Test dropping a snapshot
|
||||
req, _ := http.NewRequest("DELETE", "/api/snapshots/test-snapshot", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotCreateFromRepository(c *C) {
|
||||
// Test creating a snapshot from repository
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "new-snapshot",
|
||||
Description: "Test snapshot",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/repos/test-repo/snapshots", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the repo doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotDiff(c *C) {
|
||||
// Test diffing two snapshots
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/snap1/diff/snap2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshots don't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotSearchPackages(c *C) {
|
||||
// Test searching packages in snapshot
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots/test-snapshot/packages?q=Name", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the snapshot doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotMerge(c *C) {
|
||||
// Test merging snapshots
|
||||
params := struct {
|
||||
Destination string `json:"Destination"`
|
||||
Sources []string `json:"Sources"`
|
||||
}{
|
||||
Destination: "merged-snapshot",
|
||||
Sources: []string{"snap1", "snap2"},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots/merge", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return error as snapshots don't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotPull(c *C) {
|
||||
// Test pulling packages between snapshots
|
||||
params := struct {
|
||||
Source string `json:"Source"`
|
||||
Destination string `json:"Destination"`
|
||||
Queries []string `json:"Queries"`
|
||||
}{
|
||||
Source: "source-snap",
|
||||
Destination: "dest-snap",
|
||||
Queries: []string{"Name (~ nginx)"},
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots/pull", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return error as snapshots don't exist
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotCreateFromMirror(c *C) {
|
||||
// Test creating snapshot from mirror
|
||||
params := struct {
|
||||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
}{
|
||||
Name: "mirror-snapshot",
|
||||
Description: "Snapshot from mirror",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(params)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will return 404 as the mirror doesn't exist
|
||||
c.Check(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListGet(c *C) {
|
||||
// Test GET /api/snapshots endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListWithSort(c *C) {
|
||||
// Test GET /api/snapshots with sort parameter
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots?sort=name", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListWithDifferentSorts(c *C) {
|
||||
// Test various sort methods
|
||||
sortMethods := []string{"name", "time", "created"}
|
||||
|
||||
for _, sortMethod := range sortMethods {
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots?sort="+sortMethod, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Sort method: %s", sortMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreatePost(c *C) {
|
||||
// Test POST /api/snapshots endpoint
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "test-snapshot",
|
||||
Description: "Test snapshot",
|
||||
SourceSnapshots: []string{"source1"},
|
||||
PackageRefs: []string{},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateInvalidJSON(c *C) {
|
||||
// Test POST with invalid JSON
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for invalid JSON
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateMissingName(c *C) {
|
||||
// Test POST with missing required name field
|
||||
requestBody := map[string]interface{}{
|
||||
"Description": "Test without name",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for missing name
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorPost(c *C) {
|
||||
// Test POST /api/mirrors/{name}/snapshots endpoint
|
||||
requestBody := snapshotsCreateFromMirrorParams{
|
||||
Name: "mirror-snapshot",
|
||||
Description: "Snapshot from mirror",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request without crashing (will likely error due to no context)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorInvalidJSON(c *C) {
|
||||
// Test POST with invalid JSON for mirror snapshot
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", strings.NewReader("invalid json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for invalid JSON
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorMissingName(c *C) {
|
||||
// Test POST with missing required name field for mirror snapshot
|
||||
requestBody := map[string]interface{}{
|
||||
"Description": "Mirror snapshot without name",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for missing name
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateWithAsync(c *C) {
|
||||
// Test POST with async parameter
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "async-snapshot",
|
||||
Description: "Async test snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots?_async=true", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorWithAsync(c *C) {
|
||||
// Test POST mirror snapshot with async parameter
|
||||
requestBody := snapshotsCreateFromMirrorParams{
|
||||
Name: "async-mirror-snapshot",
|
||||
Description: "Async mirror snapshot",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots?_async=true", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotsCreateParamsStruct(c *C) {
|
||||
// Test snapshotsCreateParams struct
|
||||
params := snapshotsCreateParams{
|
||||
Name: "test-name",
|
||||
Description: "test-description",
|
||||
SourceSnapshots: []string{"snap1", "snap2"},
|
||||
PackageRefs: []string{"ref1", "ref2"},
|
||||
}
|
||||
|
||||
c.Check(params.Name, Equals, "test-name")
|
||||
c.Check(params.Description, Equals, "test-description")
|
||||
c.Check(params.SourceSnapshots, DeepEquals, []string{"snap1", "snap2"})
|
||||
c.Check(params.PackageRefs, DeepEquals, []string{"ref1", "ref2"})
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestSnapshotsCreateFromMirrorParamsStruct(c *C) {
|
||||
// Test snapshotsCreateFromMirrorParams struct
|
||||
params := snapshotsCreateFromMirrorParams{
|
||||
Name: "mirror-test-name",
|
||||
Description: "mirror-test-description",
|
||||
}
|
||||
|
||||
c.Check(params.Name, Equals, "mirror-test-name")
|
||||
c.Check(params.Description, Equals, "mirror-test-description")
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateEmptyRequest(c *C) {
|
||||
// Test POST with empty request body
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for empty body
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateFromMirrorEmptyRequest(c *C) {
|
||||
// Test POST mirror snapshot with empty request body
|
||||
req, _ := http.NewRequest("POST", "/api/mirrors/test-mirror/snapshots", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 400) // Should return bad request for empty body
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListDefaultSort(c *C) {
|
||||
// Test that default sort is applied when no sort parameter provided
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Endpoint should handle default sort without issues
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateComplexPayload(c *C) {
|
||||
// Test POST with complex payload including all fields
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: "complex-snapshot",
|
||||
Description: "Complex test snapshot with multiple sources",
|
||||
SourceSnapshots: []string{"base-snapshot", "updates-snapshot", "security-snapshot"},
|
||||
PackageRefs: []string{"pkg1_1.0_amd64", "pkg2_2.0_i386", "pkg3_3.0_all"},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsHTTPMethods(c *C) {
|
||||
// Test that only allowed HTTP methods work
|
||||
|
||||
// Test unsupported methods for snapshots list
|
||||
deniedMethods := []string{"PUT", "DELETE", "PATCH"}
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method %s should not be allowed for snapshots list", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateSpecialCharacters(c *C) {
|
||||
// Test snapshot creation with special characters in names
|
||||
specialNames := []string{
|
||||
"snapshot-with-dashes",
|
||||
"snapshot_with_underscores",
|
||||
"snapshot.with.dots",
|
||||
"snapshot123",
|
||||
"UPPERCASESNAPSHOT",
|
||||
}
|
||||
|
||||
for _, name := range specialNames {
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: name,
|
||||
Description: "Test snapshot with special characters",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Special name test failed: %s", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsListEmptyResponse(c *C) {
|
||||
// Test snapshots list when no snapshots exist
|
||||
req, _ := http.NewRequest("GET", "/api/snapshots", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return some response (likely error due to no context, but shouldn't crash)
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsCreateWithoutContentType(c *C) {
|
||||
// Test POST without Content-Type header
|
||||
requestBody := `{"Name": "test-snapshot"}`
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", strings.NewReader(requestBody))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle missing content type
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
|
||||
func (s *SnapshotAPITestSuite) TestApiSnapshotsParameterEdgeCases(c *C) {
|
||||
// Test edge cases for parameter validation
|
||||
|
||||
// Test with very long name
|
||||
longName := strings.Repeat("a", 1000)
|
||||
requestBody := snapshotsCreateParams{
|
||||
Name: longName,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(requestBody)
|
||||
req, _ := http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
|
||||
// Test with empty arrays
|
||||
emptyArrayBody := snapshotsCreateParams{
|
||||
Name: "empty-arrays",
|
||||
SourceSnapshots: []string{},
|
||||
PackageRefs: []string{},
|
||||
}
|
||||
|
||||
jsonBody, _ = json.Marshal(emptyArrayBody)
|
||||
req, _ = http.NewRequest("POST", "/api/snapshots", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Not(Equals), 0)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type diskFree struct {
|
||||
// Storage size [MiB]
|
||||
Total uint64
|
||||
// Available Storage [MiB]
|
||||
Free uint64
|
||||
// Percentage Full
|
||||
PercentFull float32
|
||||
}
|
||||
|
||||
// @Summary Get Storage Utilization
|
||||
// @Description **Get disk free information of aptly storage**
|
||||
// @Description
|
||||
// @Description Units in MiB.
|
||||
// @Tags Status
|
||||
// @Produce json
|
||||
// @Success 200 {object} diskFree "Storage information"
|
||||
// @Failure 400 {object} Error "Internal Error"
|
||||
// @Router /api/storage [get]
|
||||
func apiDiskFree(c *gin.Context) {
|
||||
var df diskFree
|
||||
|
||||
fs := context.Config().GetRootDir()
|
||||
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs(fs, &stat)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("Error getting storage info on %s: %s", fs, err))
|
||||
return
|
||||
}
|
||||
|
||||
df.Total = uint64(stat.Blocks) * uint64(stat.Bsize) / 1048576
|
||||
df.Free = uint64(stat.Bavail) * uint64(stat.Bsize) / 1048576
|
||||
df.PercentFull = 100.0 - float32(stat.Bavail)/float32(stat.Blocks)*100.0
|
||||
|
||||
c.JSON(200, df)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type StorageTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&StorageTestSuite{})
|
||||
|
||||
func (s *StorageTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageListStructure(c *C) {
|
||||
// Test storage list endpoint structure
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return some storage information without error
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageHTTPMethods(c *C) {
|
||||
// Test that only GET method is allowed
|
||||
deniedMethods := []string{"POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range deniedMethods {
|
||||
req, _ := http.NewRequest(method, "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Method: %s should be denied", method))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageEndpointReliability(c *C) {
|
||||
// Test multiple calls to ensure endpoint is reliable
|
||||
for i := 0; i < 5; i++ {
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200, Commentf("Call #%d", i+1))
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StorageTestSuite) TestStorageResponseStructure(c *C) {
|
||||
// Test that response structure is consistent
|
||||
req, _ := http.NewRequest("GET", "/api/storage", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
|
||||
// Should have valid JSON response
|
||||
body := w.Body.String()
|
||||
c.Check(len(body), Not(Equals), 0)
|
||||
|
||||
// Should start with valid JSON structure
|
||||
c.Check(body[0], Equals, byte('{'), Commentf("Response should be JSON object"))
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/aptly-dev/aptly/task"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary List Tasks
|
||||
// @Description **Get list of available tasks. Each task is returned as in “show” API**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 {array} task.Task
|
||||
// @Router /api/tasks [get]
|
||||
func apiTasksList(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
c.JSON(200, list.GetTasks())
|
||||
}
|
||||
|
||||
// @Summary Clear Tasks
|
||||
// @Description **Removes finished and failed tasks from internal task list**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/tasks-clear [post]
|
||||
func apiTasksClear(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
list.Clear()
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// @Summary Wait for all Tasks
|
||||
// @Description **Waits for and returns when all running tasks are complete**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/tasks-wait [get]
|
||||
func apiTasksWait(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
list.Wait()
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
// @Summary Wait for Task
|
||||
// @Description **Waits for and returns when given Task ID is complete**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad id?"
|
||||
// @Failure 400 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/wait [get]
|
||||
func apiTasksWaitForTaskByID(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
task, err := list.WaitForTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, task)
|
||||
}
|
||||
|
||||
// @Summary Get Task Info
|
||||
// @Description **Return task information for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad id?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id} [get]
|
||||
func apiTasksShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var task task.Task
|
||||
task, err = list.GetTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, task)
|
||||
}
|
||||
|
||||
// @Summary Get Task Output
|
||||
// @Description **Return task output for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "Task output"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/output [get]
|
||||
func apiTasksOutputShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
output, err = list.GetTaskOutputByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, output)
|
||||
}
|
||||
|
||||
// @Summary Get Task Details
|
||||
// @Description **Return task detail for a given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "Task detail"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Task Not Found"
|
||||
// @Router /api/tasks/{id}/detail [get]
|
||||
func apiTasksDetailShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var detail interface{}
|
||||
detail, err = list.GetTaskDetailByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, detail)
|
||||
}
|
||||
|
||||
// @Summary Get Task Return Value
|
||||
// @Description **Return task return value (status code) by given ID**
|
||||
// @Tags Tasks
|
||||
// @Produce plain
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} string "msg"
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
// @Router /api/tasks/{id}/return_value [get]
|
||||
func apiTasksReturnValueShow(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := list.GetTaskReturnValueByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, output)
|
||||
}
|
||||
|
||||
// @Summary Delete Task
|
||||
// @Description **Delete completed task by given ID. Does not stop task execution**
|
||||
// @Tags Tasks
|
||||
// @Produce json
|
||||
// @Param id path int true "Task ID"
|
||||
// @Success 200 {object} task.Task
|
||||
// @Failure 500 {object} Error "invalid syntax, bad ID?"
|
||||
// @Failure 400 {object} Error "Task in progress or not found"
|
||||
// @Router /api/tasks/{id} [delete]
|
||||
func apiTasksDelete(c *gin.Context) {
|
||||
list := context.TaskList()
|
||||
id, err := strconv.ParseInt(c.Params.ByName("id"), 10, 0)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var delTask task.Task
|
||||
delTask, err = list.DeleteTaskByID(int(id))
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, delTask)
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type TaskTestSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&TaskTestSuite{})
|
||||
|
||||
func (s *TaskTestSuite) SetUpTest(c *C) {
|
||||
s.APISuite.SetUpTest(c)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksListEmpty(c *C) {
|
||||
// Test listing tasks when none exist
|
||||
req, _ := http.NewRequest("GET", "/api/tasks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Will likely return empty array due to no context, but tests structure
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksClearStructure(c *C) {
|
||||
// Test clearing tasks
|
||||
req, _ := http.NewRequest("POST", "/api/tasks-clear", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return empty object
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitStructure(c *C) {
|
||||
// Test waiting for all tasks
|
||||
req, _ := http.NewRequest("GET", "/api/tasks-wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 200)
|
||||
c.Check(w.Header().Get("Content-Type"), Equals, "application/json; charset=utf-8")
|
||||
|
||||
// Should return empty object after waiting
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitForTaskByIDStructure(c *C) {
|
||||
// Test waiting for specific task by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksWaitForTaskByIDInvalidID(c *C) {
|
||||
// Test waiting for task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/wait", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 400, Commentf("ID: -1 should return 400 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksShowStructure(c *C) {
|
||||
// Test showing specific task by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksShowInvalidID(c *C) {
|
||||
// Test showing task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
|
||||
// Test very large number separately - causes int overflow
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/999999999999999999999", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 500, Commentf("Very large number should return 500"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksOutputStructure(c *C) {
|
||||
// Test getting task output by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksOutputInvalidID(c *C) {
|
||||
// Test getting task output with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/output", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDetailStructure(c *C) {
|
||||
// Test getting task detail by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDetailInvalidID(c *C) {
|
||||
// Test getting task detail with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/detail", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksReturnValueStructure(c *C) {
|
||||
// Test getting task return value by ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/123/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksReturnValueInvalidID(c *C) {
|
||||
// Test getting task return value with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id+"/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/-1/return_value", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 404, Commentf("ID: -1 should return 404 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDeleteStructure(c *C) {
|
||||
// Test deleting task by ID
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/123", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Will error due to no context or invalid task, but tests structure
|
||||
c.Check(w.Code, Not(Equals), 200)
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksDeleteInvalidID(c *C) {
|
||||
// Test deleting task with invalid ID
|
||||
invalidIDs := []string{"invalid", "abc", "123.45"} // removed empty string as it causes redirect
|
||||
|
||||
for _, id := range invalidIDs {
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should return 500 for invalid ID format
|
||||
c.Check(w.Code, Equals, 500, Commentf("ID: %s", id))
|
||||
}
|
||||
|
||||
// Test negative ID separately - it's a valid int but invalid task ID
|
||||
req, _ := http.NewRequest("DELETE", "/api/tasks/-1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Check(w.Code, Equals, 400, Commentf("ID: -1 should return 400 (not found)"))
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksValidIDFormats(c *C) {
|
||||
// Test various valid ID formats
|
||||
validIDs := []string{"0", "1", "123", "999", "2147483647"} // Max int32
|
||||
|
||||
for _, id := range validIDs {
|
||||
// Test show endpoint
|
||||
req, _ := http.NewRequest("GET", "/api/tasks/"+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format), might be 404 (not found) or other error
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test wait endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/wait", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test output endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/output", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test detail endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/detail", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test return_value endpoint
|
||||
req, _ = http.NewRequest("GET", "/api/tasks/"+id+"/return_value", nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
|
||||
// Test delete endpoint
|
||||
req, _ = http.NewRequest("DELETE", "/api/tasks/"+id, nil)
|
||||
w = httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should not be 500 (invalid format)
|
||||
c.Check(w.Code, Not(Equals), 500, Commentf("ID: %s", id))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksParameterEdgeCases(c *C) {
|
||||
// Test edge cases in parameter handling
|
||||
edgeCases := []struct {
|
||||
path string
|
||||
description string
|
||||
}{
|
||||
{"/api/tasks/0", "zero ID"},
|
||||
{"/api/tasks/1", "single digit ID"},
|
||||
{"/api/tasks/2147483647", "max int32 ID"},
|
||||
{"/api/tasks/00123", "leading zeros"},
|
||||
{"/api/tasks/+123", "positive sign"},
|
||||
}
|
||||
|
||||
for _, tc := range edgeCases {
|
||||
req, _ := http.NewRequest("GET", tc.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle edge cases gracefully without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", tc.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksHTTPMethods(c *C) {
|
||||
// Test that correct HTTP methods are supported for each endpoint
|
||||
methodTests := []struct {
|
||||
path string
|
||||
allowedMethods []string
|
||||
deniedMethods []string
|
||||
}{
|
||||
{"/api/tasks", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks-clear", []string{"POST"}, []string{"GET", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks-wait", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123", []string{"GET", "DELETE"}, []string{"POST", "PUT", "PATCH"}},
|
||||
{"/api/tasks/123/wait", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/output", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/detail", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
{"/api/tasks/123/return_value", []string{"GET"}, []string{"POST", "PUT", "DELETE", "PATCH"}},
|
||||
}
|
||||
|
||||
for _, test := range methodTests {
|
||||
// Test denied methods return 404 (method not allowed for route)
|
||||
for _, method := range test.deniedMethods {
|
||||
req, _ := http.NewRequest(method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Check(w.Code, Equals, 404, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
}
|
||||
|
||||
// Test allowed methods are handled (may return errors but not method not allowed)
|
||||
for _, method := range test.allowedMethods {
|
||||
req, _ := http.NewRequest(method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should handle the request (200, 400, 404 for not found are OK)
|
||||
// Just ensure it's not 0 (no response) or 405 (method not allowed)
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
c.Check(w.Code, Not(Equals), 405, Commentf("Path: %s, Method: %s", test.path, method))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksContentTypes(c *C) {
|
||||
// Test content type handling for different endpoints
|
||||
contentTypeTests := []struct {
|
||||
path string
|
||||
method string
|
||||
expectedType string
|
||||
}{
|
||||
{"/api/tasks", "GET", "application/json"},
|
||||
{"/api/tasks-clear", "POST", "application/json"},
|
||||
{"/api/tasks-wait", "GET", "application/json"},
|
||||
{"/api/tasks/123", "GET", "application/json"},
|
||||
{"/api/tasks/123/wait", "GET", "application/json"},
|
||||
{"/api/tasks/123/output", "GET", ""}, // Text content
|
||||
{"/api/tasks/123/detail", "GET", "application/json"},
|
||||
{"/api/tasks/123/return_value", "GET", "application/json"},
|
||||
}
|
||||
|
||||
for _, test := range contentTypeTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
if test.expectedType != "" {
|
||||
// Check that JSON endpoints return JSON content type
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
c.Check(contentType, Matches, ".*"+test.expectedType+".*",
|
||||
Commentf("Path: %s, Expected: %s, Got: %s", test.path, test.expectedType, contentType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksErrorConditions(c *C) {
|
||||
// Test various error conditions
|
||||
errorTests := []struct {
|
||||
description string
|
||||
path string
|
||||
method string
|
||||
expectedErr bool
|
||||
}{
|
||||
{"Non-existent task ID", "/api/tasks/999999", "GET", true},
|
||||
{"Non-existent task wait", "/api/tasks/999999/wait", "GET", true},
|
||||
{"Non-existent task output", "/api/tasks/999999/output", "GET", true},
|
||||
{"Non-existent task detail", "/api/tasks/999999/detail", "GET", true},
|
||||
{"Non-existent task return value", "/api/tasks/999999/return_value", "GET", true},
|
||||
{"Non-existent task delete", "/api/tasks/999999", "DELETE", true},
|
||||
{"Tasks list endpoint", "/api/tasks", "GET", true}, // Valid endpoint
|
||||
{"Extra path segments", "/api/tasks/123/extra/segment", "GET", false}, // Route not matched
|
||||
}
|
||||
|
||||
for _, test := range errorTests {
|
||||
req, _ := http.NewRequest(test.method, test.path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// All should return some response without crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Test: %s", test.description))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TaskTestSuite) TestTasksResourceManagement(c *C) {
|
||||
// Test that endpoints handle resource management correctly
|
||||
endpoints := []string{
|
||||
"/api/tasks",
|
||||
"/api/tasks-clear",
|
||||
"/api/tasks-wait",
|
||||
"/api/tasks/1",
|
||||
"/api/tasks/1/wait",
|
||||
"/api/tasks/1/output",
|
||||
"/api/tasks/1/detail",
|
||||
"/api/tasks/1/return_value",
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
method := "GET"
|
||||
if endpoint == "/api/tasks-clear" {
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(method, endpoint, nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Should complete without hanging or crashing
|
||||
c.Check(w.Code, Not(Equals), 0, Commentf("Endpoint: %s", endpoint))
|
||||
|
||||
// Response should have proper headers
|
||||
c.Check(w.Header(), NotNil, Commentf("Endpoint: %s", endpoint))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"rootDir": "~/.aptly",
|
||||
"downloadConcurrency": 4,
|
||||
"downloadSpeedLimit": 0,
|
||||
"databaseOpenAttempts": 10,
|
||||
"architectures": ["amd64", "i386", "arm64"],
|
||||
"dependencyFollowSuggests": false,
|
||||
"dependencyFollowRecommends": false,
|
||||
"dependencyFollowAllVariants": false,
|
||||
"dependencyFollowSource": false,
|
||||
"gpgDisableSign": false,
|
||||
"gpgDisableVerify": false,
|
||||
"downloadSourcePackages": false,
|
||||
"ppaDistributorID": "ubuntu",
|
||||
"ppaCodename": "",
|
||||
"s3ConcurrentUploads": 4,
|
||||
"s3UploadQueueSize": 1000,
|
||||
"databaseBackend": {
|
||||
"type": "etcd",
|
||||
"url": "localhost:2379",
|
||||
"timeout": "120s",
|
||||
"writeRetries": 3,
|
||||
"writeQueue": {
|
||||
"enabled": true,
|
||||
"queueSize": 1000,
|
||||
"maxWritesPerSec": 100,
|
||||
"batchMaxSize": 50,
|
||||
"batchMaxWaitMs": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type AptlySuite struct{}
|
||||
|
||||
var _ = Suite(&AptlySuite{})
|
||||
|
||||
// Mock implementations for testing interfaces
|
||||
|
||||
type MockPackagePool struct {
|
||||
verifyFunc func(string, string, *utils.ChecksumInfo, ChecksumStorage) (string, bool, error)
|
||||
importFunc func(string, string, *utils.ChecksumInfo, bool, ChecksumStorage) (string, error)
|
||||
legacyPathFunc func(string, *utils.ChecksumInfo) (string, error)
|
||||
sizeFunc func(string) (int64, error)
|
||||
openFunc func(string) (ReadSeekerCloser, error)
|
||||
filepathListFunc func(Progress) ([]string, error)
|
||||
removeFunc func(string) (int64, error)
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, storage ChecksumStorage) (string, bool, error) {
|
||||
if m.verifyFunc != nil {
|
||||
return m.verifyFunc(poolPath, basename, checksums, storage)
|
||||
}
|
||||
return poolPath, true, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage ChecksumStorage) (string, error) {
|
||||
if m.importFunc != nil {
|
||||
return m.importFunc(srcPath, basename, checksums, move, storage)
|
||||
}
|
||||
return "imported/path/" + basename, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error) {
|
||||
if m.legacyPathFunc != nil {
|
||||
return m.legacyPathFunc(filename, checksums)
|
||||
}
|
||||
return "legacy/" + filename, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Size(path string) (int64, error) {
|
||||
if m.sizeFunc != nil {
|
||||
return m.sizeFunc(path)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Open(path string) (ReadSeekerCloser, error) {
|
||||
if m.openFunc != nil {
|
||||
return m.openFunc(path)
|
||||
}
|
||||
return &MockReadSeekerCloser{content: []byte("mock file content")}, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) FilepathList(progress Progress) ([]string, error) {
|
||||
if m.filepathListFunc != nil {
|
||||
return m.filepathListFunc(progress)
|
||||
}
|
||||
return []string{"file1.deb", "file2.deb"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPackagePool) Remove(path string) (int64, error) {
|
||||
if m.removeFunc != nil {
|
||||
return m.removeFunc(path)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
type MockReadSeekerCloser struct {
|
||||
content []byte
|
||||
pos int64
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Read(p []byte) (int, error) {
|
||||
if m.closed {
|
||||
return 0, errors.New("closed")
|
||||
}
|
||||
if m.pos >= int64(len(m.content)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, m.content[m.pos:])
|
||||
m.pos += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Seek(offset int64, whence int) (int64, error) {
|
||||
if m.closed {
|
||||
return 0, errors.New("closed")
|
||||
}
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
m.pos = offset
|
||||
case io.SeekCurrent:
|
||||
m.pos += offset
|
||||
case io.SeekEnd:
|
||||
m.pos = int64(len(m.content)) + offset
|
||||
}
|
||||
if m.pos < 0 {
|
||||
m.pos = 0
|
||||
}
|
||||
if m.pos > int64(len(m.content)) {
|
||||
m.pos = int64(len(m.content))
|
||||
}
|
||||
return m.pos, nil
|
||||
}
|
||||
|
||||
func (m *MockReadSeekerCloser) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type MockPublishedStorage struct {
|
||||
mkDirFunc func(string) error
|
||||
putFileFunc func(string, string) error
|
||||
removeDirsFunc func(string, Progress) error
|
||||
removeFunc func(string) error
|
||||
linkFromPoolFunc func(string, string, string, PackagePool, string, utils.ChecksumInfo, bool) error
|
||||
filelistFunc func(string) ([]string, error)
|
||||
renameFileFunc func(string, string) error
|
||||
symLinkFunc func(string, string) error
|
||||
hardLinkFunc func(string, string) error
|
||||
fileExistsFunc func(string) (bool, error)
|
||||
readLinkFunc func(string) (string, error)
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) MkDir(path string) error {
|
||||
if m.mkDirFunc != nil {
|
||||
return m.mkDirFunc(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) PutFile(path, sourceFilename string) error {
|
||||
if m.putFileFunc != nil {
|
||||
return m.putFileFunc(path, sourceFilename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) RemoveDirs(path string, progress Progress) error {
|
||||
if m.removeDirsFunc != nil {
|
||||
return m.removeDirsFunc(path, progress)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Remove(path string) error {
|
||||
if m.removeFunc != nil {
|
||||
return m.removeFunc(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool PackagePool, sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
if m.linkFromPoolFunc != nil {
|
||||
return m.linkFromPoolFunc(publishedPrefix, publishedRelPath, fileName, sourcePool, sourcePath, sourceChecksums, force)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
if m.filelistFunc != nil {
|
||||
return m.filelistFunc(prefix)
|
||||
}
|
||||
return []string{"file1", "file2"}, nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) RenameFile(oldName, newName string) error {
|
||||
if m.renameFileFunc != nil {
|
||||
return m.renameFileFunc(oldName, newName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) SymLink(src, dst string) error {
|
||||
if m.symLinkFunc != nil {
|
||||
return m.symLinkFunc(src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) HardLink(src, dst string) error {
|
||||
if m.hardLinkFunc != nil {
|
||||
return m.hardLinkFunc(src, dst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) FileExists(path string) (bool, error) {
|
||||
if m.fileExistsFunc != nil {
|
||||
return m.fileExistsFunc(path)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) ReadLink(path string) (string, error) {
|
||||
if m.readLinkFunc != nil {
|
||||
return m.readLinkFunc(path)
|
||||
}
|
||||
return "target", nil
|
||||
}
|
||||
|
||||
func (m *MockPublishedStorage) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type MockProgress struct {
|
||||
buffer bytes.Buffer
|
||||
started bool
|
||||
barStarted bool
|
||||
barProgress int
|
||||
}
|
||||
|
||||
func (m *MockProgress) Write(p []byte) (n int, err error) {
|
||||
return m.buffer.Write(p)
|
||||
}
|
||||
|
||||
func (m *MockProgress) Start() {
|
||||
m.started = true
|
||||
}
|
||||
|
||||
func (m *MockProgress) Shutdown() {
|
||||
m.started = false
|
||||
}
|
||||
|
||||
func (m *MockProgress) Flush() {
|
||||
// Nothing to do in mock
|
||||
}
|
||||
|
||||
func (m *MockProgress) InitBar(count int64, isBytes bool, barType BarType) {
|
||||
m.barStarted = true
|
||||
}
|
||||
|
||||
func (m *MockProgress) ShutdownBar() {
|
||||
m.barStarted = false
|
||||
}
|
||||
|
||||
func (m *MockProgress) AddBar(count int) {
|
||||
m.barProgress += count
|
||||
}
|
||||
|
||||
func (m *MockProgress) SetBar(count int) {
|
||||
m.barProgress = count
|
||||
}
|
||||
|
||||
func (m *MockProgress) Printf(msg string, a ...interface{}) {
|
||||
fmt.Fprintf(&m.buffer, msg, a...)
|
||||
}
|
||||
|
||||
func (m *MockProgress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
// Strip color codes for testing
|
||||
cleanMsg := strings.ReplaceAll(msg, "@r", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@g", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@y", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@!", "")
|
||||
cleanMsg = strings.ReplaceAll(cleanMsg, "@|", "")
|
||||
fmt.Fprintf(&m.buffer, cleanMsg, a...)
|
||||
}
|
||||
|
||||
func (m *MockProgress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
fmt.Fprintf(&m.buffer, "[STDERR] "+msg, a...)
|
||||
}
|
||||
|
||||
type MockDownloader struct {
|
||||
downloadFunc func(context.Context, string, string) error
|
||||
downloadWithChecksumFunc func(context.Context, string, string, *utils.ChecksumInfo, bool) error
|
||||
progress Progress
|
||||
getLengthFunc func(context.Context, string) (int64, error)
|
||||
}
|
||||
|
||||
func (m *MockDownloader) Download(ctx context.Context, url, destination string) error {
|
||||
if m.downloadFunc != nil {
|
||||
return m.downloadFunc(ctx, url, destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDownloader) DownloadWithChecksum(ctx context.Context, url, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error {
|
||||
if m.downloadWithChecksumFunc != nil {
|
||||
return m.downloadWithChecksumFunc(ctx, url, destination, expected, ignoreMismatch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockDownloader) GetProgress() Progress {
|
||||
if m.progress != nil {
|
||||
return m.progress
|
||||
}
|
||||
return &MockProgress{}
|
||||
}
|
||||
|
||||
func (m *MockDownloader) GetLength(ctx context.Context, url string) (int64, error) {
|
||||
if m.getLengthFunc != nil {
|
||||
return m.getLengthFunc(ctx, url)
|
||||
}
|
||||
return 1024, nil
|
||||
}
|
||||
|
||||
type MockChecksumStorage struct {
|
||||
getFunc func(string) (*utils.ChecksumInfo, error)
|
||||
updateFunc func(string, *utils.ChecksumInfo) error
|
||||
}
|
||||
|
||||
func (m *MockChecksumStorage) Get(path string) (*utils.ChecksumInfo, error) {
|
||||
if m.getFunc != nil {
|
||||
return m.getFunc(path)
|
||||
}
|
||||
return &utils.ChecksumInfo{}, nil
|
||||
}
|
||||
|
||||
func (m *MockChecksumStorage) Update(path string, c *utils.ChecksumInfo) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(path, c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test interfaces and their basic functionality
|
||||
|
||||
func (s *AptlySuite) TestPackagePoolInterface(c *C) {
|
||||
// Test PackagePool interface with mock implementation
|
||||
var pool PackagePool = &MockPackagePool{}
|
||||
|
||||
checksums := &utils.ChecksumInfo{}
|
||||
mockStorage := &MockChecksumStorage{}
|
||||
|
||||
// Test Verify
|
||||
path, exists, err := pool.Verify("test/path", "package.deb", checksums, mockStorage)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(path, Equals, "test/path")
|
||||
|
||||
// Test Import
|
||||
importedPath, err := pool.Import("/src/package.deb", "package.deb", checksums, false, mockStorage)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(importedPath, Equals, "imported/path/package.deb")
|
||||
|
||||
// Test LegacyPath
|
||||
legacyPath, err := pool.LegacyPath("package.deb", checksums)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(legacyPath, Equals, "legacy/package.deb")
|
||||
|
||||
// Test Size
|
||||
size, err := pool.Size("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(size, Equals, int64(1024))
|
||||
|
||||
// Test Open
|
||||
reader, err := pool.Open("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(reader, NotNil)
|
||||
reader.Close()
|
||||
|
||||
// Test FilepathList
|
||||
mockProgress := &MockProgress{}
|
||||
files, err := pool.FilepathList(mockProgress)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(files), Equals, 2)
|
||||
c.Check(files[0], Equals, "file1.deb")
|
||||
|
||||
// Test Remove
|
||||
removedSize, err := pool.Remove("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(removedSize, Equals, int64(1024))
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestPublishedStorageInterface(c *C) {
|
||||
// Test PublishedStorage interface with mock implementation
|
||||
var storage PublishedStorage = &MockPublishedStorage{}
|
||||
|
||||
// Test MkDir
|
||||
err := storage.MkDir("test/dir")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test PutFile
|
||||
err = storage.PutFile("dest/path", "source/file")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test RemoveDirs
|
||||
mockProgress := &MockProgress{}
|
||||
err = storage.RemoveDirs("test/dir", mockProgress)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Remove
|
||||
err = storage.Remove("test/file")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test LinkFromPool
|
||||
mockPool := &MockPackagePool{}
|
||||
checksums := utils.ChecksumInfo{}
|
||||
err = storage.LinkFromPool("prefix", "rel/path", "file.deb", mockPool, "pool/path", checksums, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Filelist
|
||||
files, err := storage.Filelist("prefix")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(len(files), Equals, 2)
|
||||
|
||||
// Test RenameFile
|
||||
err = storage.RenameFile("old", "new")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test SymLink
|
||||
err = storage.SymLink("src", "dst")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test HardLink
|
||||
err = storage.HardLink("src", "dst")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test FileExists
|
||||
exists, err := storage.FileExists("test/file")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
|
||||
// Test ReadLink
|
||||
target, err := storage.ReadLink("link")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(target, Equals, "target")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestProgressInterface(c *C) {
|
||||
// Test Progress interface with mock implementation
|
||||
var progress Progress = &MockProgress{}
|
||||
|
||||
// Test Start/Shutdown
|
||||
progress.Start()
|
||||
progress.Shutdown()
|
||||
|
||||
// Test Write
|
||||
n, err := progress.Write([]byte("test"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(n, Equals, 4)
|
||||
|
||||
// Test progress bar functions
|
||||
progress.InitBar(100, false, BarGeneralBuildPackageList)
|
||||
progress.AddBar(10)
|
||||
progress.SetBar(50)
|
||||
progress.ShutdownBar()
|
||||
|
||||
// Test Printf functions
|
||||
progress.Printf("test %s", "message")
|
||||
progress.ColoredPrintf("colored %s", "message")
|
||||
progress.PrintfStdErr("error %s", "message")
|
||||
|
||||
// Test Flush
|
||||
progress.Flush()
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestDownloaderInterface(c *C) {
|
||||
// Test Downloader interface with mock implementation
|
||||
var downloader Downloader = &MockDownloader{}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test Download
|
||||
err := downloader.Download(ctx, "http://example.com/file", "/tmp/dest")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test DownloadWithChecksum
|
||||
checksums := &utils.ChecksumInfo{}
|
||||
err = downloader.DownloadWithChecksum(ctx, "http://example.com/file", "/tmp/dest", checksums, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test GetProgress
|
||||
progress := downloader.GetProgress()
|
||||
c.Check(progress, NotNil)
|
||||
|
||||
// Test GetLength
|
||||
length, err := downloader.GetLength(ctx, "http://example.com/file")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(length, Equals, int64(1024))
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestChecksumStorageInterface(c *C) {
|
||||
// Test ChecksumStorage interface with mock implementation
|
||||
var storage ChecksumStorage = &MockChecksumStorage{}
|
||||
|
||||
// Test Get
|
||||
checksums, err := storage.Get("test/path")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(checksums, NotNil)
|
||||
|
||||
// Test Update
|
||||
newChecksums := &utils.ChecksumInfo{}
|
||||
err = storage.Update("test/path", newChecksums)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestConsoleResultReporter(c *C) {
|
||||
// Test ConsoleResultReporter implementation
|
||||
mockProgress := &MockProgress{}
|
||||
reporter := &ConsoleResultReporter{Progress: mockProgress}
|
||||
|
||||
// Test interface compliance
|
||||
var _ ResultReporter = reporter
|
||||
|
||||
// Test Warning
|
||||
reporter.Warning("test warning %s", "message")
|
||||
output := mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "test warning message"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[!]"), Equals, true)
|
||||
|
||||
// Reset buffer
|
||||
mockProgress.buffer.Reset()
|
||||
|
||||
// Test Removed
|
||||
reporter.Removed("removed %s", "item")
|
||||
output = mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "removed item"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[-]"), Equals, true)
|
||||
|
||||
// Reset buffer
|
||||
mockProgress.buffer.Reset()
|
||||
|
||||
// Test Added
|
||||
reporter.Added("added %s", "item")
|
||||
output = mockProgress.buffer.String()
|
||||
c.Check(strings.Contains(output, "added item"), Equals, true)
|
||||
c.Check(strings.Contains(output, "[+]"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestRecordingResultReporter(c *C) {
|
||||
// Test RecordingResultReporter implementation
|
||||
reporter := &RecordingResultReporter{
|
||||
Warnings: []string{},
|
||||
AddedLines: []string{},
|
||||
RemovedLines: []string{},
|
||||
}
|
||||
|
||||
// Test interface compliance
|
||||
var _ ResultReporter = reporter
|
||||
|
||||
// Test Warning
|
||||
reporter.Warning("test warning %s", "message")
|
||||
c.Check(len(reporter.Warnings), Equals, 1)
|
||||
c.Check(reporter.Warnings[0], Equals, "test warning message")
|
||||
|
||||
// Test Removed
|
||||
reporter.Removed("removed %s", "item")
|
||||
c.Check(len(reporter.RemovedLines), Equals, 1)
|
||||
c.Check(reporter.RemovedLines[0], Equals, "removed item")
|
||||
|
||||
// Test Added
|
||||
reporter.Added("added %s", "item")
|
||||
c.Check(len(reporter.AddedLines), Equals, 1)
|
||||
c.Check(reporter.AddedLines[0], Equals, "added item")
|
||||
|
||||
// Test multiple entries
|
||||
reporter.Warning("second warning")
|
||||
reporter.Added("second addition")
|
||||
c.Check(len(reporter.Warnings), Equals, 2)
|
||||
c.Check(len(reporter.AddedLines), Equals, 2)
|
||||
c.Check(reporter.Warnings[1], Equals, "second warning")
|
||||
c.Check(reporter.AddedLines[1], Equals, "second addition")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestReadSeekerCloserInterface(c *C) {
|
||||
// Test ReadSeekerCloser interface with mock implementation
|
||||
var rsc ReadSeekerCloser = &MockReadSeekerCloser{
|
||||
content: []byte("Hello, World!"),
|
||||
}
|
||||
|
||||
// Test Read
|
||||
buf := make([]byte, 5)
|
||||
n, err := rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(n, Equals, 5)
|
||||
c.Check(string(buf), Equals, "Hello")
|
||||
|
||||
// Test Seek
|
||||
pos, err := rsc.Seek(0, io.SeekStart)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(pos, Equals, int64(0))
|
||||
|
||||
// Test Read again from beginning
|
||||
n, err = rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(buf), Equals, "Hello")
|
||||
|
||||
// Test Seek to end
|
||||
pos, err = rsc.Seek(-6, io.SeekEnd)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(pos, Equals, int64(7))
|
||||
|
||||
// Test Read from near end
|
||||
buf = make([]byte, 10)
|
||||
n, err = rsc.Read(buf)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(string(buf[:n]), Equals, "World!")
|
||||
|
||||
// Test Close
|
||||
err = rsc.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Test Read after close (should error)
|
||||
_, err = rsc.Read(buf)
|
||||
c.Check(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestBarTypeConstants(c *C) {
|
||||
// Test BarType constants are defined and different
|
||||
barTypes := []BarType{
|
||||
BarGeneralBuildPackageList,
|
||||
BarGeneralVerifyDependencies,
|
||||
BarGeneralBuildFileList,
|
||||
BarCleanupBuildList,
|
||||
BarCleanupDeleteUnreferencedFiles,
|
||||
BarMirrorUpdateDownloadIndexes,
|
||||
BarMirrorUpdateDownloadPackages,
|
||||
BarMirrorUpdateBuildPackageList,
|
||||
BarMirrorUpdateImportFiles,
|
||||
BarMirrorUpdateFinalizeDownload,
|
||||
BarPublishGeneratePackageFiles,
|
||||
BarPublishFinalizeIndexes,
|
||||
}
|
||||
|
||||
// Check that all constants are different
|
||||
seen := make(map[BarType]bool)
|
||||
for _, barType := range barTypes {
|
||||
c.Check(seen[barType], Equals, false, Commentf("Duplicate BarType: %v", barType))
|
||||
seen[barType] = true
|
||||
}
|
||||
|
||||
// Check that they are sequential integers starting from 0
|
||||
for i, barType := range barTypes {
|
||||
c.Check(int(barType), Equals, i, Commentf("BarType not sequential: %v", barType))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestErrorHandling(c *C) {
|
||||
// Test error handling in mock implementations
|
||||
|
||||
// Test PackagePool with errors
|
||||
pool := &MockPackagePool{
|
||||
verifyFunc: func(string, string, *utils.ChecksumInfo, ChecksumStorage) (string, bool, error) {
|
||||
return "", false, errors.New("verify error")
|
||||
},
|
||||
importFunc: func(string, string, *utils.ChecksumInfo, bool, ChecksumStorage) (string, error) {
|
||||
return "", errors.New("import error")
|
||||
},
|
||||
}
|
||||
|
||||
_, _, err := pool.Verify("", "", nil, nil)
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "verify error")
|
||||
|
||||
_, err = pool.Import("", "", nil, false, nil)
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "import error")
|
||||
|
||||
// Test PublishedStorage with errors
|
||||
storage := &MockPublishedStorage{
|
||||
mkDirFunc: func(string) error {
|
||||
return errors.New("mkdir error")
|
||||
},
|
||||
fileExistsFunc: func(string) (bool, error) {
|
||||
return false, errors.New("file exists error")
|
||||
},
|
||||
}
|
||||
|
||||
err = storage.MkDir("test")
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "mkdir error")
|
||||
|
||||
_, err = storage.FileExists("test")
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "file exists error")
|
||||
}
|
||||
|
||||
func (s *AptlySuite) TestInterfaceCompatibility(c *C) {
|
||||
// Test that our mocks properly implement the interfaces
|
||||
|
||||
// PackagePool interface
|
||||
var _ PackagePool = &MockPackagePool{}
|
||||
|
||||
// PublishedStorage interface
|
||||
var _ PublishedStorage = &MockPublishedStorage{}
|
||||
|
||||
// Progress interface
|
||||
var _ Progress = &MockProgress{}
|
||||
|
||||
// Downloader interface
|
||||
var _ Downloader = &MockDownloader{}
|
||||
|
||||
// ChecksumStorage interface
|
||||
var _ ChecksumStorage = &MockChecksumStorage{}
|
||||
|
||||
// ReadSeekerCloser interface
|
||||
var _ ReadSeekerCloser = &MockReadSeekerCloser{}
|
||||
|
||||
// ResultReporter interface
|
||||
var _ ResultReporter = &ConsoleResultReporter{}
|
||||
var _ ResultReporter = &RecordingResultReporter{}
|
||||
|
||||
// Test that the interface checks pass
|
||||
c.Check(true, Equals, true)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package aptly
|
||||
|
||||
// AptlyConf holds the default aptly.conf (filled in at link time)
|
||||
var AptlyConf []byte
|
||||
@@ -0,0 +1,3 @@
|
||||
package aptly
|
||||
|
||||
var DistributionFocal = "focal"
|
||||
+106
-22
@@ -3,24 +3,62 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/utils"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
)
|
||||
|
||||
// ReadSeekerCloser = ReadSeeker + Closer
|
||||
type ReadSeekerCloser interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// PackagePool is asbtraction of package pool storage.
|
||||
//
|
||||
// PackagePool stores all the package files, deduplicating them.
|
||||
type PackagePool interface {
|
||||
// Path returns full path to package file in pool given any name and hash of file contents
|
||||
Path(filename string, hashMD5 string) (string, error)
|
||||
// RelativePath returns path relative to pool's root for package files given MD5 and original filename
|
||||
RelativePath(filename string, hashMD5 string) (string, error)
|
||||
// Verify checks whether file exists in the pool and fills back checksum info
|
||||
//
|
||||
// if poolPath is empty, poolPath is generated automatically based on checksum info (if available)
|
||||
// in any case, if function returns true, it also fills back checksums with complete information about the file in the pool
|
||||
Verify(poolPath, basename string, checksums *utils.ChecksumInfo, checksumStorage ChecksumStorage) (string, bool, error)
|
||||
// Import copies file into package pool
|
||||
//
|
||||
// - srcPath is full path to source file as it is now
|
||||
// - basename is desired human-readable name (canonical filename)
|
||||
// - checksums are used to calculate file placement
|
||||
// - move indicates whether srcPath can be removed
|
||||
Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage ChecksumStorage) (path string, err error)
|
||||
// LegacyPath returns legacy (pre 1.1) path to package file (relative to root)
|
||||
LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error)
|
||||
// Size returns the size of the given file in bytes.
|
||||
Size(path string) (size int64, err error)
|
||||
// Open returns ReadSeekerCloser to access the file
|
||||
Open(path string) (ReadSeekerCloser, error)
|
||||
// FilepathList returns file paths of all the files in the pool
|
||||
FilepathList(progress Progress) ([]string, error)
|
||||
// Remove deletes file in package pool returns its size
|
||||
Remove(path string) (size int64, err error)
|
||||
// Import copies file into package pool
|
||||
Import(path string, hashMD5 string) error
|
||||
}
|
||||
|
||||
// LocalPackagePool is implemented by PackagePools residing on the same filesystem
|
||||
type LocalPackagePool interface {
|
||||
// Stat returns Unix stat(2) info
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
// GenerateTempPath generates temporary path for download (which is fast to import into package pool later on)
|
||||
GenerateTempPath(filename string) (string, error)
|
||||
// Link generates hardlink to destination path
|
||||
Link(path, dstPath string) error
|
||||
// Symlink generates symlink to destination path
|
||||
Symlink(path, dstPath string) error
|
||||
// FullPath generates full path to the file in pool
|
||||
//
|
||||
// Please use with care: it's not supposed to be used to access files
|
||||
FullPath(path string) string
|
||||
}
|
||||
|
||||
// PublishedStorage is abstraction of filesystem storing all published repositories
|
||||
@@ -34,15 +72,25 @@ type PublishedStorage interface {
|
||||
// Remove removes single file under public path
|
||||
Remove(path string) error
|
||||
// LinkFromPool links package file from pool to dist's pool location
|
||||
LinkFromPool(publishedDirectory string, sourcePool PackagePool, sourcePath, sourceMD5 string, force bool) error
|
||||
LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool PackagePool, sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error
|
||||
// Filelist returns list of files under prefix
|
||||
Filelist(prefix string) ([]string, error)
|
||||
// RenameFile renames (moves) file
|
||||
RenameFile(oldName, newName string) error
|
||||
// SymLink creates a symbolic link, which can be read with ReadLink
|
||||
SymLink(src string, dst string) error
|
||||
// HardLink creates a hardlink of a file
|
||||
HardLink(src string, dst string) error
|
||||
// FileExists returns true if path exists
|
||||
FileExists(path string) (bool, error)
|
||||
// ReadLink returns the symbolic link pointed to by path
|
||||
ReadLink(path string) (string, error)
|
||||
// Flush waits for any pending operations to complete (used by concurrent upload implementations)
|
||||
Flush() error
|
||||
}
|
||||
|
||||
// LocalPublishedStorage is published storage on local filesystem
|
||||
type LocalPublishedStorage interface {
|
||||
// FileSystemPublishedStorage is published storage on filesystem
|
||||
type FileSystemPublishedStorage interface {
|
||||
// PublicPath returns root of public part
|
||||
PublicPath() string
|
||||
}
|
||||
@@ -53,6 +101,36 @@ type PublishedStorageProvider interface {
|
||||
GetPublishedStorage(name string) PublishedStorage
|
||||
}
|
||||
|
||||
// BarType used to differentiate between different progress bars
|
||||
type BarType int
|
||||
|
||||
const (
|
||||
// BarGeneralBuildPackageList identifies bar for building package list
|
||||
BarGeneralBuildPackageList BarType = iota
|
||||
// BarGeneralVerifyDependencies identifies bar for verifying dependencies
|
||||
BarGeneralVerifyDependencies
|
||||
// BarGeneralBuildFileList identifies bar for building file list
|
||||
BarGeneralBuildFileList
|
||||
// BarCleanupBuildList identifies bar for building list to cleanup
|
||||
BarCleanupBuildList
|
||||
// BarCleanupDeleteUnreferencedFiles identifies bar for deleting unreferenced files
|
||||
BarCleanupDeleteUnreferencedFiles
|
||||
// BarMirrorUpdateDownloadIndexes identifies bar for downloading index files
|
||||
BarMirrorUpdateDownloadIndexes
|
||||
// BarMirrorUpdateDownloadPackages identifies bar for downloading packages
|
||||
BarMirrorUpdateDownloadPackages
|
||||
// BarMirrorUpdateBuildPackageList identifies bar for building package list of downloaded files
|
||||
BarMirrorUpdateBuildPackageList
|
||||
// BarMirrorUpdateImportFiles identifies bar for importing package files
|
||||
BarMirrorUpdateImportFiles
|
||||
// BarMirrorUpdateFinalizeDownload identifies bar for finalizing downloads
|
||||
BarMirrorUpdateFinalizeDownload
|
||||
// BarPublishGeneratePackageFiles identifies bar for generating package files to publish
|
||||
BarPublishGeneratePackageFiles
|
||||
// BarPublishFinalizeIndexes identifies bar for finalizing index files
|
||||
BarPublishFinalizeIndexes
|
||||
)
|
||||
|
||||
// Progress is a progress displaying entity, it allows progress bars & simple prints
|
||||
type Progress interface {
|
||||
// Writer interface to support progress bar ticking
|
||||
@@ -64,7 +142,7 @@ type Progress interface {
|
||||
// Flush returns when all queued messages are sent
|
||||
Flush()
|
||||
// InitBar starts progressbar for count bytes or count items
|
||||
InitBar(count int64, isBytes bool)
|
||||
InitBar(count int64, isBytes bool, barType BarType)
|
||||
// ShutdownBar stops progress bar and hides it
|
||||
ShutdownBar()
|
||||
// AddBar increments progress for progress bar
|
||||
@@ -75,23 +153,29 @@ type Progress interface {
|
||||
Printf(msg string, a ...interface{})
|
||||
// ColoredPrintf does printf in colored way + newline
|
||||
ColoredPrintf(msg string, a ...interface{})
|
||||
// PrintfStdErr does printf but in safe manner to stderr
|
||||
PrintfStdErr(msg string, a ...interface{})
|
||||
}
|
||||
|
||||
// Downloader is parallel HTTP fetcher
|
||||
type Downloader interface {
|
||||
// Download starts new download task
|
||||
Download(url string, destination string, result chan<- error)
|
||||
Download(ctx context.Context, url string, destination string) error
|
||||
// DownloadWithChecksum starts new download task with checksum verification
|
||||
DownloadWithChecksum(url string, destination string, result chan<- error, expected utils.ChecksumInfo, ignoreMismatch bool)
|
||||
// Pause pauses task processing
|
||||
Pause()
|
||||
// Resume resumes task processing
|
||||
Resume()
|
||||
// Shutdown stops downloader after current tasks are finished,
|
||||
// but doesn't process rest of queue
|
||||
Shutdown()
|
||||
// Abort stops downloader without waiting for shutdown
|
||||
Abort()
|
||||
DownloadWithChecksum(ctx context.Context, url string, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error
|
||||
// GetProgress returns Progress object
|
||||
GetProgress() Progress
|
||||
// GetLength returns size by heading object with url
|
||||
GetLength(ctx context.Context, url string) (int64, error)
|
||||
}
|
||||
|
||||
// ChecksumStorageProvider creates ChecksumStorage based on DB
|
||||
type ChecksumStorageProvider func(db database.ReaderWriter) ChecksumStorage
|
||||
|
||||
// ChecksumStorage is stores checksums in some (persistent) storage
|
||||
type ChecksumStorage interface {
|
||||
// Get finds checksums in DB by path
|
||||
Get(path string) (*utils.ChecksumInfo, error)
|
||||
// Update adds or updates information about checksum in DB
|
||||
Update(path string, c *utils.ChecksumInfo) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type InterfacesSuite struct{}
|
||||
|
||||
var _ = Suite(&InterfacesSuite{})
|
||||
|
||||
func (s *InterfacesSuite) TestBarTypeValues(c *C) {
|
||||
// Test that BarType enum values are as expected
|
||||
c.Check(int(BarGeneralBuildPackageList), Equals, 0)
|
||||
c.Check(int(BarGeneralVerifyDependencies), Equals, 1)
|
||||
c.Check(int(BarGeneralBuildFileList), Equals, 2)
|
||||
c.Check(int(BarCleanupBuildList), Equals, 3)
|
||||
c.Check(int(BarCleanupDeleteUnreferencedFiles), Equals, 4)
|
||||
c.Check(int(BarMirrorUpdateDownloadIndexes), Equals, 5)
|
||||
c.Check(int(BarMirrorUpdateDownloadPackages), Equals, 6)
|
||||
c.Check(int(BarMirrorUpdateBuildPackageList), Equals, 7)
|
||||
c.Check(int(BarMirrorUpdateImportFiles), Equals, 8)
|
||||
c.Check(int(BarMirrorUpdateFinalizeDownload), Equals, 9)
|
||||
c.Check(int(BarPublishGeneratePackageFiles), Equals, 10)
|
||||
c.Check(int(BarPublishFinalizeIndexes), Equals, 11)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package aptly
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ResultReporter is abstraction for result reporting from complex processing functions
|
||||
type ResultReporter interface {
|
||||
// Warning is non-fatal error message
|
||||
Warning(msg string, a ...interface{})
|
||||
// Removed is signal that something has been removed
|
||||
Removed(msg string, a ...interface{})
|
||||
// Added is signal that something has been added
|
||||
Added(msg string, a ...interface{})
|
||||
}
|
||||
|
||||
// ConsoleResultReporter is implementation of ResultReporter that prints in colors to console
|
||||
type ConsoleResultReporter struct {
|
||||
Progress Progress
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ ResultReporter = &ConsoleResultReporter{}
|
||||
)
|
||||
|
||||
// Warning is non-fatal error message (yellow)
|
||||
func (c *ConsoleResultReporter) Warning(msg string, a ...interface{}) {
|
||||
c.Progress.ColoredPrintf("@y[!]@| @!"+msg+"@|", a...)
|
||||
}
|
||||
|
||||
// Removed is signal that something has been removed (red)
|
||||
func (c *ConsoleResultReporter) Removed(msg string, a ...interface{}) {
|
||||
c.Progress.ColoredPrintf("@r[-]@| "+msg, a...)
|
||||
}
|
||||
|
||||
// Added is signal that something has been added (green)
|
||||
func (c *ConsoleResultReporter) Added(msg string, a ...interface{}) {
|
||||
c.Progress.ColoredPrintf("@g[+]@| "+msg, a...)
|
||||
}
|
||||
|
||||
// RecordingResultReporter is implementation of ResultReporter that collects all messages
|
||||
type RecordingResultReporter struct {
|
||||
Warnings []string
|
||||
AddedLines []string `json:"Added"`
|
||||
RemovedLines []string `json:"Removed"`
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ ResultReporter = &RecordingResultReporter{}
|
||||
)
|
||||
|
||||
// Warning is non-fatal error message
|
||||
func (r *RecordingResultReporter) Warning(msg string, a ...interface{}) {
|
||||
r.Warnings = append(r.Warnings, fmt.Sprintf(msg, a...))
|
||||
}
|
||||
|
||||
// Removed is signal that something has been removed
|
||||
func (r *RecordingResultReporter) Removed(msg string, a ...interface{}) {
|
||||
r.RemovedLines = append(r.RemovedLines, fmt.Sprintf(msg, a...))
|
||||
}
|
||||
|
||||
// Added is signal that something has been added
|
||||
func (r *RecordingResultReporter) Added(msg string, a ...interface{}) {
|
||||
r.AddedLines = append(r.AddedLines, fmt.Sprintf(msg, a...))
|
||||
}
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package aptly
|
||||
|
||||
// Version of aptly
|
||||
const Version = "0.8"
|
||||
// Version of aptly (filled in at link time)
|
||||
var Version string
|
||||
|
||||
// Enable debugging features?
|
||||
// EnableDebug triggers some debugging features
|
||||
const EnableDebug = false
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package azure
|
||||
|
||||
// Package azure handles publishing to Azure Storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
)
|
||||
|
||||
func isBlobNotFound(err error) bool {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
return respErr.StatusCode == 404 // BlobNotFound
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type azContext struct {
|
||||
client *azblob.Client
|
||||
container string
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newAzContext(accountName, accountKey, container, prefix, endpoint string) (*azContext, error) {
|
||||
cred, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
|
||||
}
|
||||
|
||||
serviceClient, err := azblob.NewClientWithSharedKeyCredential(endpoint, cred, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &azContext{
|
||||
client: serviceClient,
|
||||
container: container,
|
||||
prefix: prefix,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (az *azContext) blobPath(path string) string {
|
||||
return filepath.Join(az.prefix, path)
|
||||
}
|
||||
|
||||
func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (paths []string, md5s []string, err error) {
|
||||
const delimiter = "/"
|
||||
paths = make([]string, 0, 1024)
|
||||
md5s = make([]string, 0, 1024)
|
||||
prefix = filepath.Join(az.prefix, prefix)
|
||||
if prefix != "" {
|
||||
prefix += delimiter
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
maxResults := int32(1)
|
||||
pager := az.client.NewListBlobsFlatPager(az.container, &azblob.ListBlobsFlatOptions{
|
||||
Prefix: &prefix,
|
||||
MaxResults: &maxResults,
|
||||
Include: azblob.ListBlobsInclude{Metadata: true},
|
||||
})
|
||||
|
||||
// Iterate over each page
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error listing under prefix %s in %s: %s", prefix, az, err)
|
||||
}
|
||||
|
||||
for _, blob := range page.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, *blob.Name)
|
||||
} else {
|
||||
name := *blob.Name
|
||||
paths = append(paths, name[len(prefix):])
|
||||
}
|
||||
b := *blob
|
||||
md5 := b.Properties.ContentMD5
|
||||
md5s = append(md5s, fmt.Sprintf("%x", md5))
|
||||
|
||||
}
|
||||
if progress != nil {
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
progress.AddBar(1)
|
||||
}
|
||||
}
|
||||
|
||||
return paths, md5s, nil
|
||||
}
|
||||
|
||||
func (az *azContext) putFile(blobName string, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := &azblob.UploadFileOptions{
|
||||
BlockSize: 4 * 1024 * 1024,
|
||||
Concurrency: 8,
|
||||
}
|
||||
|
||||
path := az.blobPath(blobName)
|
||||
if len(sourceMD5) > 0 {
|
||||
decodedMD5, err := hex.DecodeString(sourceMD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadOptions.HTTPHeaders = &blob.HTTPHeaders{
|
||||
BlobContentMD5: decodedMD5,
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if file, ok := source.(*os.File); ok {
|
||||
_, err = az.client.UploadFile(context.TODO(), az.container, path, file, uploadOptions)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// String
|
||||
func (az *azContext) String() string {
|
||||
return fmt.Sprintf("Azure: %s/%s", az.container, az.prefix)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Launch gocheck tests
|
||||
func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type PackagePool struct {
|
||||
az *azContext
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ aptly.PackagePool = (*PackagePool)(nil)
|
||||
)
|
||||
|
||||
// NewPackagePool creates published storage from Azure storage credentials
|
||||
func NewPackagePool(accountName, accountKey, container, prefix, endpoint string) (*PackagePool, error) {
|
||||
azctx, err := newAzContext(accountName, accountKey, container, prefix, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PackagePool{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
func (pool *PackagePool) String() string {
|
||||
return pool.az.String()
|
||||
}
|
||||
|
||||
func (pool *PackagePool) buildPoolPath(filename string, checksums *utils.ChecksumInfo) string {
|
||||
hash := checksums.SHA256
|
||||
// Use the same path as the file pool, for compat reasons.
|
||||
return filepath.Join(hash[0:2], hash[2:4], hash[4:32]+"_"+filename)
|
||||
}
|
||||
|
||||
func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.ChecksumStorage) (*utils.ChecksumInfo, error) {
|
||||
targetChecksums, err := checksumStorage.Get(poolPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if targetChecksums == nil {
|
||||
// we don't have checksums stored yet for this file
|
||||
download, err := pool.az.client.DownloadStream(context.Background(), pool.az.container, poolPath, nil)
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "error downloading blob at %s", poolPath)
|
||||
}
|
||||
|
||||
targetChecksums = &utils.ChecksumInfo{}
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error checksumming blob at %s", poolPath)
|
||||
}
|
||||
|
||||
err = checksumStorage.Update(poolPath, targetChecksums)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return targetChecksums, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) FilepathList(progress aptly.Progress) ([]string, error) {
|
||||
if progress != nil {
|
||||
progress.InitBar(0, false, aptly.BarGeneralBuildFileList)
|
||||
defer progress.ShutdownBar()
|
||||
}
|
||||
|
||||
paths, _, err := pool.az.internalFilelist("", progress)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
func (pool *PackagePool) LegacyPath(_ string, _ *utils.ChecksumInfo) (string, error) {
|
||||
return "", errors.New("Azure package pool does not support legacy paths")
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Size(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
temp, err := os.CreateTemp("", "blob-download")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating tempfile for %s", path)
|
||||
}
|
||||
defer func() { _ = os.Remove(temp.Name()) }()
|
||||
|
||||
_, err = pool.az.client.DownloadFile(context.TODO(), pool.az.container, path, temp, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error downloading blob %s", path)
|
||||
}
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Remove(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
}
|
||||
|
||||
_, err = pool.az.client.DeleteBlob(context.Background(), pool.az.container, path, nil)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error deleting %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, _ bool, checksumStorage aptly.ChecksumStorage) (string, error) {
|
||||
if checksums.MD5 == "" || checksums.SHA256 == "" || checksums.SHA512 == "" {
|
||||
// need to update checksums, MD5 and SHA256 should be always defined
|
||||
var err error
|
||||
*checksums, err = utils.ChecksumsForFile(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
path := pool.buildPoolPath(basename, checksums)
|
||||
targetChecksums, err := pool.ensureChecksums(path, checksumStorage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if targetChecksums != nil {
|
||||
// target already exists
|
||||
*checksums = *targetChecksums
|
||||
return path, nil
|
||||
}
|
||||
|
||||
source, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
err = pool.az.putFile(path, source, checksums.MD5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !checksums.Complete() {
|
||||
// need full checksums here
|
||||
*checksums, err = utils.ChecksumsForFile(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
err = checksumStorage.Update(path, checksums)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, checksumStorage aptly.ChecksumStorage) (string, bool, error) {
|
||||
if poolPath == "" {
|
||||
if checksums.SHA256 != "" {
|
||||
poolPath = pool.buildPoolPath(basename, checksums)
|
||||
} else {
|
||||
// No checksums or pool path, so no idea what file to look for.
|
||||
return "", false, nil
|
||||
}
|
||||
}
|
||||
|
||||
size, err := pool.Size(poolPath)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if size != checksums.Size {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
targetChecksums, err := pool.ensureChecksums(poolPath, checksumStorage)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if targetChecksums == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if checksums.MD5 != "" && targetChecksums.MD5 != checksums.MD5 ||
|
||||
checksums.SHA256 != "" && targetChecksums.SHA256 != checksums.SHA256 {
|
||||
// wrong file?
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// fill back checksums
|
||||
*checksums = *targetChecksums
|
||||
return poolPath, true, nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PackagePoolSuite struct {
|
||||
accountName, accountKey, endpoint string
|
||||
pool, prefixedPool *PackagePool
|
||||
debFile string
|
||||
cs aptly.ChecksumStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PackagePoolSuite{})
|
||||
|
||||
func (s *PackagePoolSuite) SetUpSuite(c *C) {
|
||||
s.accountName = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
if s.accountName == "" {
|
||||
println("Please set the the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCOUNT not set.")
|
||||
}
|
||||
s.accountKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
|
||||
if s.accountKey == "" {
|
||||
println("Please set the the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCESS_KEY not set.")
|
||||
}
|
||||
s.endpoint = os.Getenv("AZURE_STORAGE_ENDPOINT")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) SetUpTest(c *C) {
|
||||
container := randContainer()
|
||||
prefix := "lala"
|
||||
|
||||
var err error
|
||||
|
||||
s.pool, err = NewPackagePool(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, _File, _, _ := runtime.Caller(0)
|
||||
s.debFile = filepath.Join(filepath.Dir(_File), "../system/files/libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
s.cs = files.NewMockChecksumStorage()
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
list, err := s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
list, err = s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{
|
||||
"c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb",
|
||||
"c7/6b/4bd12fd92e4dfe1b55b18a67a669_b.deb",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestRemove(c *C) {
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
size, err := s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
_, err = s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
|
||||
list, err := s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"c7/6b/4bd12fd92e4dfe1b55b18a67a669_b.deb"})
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestImportOk(c *C) {
|
||||
var checksum utils.ChecksumInfo
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// SHA256 should be automatically calculated
|
||||
c.Check(checksum.SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
// checksum storage is filled with new checksum
|
||||
c.Check(s.cs.(*files.MockChecksumStorage).Store[path].SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
|
||||
size, err := s.pool.Size(path)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
// import as different name
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, "some.deb", &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_some.deb")
|
||||
// checksum storage is filled with new checksum
|
||||
c.Check(s.cs.(*files.MockChecksumStorage).Store[path].SHA256, Equals, "c76b4bd12fd92e4dfe1b55b18a67a669d92f62985d6a96c8a21d96120982cf12")
|
||||
|
||||
// double import, should be ok
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// clear checksum storage, and do double-import
|
||||
delete(s.cs.(*files.MockChecksumStorage).Store, path)
|
||||
checksum = utils.ChecksumInfo{}
|
||||
path, err = s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
// checksum is filled back based on re-calculation of file in the pool
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// import under new name, but with path-relevant checksums already filled in
|
||||
checksum = utils.ChecksumInfo{SHA256: checksum.SHA256}
|
||||
path, err = s.pool.Import(s.debFile, "other.deb", &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_other.deb")
|
||||
// checksum is filled back based on re-calculation of source file
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestVerify(c *C) {
|
||||
// file doesn't exist yet
|
||||
ppath, exists, err := s.pool.Verify("", filepath.Base(s.debFile), &utils.ChecksumInfo{}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// import file
|
||||
checksum := utils.ChecksumInfo{}
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &checksum, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(path, Equals, "c7/6b/4bd12fd92e4dfe1b55b18a67a669_libboost-program-options-dev_1.49.0.1_i386.deb")
|
||||
|
||||
// check existence
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, ppath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence with fixed path
|
||||
checksum = utils.ChecksumInfo{Size: checksum.Size}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, but with checksums missing (that aren't needed to find the path)
|
||||
checksum.SHA512 = ""
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with missing checksum info but correct path and size available
|
||||
checksum = utils.ChecksumInfo{Size: checksum.Size}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on checksum storage
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with wrong checksum info but correct path and size available
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &utils.ChecksumInfo{
|
||||
SHA256: "abc",
|
||||
Size: checksum.Size,
|
||||
}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// check existence, with missing checksums (that aren't needed to find the path)
|
||||
// and no info in checksum storage
|
||||
delete(s.cs.(*files.MockChecksumStorage).Store, path)
|
||||
checksum.SHA512 = ""
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, path)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
// checksum is filled back based on re-calculation
|
||||
c.Check(checksum.SHA512, Equals, "d7302241373da972aa9b9e71d2fd769b31a38f71182aa71bc0d69d090d452c69bb74b8612c002ccf8a89c279ced84ac27177c8b92d20f00023b3d268e6cec69c")
|
||||
|
||||
// check existence, with wrong size
|
||||
checksum = utils.ChecksumInfo{Size: 13455}
|
||||
ppath, exists, err = s.pool.Verify(path, filepath.Base(s.debFile), &checksum, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
|
||||
// check existence, with empty checksum info
|
||||
ppath, exists, err = s.pool.Verify("", filepath.Base(s.debFile), &utils.ChecksumInfo{}, s.cs)
|
||||
c.Check(ppath, Equals, "")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestImportNotExist(c *C) {
|
||||
_, err := s.pool.Import("no-such-file", "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, ErrorMatches, ".*no such file or directory")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestSize(c *C) {
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
size, err := s.pool.Size(path)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(2738))
|
||||
|
||||
_, err = s.pool.Size("do/es/ntexist")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestOpen(c *C) {
|
||||
path, err := s.pool.Import(s.debFile, filepath.Base(s.debFile), &utils.ChecksumInfo{}, false, s.cs)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
f, err := s.pool.Open(path)
|
||||
c.Assert(err, IsNil)
|
||||
contents, err := io.ReadAll(f)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(contents), Equals, 2738)
|
||||
c.Check(f.Close(), IsNil)
|
||||
|
||||
_, err = s.pool.Open("do/es/ntexist")
|
||||
c.Check(err, ErrorMatches, "(.|\n)*BlobNotFound(.|\n)*")
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PublishedStorage abstract file system with published files (actually hosted on Azure)
|
||||
type PublishedStorage struct {
|
||||
// FIXME: unused ???? prefix string
|
||||
az *azContext
|
||||
pathCache map[string]map[string]string
|
||||
}
|
||||
|
||||
// Check interface
|
||||
var (
|
||||
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
)
|
||||
|
||||
// NewPublishedStorage creates published storage from Azure storage credentials
|
||||
func NewPublishedStorage(accountName, accountKey, container, prefix, endpoint string) (*PublishedStorage, error) {
|
||||
azctx, err := newAzContext(accountName, accountKey, container, prefix, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PublishedStorage{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
func (storage *PublishedStorage) String() string {
|
||||
return storage.az.String()
|
||||
}
|
||||
|
||||
// MkDir creates directory recursively under public path
|
||||
func (storage *PublishedStorage) MkDir(_ string) error {
|
||||
// no op for Azure
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutFile puts file into published storage at specified path
|
||||
func (storage *PublishedStorage) PutFile(path string, sourceFilename string) error {
|
||||
var (
|
||||
source *os.File
|
||||
err error
|
||||
)
|
||||
|
||||
sourceMD5, err := utils.MD5ChecksumForFile(sourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err = os.Open(sourceFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
err = storage.az.putFile(path, source, sourceMD5)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, storage))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveDirs removes directory structure under public path
|
||||
func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
||||
path = storage.az.blobPath(path)
|
||||
filelist, err := storage.Filelist(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filename := range filelist {
|
||||
blob := filepath.Join(path, filename)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, blob, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting path %s from %s: %s", filename, storage, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes single file under public path
|
||||
func (storage *PublishedStorage) Remove(path string) error {
|
||||
path = storage.az.blobPath(path)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, path, nil)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error deleting %s from %s: %s", path, storage, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LinkFromPool links package file from pool to dist's pool location
|
||||
//
|
||||
// publishedPrefix is desired prefix for the location in the pool.
|
||||
// publishedRelPath is desired location in pool (like pool/component/liba/libav/)
|
||||
// sourcePool is instance of aptly.PackagePool
|
||||
// sourcePath is filepath to package file in package pool
|
||||
//
|
||||
// LinkFromPool returns relative path for the published file to be included in package index
|
||||
func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath, fileName string, sourcePool aptly.PackagePool,
|
||||
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
|
||||
relFilePath := filepath.Join(publishedRelPath, fileName)
|
||||
prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
poolPath := storage.az.blobPath(prefixRelFilePath)
|
||||
|
||||
if storage.pathCache == nil {
|
||||
storage.pathCache = make(map[string]map[string]string)
|
||||
}
|
||||
pathCache := storage.pathCache[publishedPrefix]
|
||||
if pathCache == nil {
|
||||
paths, md5s, err := storage.az.internalFilelist(publishedPrefix, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error caching paths under prefix: %s", err)
|
||||
}
|
||||
|
||||
pathCache = make(map[string]string, len(paths))
|
||||
|
||||
for i := range paths {
|
||||
pathCache[paths[i]] = md5s[i]
|
||||
}
|
||||
storage.pathCache[publishedPrefix] = pathCache
|
||||
}
|
||||
|
||||
destinationMD5, exists := pathCache[relFilePath]
|
||||
sourceMD5 := sourceChecksums.MD5
|
||||
|
||||
if exists {
|
||||
if sourceMD5 == "" {
|
||||
return fmt.Errorf("unable to compare object, MD5 checksum missing")
|
||||
}
|
||||
|
||||
if destinationMD5 == sourceMD5 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !force && destinationMD5 != sourceMD5 {
|
||||
return fmt.Errorf("error putting file to %s: file already exists and is different: %s", poolPath, storage)
|
||||
}
|
||||
}
|
||||
|
||||
source, err := sourcePool.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
err = storage.az.putFile(relFilePath, source, sourceMD5)
|
||||
if err == nil {
|
||||
pathCache[relFilePath] = sourceMD5
|
||||
} else {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s: %s", sourcePath, storage, poolPath))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Filelist returns list of files under prefix
|
||||
func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
paths, _, err := storage.az.internalFilelist(prefix, nil)
|
||||
return paths, err
|
||||
}
|
||||
|
||||
// Internal copy or move implementation
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata map[string]*string, move bool) error {
|
||||
const leaseDuration = 30
|
||||
leaseID := uuid.NewString()
|
||||
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
srcBlobClient := containerClient.NewBlobClient(src)
|
||||
blobLeaseClient, err := lease.NewBlobClient(srcBlobClient, &lease.BlobClientOptions{LeaseID: to.Ptr(leaseID)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
}
|
||||
|
||||
_, err = blobLeaseClient.AcquireLease(context.Background(), leaseDuration, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = blobLeaseClient.BreakLease(context.Background(), &lease.BlobBreakOptions{BreakPeriod: to.Ptr(int32(60))})
|
||||
}()
|
||||
|
||||
dstBlobClient := containerClient.NewBlobClient(dst)
|
||||
copyResp, err := dstBlobClient.StartCopyFromURL(context.Background(), srcBlobClient.URL(), &blob.StartCopyFromURLOptions{
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", src, dst, storage, err)
|
||||
}
|
||||
|
||||
copyStatus := *copyResp.CopyStatus
|
||||
for {
|
||||
if copyStatus == blob.CopyStatusTypeSuccess {
|
||||
if move {
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, src, &blob.DeleteOptions{
|
||||
AccessConditions: &blob.AccessConditions{
|
||||
LeaseAccessConditions: &blob.LeaseAccessConditions{
|
||||
LeaseID: &leaseID,
|
||||
},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if copyStatus == blob.CopyStatusTypePending {
|
||||
time.Sleep(1 * time.Second)
|
||||
getMetadata, err := dstBlobClient.GetProperties(context.TODO(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting copy progress %s", dst)
|
||||
}
|
||||
copyStatus = *getMetadata.CopyStatus
|
||||
|
||||
_, err = blobLeaseClient.RenewLease(context.Background(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error renewing source blob lease %s", src)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", dst, src, storage, copyStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RenameFile renames (moves) file
|
||||
func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
return storage.internalCopyOrMoveBlob(oldName, newName, nil, true /* move */)
|
||||
}
|
||||
|
||||
// SymLink creates a copy of src file and adds link information as meta data
|
||||
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
||||
metadata := make(map[string]*string)
|
||||
metadata["SymLink"] = &src
|
||||
return storage.internalCopyOrMoveBlob(src, dst, metadata, false /* do not remove src */)
|
||||
}
|
||||
|
||||
// HardLink using symlink functionality as hard links do not exist
|
||||
func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
||||
return storage.SymLink(src, dst)
|
||||
}
|
||||
|
||||
// FileExists returns true if path exists
|
||||
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("error checking if blob %s exists: %v", path, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ReadLink returns the symbolic link pointed to by path.
|
||||
// This simply reads text file created with SymLink
|
||||
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
props, err := blobClient.GetProperties(context.Background(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get blob properties: %v", err)
|
||||
}
|
||||
|
||||
metadata := props.Metadata
|
||||
if originalBlob, exists := metadata["original_blob"]; exists {
|
||||
return *originalBlob, nil
|
||||
}
|
||||
return "", fmt.Errorf("error reading link %s: %v", path, err)
|
||||
}
|
||||
|
||||
// Flush is a no-op for Azure storage
|
||||
func (storage *PublishedStorage) Flush() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
accountName, accountKey, endpoint string
|
||||
storage, prefixedStorage *PublishedStorage
|
||||
}
|
||||
|
||||
var _ = Suite(&PublishedStorageSuite{})
|
||||
|
||||
const testContainerPrefix = "aptlytest-"
|
||||
|
||||
func randContainer() string {
|
||||
return testContainerPrefix + randString(32-len(testContainerPrefix))
|
||||
}
|
||||
|
||||
func randString(n int) string {
|
||||
if n <= 0 {
|
||||
panic("negative number")
|
||||
}
|
||||
const alphanum = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
var bytes = make([]byte, n)
|
||||
_, _ = rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpSuite(c *C) {
|
||||
s.accountName = os.Getenv("AZURE_STORAGE_ACCOUNT")
|
||||
if s.accountName == "" {
|
||||
println("Please set the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCOUNT not set.")
|
||||
}
|
||||
s.accountKey = os.Getenv("AZURE_STORAGE_ACCESS_KEY")
|
||||
if s.accountKey == "" {
|
||||
println("Please set the following two environment variables to run the Azure storage tests.")
|
||||
println(" 1. AZURE_STORAGE_ACCOUNT")
|
||||
println(" 2. AZURE_STORAGE_ACCESS_KEY")
|
||||
c.Skip("AZURE_STORAGE_ACCESS_KEY not set.")
|
||||
}
|
||||
s.endpoint = os.Getenv("AZURE_STORAGE_ENDPOINT")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
container := randContainer()
|
||||
prefix := "lala"
|
||||
|
||||
var err error
|
||||
|
||||
s.storage, err = NewPublishedStorage(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
c.Assert(err, IsNil)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
c.Assert(err, IsNil)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
c.Assert(err, NotNil)
|
||||
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(storageError.StatusCode, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
hash := md5.Sum(data)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
content := []byte("Welcome to Azure!")
|
||||
filename := "a/b.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filename), DeepEquals, content)
|
||||
|
||||
err = s.prefixedStorage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filepath.Join(s.prefixedStorage.az.prefix, filename)), DeepEquals, content)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFilePlus(c *C) {
|
||||
content := []byte("Welcome to Azure!")
|
||||
filename := "a/b+c.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, filename), DeepEquals, content)
|
||||
s.AssertNoFile(c, "a/b c.txt")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "test/a", "test/b", "testa"})
|
||||
|
||||
list, err = s.storage.Filelist("test")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b"})
|
||||
|
||||
list, err = s.storage.Filelist("test2")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
list, err = s.prefixedStorage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelistPlus(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a+1", "test/a 1", "lala/a+b", "lala/a b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a b", "lala/a+b", "lala/c", "test/a 1", "test/a+1", "testa"})
|
||||
|
||||
list, err = s.storage.Filelist("test")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a 1", "a+1"})
|
||||
|
||||
list, err = s.storage.Filelist("test2")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
list, err = s.prefixedStorage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a b", "a+b", "c"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.Remove("a/b")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b")
|
||||
|
||||
s.PutFile(c, "lala/xyz", []byte("test"))
|
||||
|
||||
err = s.prefixedStorage.Remove("xyz")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "lala/xyz")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemovePlus(c *C) {
|
||||
s.PutFile(c, "a/b+c", []byte("test"))
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.Remove("a/b+c")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b+c")
|
||||
s.AssertNoFile(c, "a/b c")
|
||||
|
||||
err = s.storage.Remove("a/b")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
s.AssertNoFile(c, "a/b")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirs(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a", "test/b", "lala/a", "lala/b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
err := s.storage.RemoveDirs("test", nil)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a", "lala/b", "lala/c", "testa"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsPlus(c *C) {
|
||||
paths := []string{"a", "b", "c", "testa", "test/a+1", "test/a 1", "lala/a+b", "lala/a b", "lala/c"}
|
||||
for _, path := range paths {
|
||||
s.PutFile(c, path, []byte("test"))
|
||||
}
|
||||
|
||||
err := s.storage.RemoveDirs("test", nil)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
list, err := s.storage.Filelist("")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{"a", "b", "c", "lala/a b", "lala/a+b", "lala/c", "testa"})
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("source.txt", filepath.Join(dir, "a"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = s.storage.RenameFile("source.txt", "dest.txt")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "dest.txt"), DeepEquals, []byte("Welcome to Azure!"))
|
||||
|
||||
exists, err := s.storage.FileExists("source.txt")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
root := c.MkDir()
|
||||
pool := files.NewPackagePool(root, false)
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err := os.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum1 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err = os.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum2 := utils.ChecksumInfo{MD5: "e9dfd31cc505d51fc26975250750deab"}
|
||||
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "netboot/boot.img.gz")
|
||||
_ = os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = os.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum3 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
src1, err := pool.Import(tmpFile1, "mars-invaders_1.03.deb", &cksum1, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src2, err := pool.Import(tmpFile2, "mars-invaders_1.03.deb", &cksum2, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
src3, err := pool.Import(tmpFile3, "netboot/boot.img.gz", &cksum3, true, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// first link from pool
|
||||
err = s.storage.LinkFromPool("", filepath.Join("", "pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// duplicate link from pool
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, false)
|
||||
c.Check(err, ErrorMatches, ".*file already exists and is different.*")
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with conflict and force
|
||||
err = s.storage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src2, cksum2, true)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Spam"))
|
||||
|
||||
// for prefixed storage:
|
||||
// first link from pool
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, src1, cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// 2nd link from pool, providing wrong path for source file
|
||||
//
|
||||
// this test should check that file already exists in Azure and skip upload (which would fail if not skipped)
|
||||
s.prefixedStorage.pathCache = nil
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, "wrong-looks-like-pathcache-doesnt-work", cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "lala/pool/main/m/mars-invaders/mars-invaders_1.03.deb"), DeepEquals, []byte("Contents"))
|
||||
|
||||
// link from pool with nested file name
|
||||
err = s.storage.LinkFromPool("", "dists/jessie/non-free/installer-i386/current/images", "netboot/boot.img.gz", pool, src3, cksum3, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(s.GetFile(c, "dists/jessie/non-free/installer-i386/current/images/netboot/boot.img.gz"), DeepEquals, []byte("Contents"))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestSymLink(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
err := s.storage.SymLink("a/b", "a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
|
||||
var link string
|
||||
link, err = s.storage.ReadLink("a/b.link")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(link, Equals, "a/b")
|
||||
|
||||
c.Skip("copy not available in azure test")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFileExists(c *C) {
|
||||
s.PutFile(c, "a/b", []byte("test"))
|
||||
|
||||
exists, err := s.storage.FileExists("a/b")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, true)
|
||||
|
||||
exists, _ = s.storage.FileExists("a/b.invalid")
|
||||
c.Check(err, IsNil)
|
||||
c.Check(exists, Equals, false)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdAPI() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "api",
|
||||
Short: "start API server/issue requests",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdAPIServe(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
stdcontext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/api"
|
||||
"github.com/aptly-dev/aptly/systemd/activation"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyAPIServe(cmd *commander.Command, args []string) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
// There are only two working options for aptly's rootDir:
|
||||
// 1. rootDir does not exist, then we'll create it
|
||||
// 2. rootDir exists and is writable
|
||||
// anything else must fail.
|
||||
// E.g.: Running the service under a different user may lead to a rootDir
|
||||
// that exists but is not usable due to access permissions.
|
||||
err = utils.DirIsAccessible(context.Config().GetRootDir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to recycle systemd fds for listening
|
||||
listeners, err := activation.Listeners(true)
|
||||
if len(listeners) > 1 {
|
||||
panic("Got more than 1 listener from systemd. This is currently not supported!")
|
||||
}
|
||||
if err == nil && len(listeners) == 1 {
|
||||
listener := listeners[0]
|
||||
defer func() { _ = listener.Close() }()
|
||||
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
|
||||
err = http.Serve(listener, api.Router(context))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to serve: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there are none: use the listen argument.
|
||||
listen := context.Flags().Lookup("listen").Value.String()
|
||||
fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen)
|
||||
|
||||
server := http.Server{Handler: api.Router(context)}
|
||||
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go (func() {
|
||||
if _, ok := <-sigchan; ok {
|
||||
fmt.Printf("\nShutdown signal received, waiting for background tasks...\n")
|
||||
context.TaskList().Wait()
|
||||
_ = server.Shutdown(stdcontext.Background())
|
||||
}
|
||||
})()
|
||||
defer close(sigchan)
|
||||
|
||||
listenURL, err := url.Parse(listen)
|
||||
if err == nil && listenURL.Scheme == "unix" {
|
||||
file := listenURL.Path
|
||||
_ = os.Remove(file)
|
||||
|
||||
var listener net.Listener
|
||||
listener, err = net.Listen("unix", file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on: %s\n%s", file, err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
err = server.Serve(listener)
|
||||
} else {
|
||||
server.Addr = listen
|
||||
err = server.ListenAndServe()
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("unable to serve: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeCmdAPIServe() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyAPIServe,
|
||||
UsageLine: "serve",
|
||||
Short: "start API HTTP service",
|
||||
Long: `
|
||||
Start HTTP server with aptly REST API. The server can listen to either a port
|
||||
or Unix domain socket. When using a socket, Aptly will fully manage the socket
|
||||
file. This command also supports taking over from a systemd file descriptors to
|
||||
enable systemd socket activation.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly api serve -listen=:8080
|
||||
$ aptly api serve -listen=unix:///tmp/aptly.sock
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-serve", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.String("listen", ":8080", "host:port for HTTP listening or unix://path to listen on a Unix domain socket")
|
||||
cmd.Flag.Bool("no-lock", false, "don't lock the database")
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
+54
-19
@@ -2,36 +2,67 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/aptly"
|
||||
"github.com/smira/aptly/deb"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Various command flags/UI things
|
||||
const (
|
||||
Yes = "yes"
|
||||
No = "no"
|
||||
)
|
||||
|
||||
// ListPackagesRefList shows list of packages in PackageRefList
|
||||
func ListPackagesRefList(reflist *deb.PackageRefList) (err error) {
|
||||
func ListPackagesRefList(reflist *deb.PackageRefList, collectionFactory *deb.CollectionFactory) (err error) {
|
||||
fmt.Printf("Packages:\n")
|
||||
|
||||
if reflist == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = reflist.ForEach(func(key []byte) error {
|
||||
p, err2 := context.CollectionFactory().PackageCollection().ByKey(key)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
fmt.Printf(" %s\n", p)
|
||||
return nil
|
||||
})
|
||||
list, err := deb.NewPackageListFromRefList(reflist, collectionFactory.PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load packages: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
return PrintPackageList(list, "", " ")
|
||||
|
||||
}
|
||||
|
||||
// PrintPackageList shows package list with specified format or default representation
|
||||
func PrintPackageList(result *deb.PackageList, format, prefix string) error {
|
||||
result.PrepareIndex()
|
||||
|
||||
if format == "" {
|
||||
return result.ForEachIndexed(func(p *deb.Package) error {
|
||||
context.Progress().Printf(prefix+"%s\n", p)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
formatTemplate, err := template.New("format").Parse(format)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing -format template: %s", err)
|
||||
}
|
||||
|
||||
return result.ForEachIndexed(func(p *deb.Package) error {
|
||||
b := &bytes.Buffer{}
|
||||
err = formatTemplate.Execute(b, p.ExtendedStanza())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error applying template: %s", err)
|
||||
}
|
||||
context.Progress().Printf(prefix+"%s\n", b.String())
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// LookupOption checks boolean flag with default (usually config) and command-line
|
||||
@@ -65,26 +96,30 @@ fine-grained changes in repository contents to transition your
|
||||
package environment to new version.`,
|
||||
Flag: *flag.NewFlagSet("aptly", flag.ExitOnError),
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdDb(),
|
||||
makeCmdConfig(),
|
||||
makeCmdDB(),
|
||||
makeCmdGraph(),
|
||||
makeCmdMirror(),
|
||||
makeCmdRepo(),
|
||||
makeCmdServe(),
|
||||
makeCmdSnapshot(),
|
||||
// Disabled on no docs
|
||||
//makeCmdTask(),
|
||||
makeCmdTask(),
|
||||
makeCmdPublish(),
|
||||
makeCmdVersion(),
|
||||
makeCmdPackage(),
|
||||
makeCmdAPI(),
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flag.Int("db-open-attempts", 10, "number of attempts to open DB if it's locked by other instance")
|
||||
cmd.Flag.Bool("dep-follow-suggests", false, "when processing dependencies, follow Suggests")
|
||||
cmd.Flag.Bool("dep-follow-source", false, "when processing dependencies, follow from binary to Source packages")
|
||||
cmd.Flag.Bool("dep-follow-recommends", false, "when processing dependencies, follow Recommends")
|
||||
cmd.Flag.Bool("dep-follow-all-variants", false, "when processing dependencies, follow a & b if depdency is 'a|b'")
|
||||
cmd.Flag.Bool("dep-follow-all-variants", false, "when processing dependencies, follow a & b if dependency is 'a|b'")
|
||||
cmd.Flag.Bool("dep-verbose-resolve", false, "when processing dependencies, print detailed logs")
|
||||
cmd.Flag.String("architectures", "", "list of architectures to consider during (comma-separated), default to all available")
|
||||
cmd.Flag.String("config", "", "location of configuration file (default locations are /etc/aptly.conf, ~/.aptly.conf)")
|
||||
cmd.Flag.String("config", "", "location of configuration file (default locations in order: ~/.aptly.conf, /usr/local/etc/aptly.conf, /etc/aptly.conf)")
|
||||
cmd.Flag.String("gpg-provider", "", "PGP implementation (\"gpg\", \"gpg1\", \"gpg2\" for external gpg or \"internal\" for Go internal implementation)")
|
||||
|
||||
if aptly.EnableDebug {
|
||||
cmd.Flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/flag"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type CmdSuite struct {
|
||||
mockProgress *MockCmdProgress
|
||||
collectionFactory *deb.CollectionFactory
|
||||
mockContext *MockCmdContext
|
||||
}
|
||||
|
||||
var _ = Suite(&CmdSuite{})
|
||||
|
||||
func (s *CmdSuite) SetUpTest(c *C) {
|
||||
s.mockProgress = &MockCmdProgress{}
|
||||
|
||||
// Set up mock collections - use real collection factory
|
||||
s.collectionFactory = deb.NewCollectionFactory(nil)
|
||||
|
||||
// Set up mock context
|
||||
s.mockContext = &MockCmdContext{
|
||||
progress: s.mockProgress,
|
||||
collectionFactory: s.collectionFactory,
|
||||
}
|
||||
|
||||
// Skip setting mock context globally for type compatibility
|
||||
// context = s.mockContext
|
||||
}
|
||||
|
||||
func (s *CmdSuite) TestListPackagesRefListBasic(c *C) {
|
||||
// Test basic functionality of ListPackagesRefList
|
||||
// Need to initialize context for this test
|
||||
if context == nil {
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
err := InitContext(flags)
|
||||
c.Assert(err, IsNil)
|
||||
defer ShutdownContext()
|
||||
}
|
||||
|
||||
reflist := &deb.PackageRefList{}
|
||||
|
||||
err := ListPackagesRefList(reflist, s.collectionFactory)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *CmdSuite) TestPrintPackageListBasic(c *C) {
|
||||
// Test basic PrintPackageList functionality
|
||||
packageList := deb.NewPackageList()
|
||||
|
||||
err := PrintPackageList(packageList, "", " ")
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Mock implementations for testing
|
||||
|
||||
type MockCmdProgress struct {
|
||||
messages []string
|
||||
}
|
||||
|
||||
func (m *MockCmdProgress) Printf(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) ColoredPrintf(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) PrintfStdErr(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) Flush() {}
|
||||
func (m *MockCmdProgress) Start() {}
|
||||
func (m *MockCmdProgress) Shutdown() {}
|
||||
func (m *MockCmdProgress) InitBar(count int64, isBytes bool, barType aptly.BarType) {}
|
||||
func (m *MockCmdProgress) ShutdownBar() {}
|
||||
func (m *MockCmdProgress) AddBar(count int) {}
|
||||
func (m *MockCmdProgress) SetBar(count int) {}
|
||||
func (m *MockCmdProgress) PrintfBar(msg string, a ...interface{}) {}
|
||||
func (m *MockCmdProgress) Write(p []byte) (n int, err error) { return len(p), nil }
|
||||
|
||||
type MockCmdContext struct {
|
||||
progress *MockCmdProgress
|
||||
collectionFactory *deb.CollectionFactory
|
||||
}
|
||||
|
||||
func (m *MockCmdContext) Flags() *flag.FlagSet { return &flag.FlagSet{} }
|
||||
func (m *MockCmdContext) Progress() aptly.Progress { return m.progress }
|
||||
func (m *MockCmdContext) NewCollectionFactory() *deb.CollectionFactory { return m.collectionFactory }
|
||||
func (m *MockCmdContext) Config() *utils.ConfigStructure { return &utils.ConfigStructure{} }
|
||||
|
||||
// Note: Complex integration tests have been simplified for compilation compatibility.
|
||||
@@ -0,0 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdConfig() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "config",
|
||||
Short: "manage aptly configuration",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdConfigShow(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/smira/commander"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func aptlyConfigShow(_ *commander.Command, _ []string) error {
|
||||
showYaml := context.Flags().Lookup("yaml").Value.Get().(bool)
|
||||
|
||||
config := context.Config()
|
||||
|
||||
if showYaml {
|
||||
yamlData, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling to YAML: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(yamlData))
|
||||
} else {
|
||||
prettyJSON, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to dump the config file: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(prettyJSON))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeCmdConfigShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyConfigShow,
|
||||
UsageLine: "show",
|
||||
Short: "show current aptly's config",
|
||||
Long: `
|
||||
Command show displays the current aptly configuration.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly config show
|
||||
|
||||
`,
|
||||
}
|
||||
cmd.Flag.Bool("yaml", false, "show yaml config")
|
||||
return cmd
|
||||
}
|
||||
+13
-350
@@ -1,312 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/aptly"
|
||||
"github.com/smira/aptly/console"
|
||||
"github.com/smira/aptly/database"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/files"
|
||||
"github.com/smira/aptly/http"
|
||||
"github.com/smira/aptly/s3"
|
||||
"github.com/smira/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/smira/flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AptlyContext is a common context shared by all commands
|
||||
type AptlyContext struct {
|
||||
flags, globalFlags *flag.FlagSet
|
||||
configLoaded bool
|
||||
|
||||
progress aptly.Progress
|
||||
downloader aptly.Downloader
|
||||
database database.Storage
|
||||
packagePool aptly.PackagePool
|
||||
publishedStorages map[string]aptly.PublishedStorage
|
||||
collectionFactory *deb.CollectionFactory
|
||||
dependencyOptions int
|
||||
architecturesList []string
|
||||
// Debug features
|
||||
fileCPUProfile *os.File
|
||||
fileMemProfile *os.File
|
||||
fileMemStats *os.File
|
||||
}
|
||||
|
||||
var context *AptlyContext
|
||||
|
||||
// Check interface
|
||||
var _ aptly.PublishedStorageProvider = &AptlyContext{}
|
||||
|
||||
// FatalError is type for panicking to abort execution with non-zero
|
||||
// exit code and print meaningful explanation
|
||||
type FatalError struct {
|
||||
ReturnCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
// Fatal panics and aborts execution with exit code 1
|
||||
func Fatal(err error) {
|
||||
returnCode := 1
|
||||
if err == commander.ErrFlagError || err == commander.ErrCommandError {
|
||||
returnCode = 2
|
||||
}
|
||||
panic(&FatalError{ReturnCode: returnCode, Message: err.Error()})
|
||||
}
|
||||
|
||||
// Config loads and returns current configuration
|
||||
func (context *AptlyContext) Config() *utils.ConfigStructure {
|
||||
if !context.configLoaded {
|
||||
var err error
|
||||
|
||||
configLocation := context.globalFlags.Lookup("config").Value.String()
|
||||
if configLocation != "" {
|
||||
err = utils.LoadConfig(configLocation, &utils.Config)
|
||||
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
} else {
|
||||
configLocations := []string{
|
||||
filepath.Join(os.Getenv("HOME"), ".aptly.conf"),
|
||||
"/etc/aptly.conf",
|
||||
}
|
||||
|
||||
for _, configLocation := range configLocations {
|
||||
err = utils.LoadConfig(configLocation, &utils.Config)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
Fatal(fmt.Errorf("error loading config file %s: %s", configLocation, err))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Config file not found, creating default config at %s\n\n", configLocations[0])
|
||||
utils.SaveConfig(configLocations[0], &utils.Config)
|
||||
}
|
||||
}
|
||||
|
||||
context.configLoaded = true
|
||||
|
||||
}
|
||||
return &utils.Config
|
||||
}
|
||||
|
||||
// DependencyOptions calculates options related to dependecy handling
|
||||
func (context *AptlyContext) DependencyOptions() int {
|
||||
if context.dependencyOptions == -1 {
|
||||
context.dependencyOptions = 0
|
||||
if LookupOption(context.Config().DepFollowSuggests, context.globalFlags, "dep-follow-suggests") {
|
||||
context.dependencyOptions |= deb.DepFollowSuggests
|
||||
}
|
||||
if LookupOption(context.Config().DepFollowRecommends, context.globalFlags, "dep-follow-recommends") {
|
||||
context.dependencyOptions |= deb.DepFollowRecommends
|
||||
}
|
||||
if LookupOption(context.Config().DepFollowAllVariants, context.globalFlags, "dep-follow-all-variants") {
|
||||
context.dependencyOptions |= deb.DepFollowAllVariants
|
||||
}
|
||||
if LookupOption(context.Config().DepFollowSource, context.globalFlags, "dep-follow-source") {
|
||||
context.dependencyOptions |= deb.DepFollowSource
|
||||
}
|
||||
}
|
||||
|
||||
return context.dependencyOptions
|
||||
}
|
||||
|
||||
// ArchitecturesList returns list of architectures fixed via command line or config
|
||||
func (context *AptlyContext) ArchitecturesList() []string {
|
||||
if context.architecturesList == nil {
|
||||
context.architecturesList = context.Config().Architectures
|
||||
optionArchitectures := context.globalFlags.Lookup("architectures").Value.String()
|
||||
if optionArchitectures != "" {
|
||||
context.architecturesList = strings.Split(optionArchitectures, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return context.architecturesList
|
||||
}
|
||||
|
||||
// Progress creates or returns Progress object
|
||||
func (context *AptlyContext) Progress() aptly.Progress {
|
||||
if context.progress == nil {
|
||||
context.progress = console.NewProgress()
|
||||
context.progress.Start()
|
||||
}
|
||||
|
||||
return context.progress
|
||||
}
|
||||
|
||||
// Downloader returns instance of current downloader
|
||||
func (context *AptlyContext) Downloader() aptly.Downloader {
|
||||
if context.downloader == nil {
|
||||
var downloadLimit int64
|
||||
limitFlag := context.flags.Lookup("download-limit")
|
||||
if limitFlag != nil {
|
||||
downloadLimit = limitFlag.Value.Get().(int64)
|
||||
}
|
||||
if downloadLimit == 0 {
|
||||
downloadLimit = context.Config().DownloadLimit
|
||||
}
|
||||
context.downloader = http.NewDownloader(context.Config().DownloadConcurrency,
|
||||
downloadLimit*1024, context.Progress())
|
||||
}
|
||||
|
||||
return context.downloader
|
||||
}
|
||||
|
||||
// DBPath builds path to database
|
||||
func (context *AptlyContext) DBPath() string {
|
||||
return filepath.Join(context.Config().RootDir, "db")
|
||||
}
|
||||
|
||||
// Database opens and returns current instance of database
|
||||
func (context *AptlyContext) Database() (database.Storage, error) {
|
||||
if context.database == nil {
|
||||
var err error
|
||||
|
||||
context.database, err = database.OpenDB(context.DBPath())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open database: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return context.database, nil
|
||||
}
|
||||
|
||||
// CloseDatabase closes the db temporarily
|
||||
func (context *AptlyContext) CloseDatabase() error {
|
||||
if context.database == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return context.database.Close()
|
||||
}
|
||||
|
||||
// ReOpenDatabase reopens the db after close
|
||||
func (context *AptlyContext) ReOpenDatabase() error {
|
||||
if context.database == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
const MaxTries = 10
|
||||
const Delay = 10 * time.Second
|
||||
|
||||
for try := 0; try < MaxTries; try++ {
|
||||
err := context.database.ReOpen()
|
||||
if err == nil || strings.Index(err.Error(), "resource temporarily unavailable") == -1 {
|
||||
return err
|
||||
}
|
||||
context.Progress().Printf("Unable to reopen database, sleeping %s\n", Delay)
|
||||
<-time.After(Delay)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to reopen the DB, maximum number of retries reached")
|
||||
}
|
||||
|
||||
// CollectionFactory builds factory producing all kinds of collections
|
||||
func (context *AptlyContext) CollectionFactory() *deb.CollectionFactory {
|
||||
if context.collectionFactory == nil {
|
||||
db, err := context.Database()
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
context.collectionFactory = deb.NewCollectionFactory(db)
|
||||
}
|
||||
|
||||
return context.collectionFactory
|
||||
}
|
||||
|
||||
// PackagePool returns instance of PackagePool
|
||||
func (context *AptlyContext) PackagePool() aptly.PackagePool {
|
||||
if context.packagePool == nil {
|
||||
context.packagePool = files.NewPackagePool(context.Config().RootDir)
|
||||
}
|
||||
|
||||
return context.packagePool
|
||||
}
|
||||
|
||||
// GetPublishedStorage returns instance of PublishedStorage
|
||||
func (context *AptlyContext) GetPublishedStorage(name string) aptly.PublishedStorage {
|
||||
publishedStorage, ok := context.publishedStorages[name]
|
||||
if !ok {
|
||||
if name == "" {
|
||||
publishedStorage = files.NewPublishedStorage(context.Config().RootDir)
|
||||
} else if strings.HasPrefix(name, "s3:") {
|
||||
params, ok := context.Config().S3PublishRoots[name[3:]]
|
||||
if !ok {
|
||||
Fatal(fmt.Errorf("published S3 storage %v not configured", name[3:]))
|
||||
}
|
||||
|
||||
var err error
|
||||
publishedStorage, err = s3.NewPublishedStorage(params.AccessKeyID, params.SecretAccessKey,
|
||||
params.Region, params.Bucket, params.ACL, params.Prefix, params.StorageClass,
|
||||
params.EncryptionMethod, params.PlusWorkaround)
|
||||
if err != nil {
|
||||
Fatal(err)
|
||||
}
|
||||
} else {
|
||||
Fatal(fmt.Errorf("unknown published storage format: %v", name))
|
||||
}
|
||||
context.publishedStorages[name] = publishedStorage
|
||||
}
|
||||
|
||||
return publishedStorage
|
||||
}
|
||||
|
||||
// UpdateFlags sets internal copy of flags in the context
|
||||
func (context *AptlyContext) UpdateFlags(flags *flag.FlagSet) {
|
||||
context.flags = flags
|
||||
}
|
||||
var context *ctx.AptlyContext
|
||||
|
||||
// ShutdownContext shuts context down
|
||||
func ShutdownContext() {
|
||||
if aptly.EnableDebug {
|
||||
if context.fileMemProfile != nil {
|
||||
pprof.WriteHeapProfile(context.fileMemProfile)
|
||||
context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
if context.fileCPUProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
context.fileCPUProfile.Close()
|
||||
context.fileCPUProfile = nil
|
||||
}
|
||||
if context.fileMemProfile != nil {
|
||||
context.fileMemProfile.Close()
|
||||
context.fileMemProfile = nil
|
||||
}
|
||||
}
|
||||
if context.database != nil {
|
||||
context.database.Close()
|
||||
context.database = nil
|
||||
}
|
||||
if context.downloader != nil {
|
||||
context.downloader.Abort()
|
||||
context.downloader = nil
|
||||
}
|
||||
if context.progress != nil {
|
||||
context.progress.Shutdown()
|
||||
context.progress = nil
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupContext does partial shutdown of context
|
||||
func CleanupContext() {
|
||||
if context.downloader != nil {
|
||||
context.downloader.Shutdown()
|
||||
context.downloader = nil
|
||||
}
|
||||
if context.progress != nil {
|
||||
context.progress.Shutdown()
|
||||
context.progress = nil
|
||||
if context != nil {
|
||||
context.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,60 +29,12 @@ func InitContext(flags *flag.FlagSet) error {
|
||||
panic("context already initialized")
|
||||
}
|
||||
|
||||
context = &AptlyContext{
|
||||
flags: flags,
|
||||
globalFlags: flags,
|
||||
dependencyOptions: -1,
|
||||
publishedStorages: map[string]aptly.PublishedStorage{},
|
||||
}
|
||||
context, err = ctx.NewContext(flags)
|
||||
|
||||
if aptly.EnableDebug {
|
||||
cpuprofile := flags.Lookup("cpuprofile").Value.String()
|
||||
if cpuprofile != "" {
|
||||
context.fileCPUProfile, err = os.Create(cpuprofile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pprof.StartCPUProfile(context.fileCPUProfile)
|
||||
}
|
||||
|
||||
memprofile := flags.Lookup("memprofile").Value.String()
|
||||
if memprofile != "" {
|
||||
context.fileMemProfile, err = os.Create(memprofile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
memstats := flags.Lookup("memstats").Value.String()
|
||||
if memstats != "" {
|
||||
interval := flags.Lookup("meminterval").Value.Get().(time.Duration)
|
||||
|
||||
context.fileMemStats, err = os.Create(memstats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n")
|
||||
|
||||
go func() {
|
||||
var stats runtime.MemStats
|
||||
|
||||
start := time.Now().UnixNano()
|
||||
|
||||
for {
|
||||
runtime.ReadMemStats(&stats)
|
||||
if context.fileMemStats != nil {
|
||||
context.fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
||||
time.Sleep(interval)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// GetContext gives access to the context
|
||||
func GetContext() *ctx.AptlyContext {
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/smira/flag"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
type ContextSuite struct {
|
||||
originalContext *ctx.AptlyContext
|
||||
}
|
||||
|
||||
var _ = Suite(&ContextSuite{})
|
||||
|
||||
func (s *ContextSuite) SetUpTest(c *C) {
|
||||
// Save original context state
|
||||
s.originalContext = context
|
||||
context = nil // Reset context for each test
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TearDownTest(c *C) {
|
||||
// Clean up and restore original context
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
context = nil
|
||||
}
|
||||
context = s.originalContext
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextSuccess(c *C) {
|
||||
// Test successful context initialization
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
c.Check(GetContext(), Equals, context)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextPanic(c *C) {
|
||||
// Test that initializing context twice causes panic
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// First initialization should succeed
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// Second initialization should panic
|
||||
c.Check(func() { InitContext(flags) }, Panics, "context already initialized")
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestInitContextError(c *C) {
|
||||
// Test context initialization with invalid flags
|
||||
// This tests the error path where ctx.NewContext might fail
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// Add some invalid flag configuration that might cause NewContext to fail
|
||||
// Note: This depends on the ctx.NewContext implementation details
|
||||
flags.String("invalid-config", "/nonexistent/path/to/config", "invalid config")
|
||||
flags.Set("invalid-config", "/nonexistent/path/to/config")
|
||||
|
||||
err := InitContext(flags)
|
||||
// The error handling depends on the ctx.NewContext implementation
|
||||
// If it doesn't fail with invalid paths, the test still validates the error path exists
|
||||
if err != nil {
|
||||
c.Check(context, IsNil)
|
||||
} else {
|
||||
c.Check(context, NotNil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestGetContextBeforeInit(c *C) {
|
||||
// Test GetContext when context is nil
|
||||
c.Check(context, IsNil)
|
||||
result := GetContext()
|
||||
c.Check(result, IsNil)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestGetContextAfterInit(c *C) {
|
||||
// Test GetContext after successful initialization
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
result := GetContext()
|
||||
c.Check(result, NotNil)
|
||||
c.Check(result, Equals, context)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestShutdownContext(c *C) {
|
||||
// Test ShutdownContext function
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// ShutdownContext should not panic and should call context.Shutdown()
|
||||
ShutdownContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestShutdownContextNil(c *C) {
|
||||
// Test ShutdownContext when context is nil (should handle gracefully)
|
||||
context = nil
|
||||
|
||||
// Should not panic when context is nil
|
||||
ShutdownContext() // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestCleanupContext(c *C) {
|
||||
// Test CleanupContext function
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// CleanupContext should not panic and should call context.Cleanup()
|
||||
CleanupContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestCleanupContextNil(c *C) {
|
||||
// Test CleanupContext when context is nil (should handle gracefully)
|
||||
context = nil
|
||||
|
||||
// Should not panic when context is nil
|
||||
CleanupContext() // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestContextLifecycle(c *C) {
|
||||
// Test complete context lifecycle: init -> use -> cleanup -> shutdown
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
// Initialize
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(context, NotNil)
|
||||
|
||||
// Use
|
||||
ctx := GetContext()
|
||||
c.Check(ctx, NotNil)
|
||||
c.Check(ctx, Equals, context)
|
||||
|
||||
// Cleanup
|
||||
CleanupContext() // Should not panic
|
||||
|
||||
// Context should still exist after cleanup
|
||||
c.Check(context, NotNil)
|
||||
c.Check(GetContext(), NotNil)
|
||||
|
||||
// Shutdown
|
||||
ShutdownContext() // Should not panic
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestMultipleCleanups(c *C) {
|
||||
// Test calling CleanupContext multiple times
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Multiple cleanups should not cause issues
|
||||
CleanupContext() // First cleanup
|
||||
CleanupContext() // Second cleanup
|
||||
CleanupContext() // Third cleanup
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestContextVariableIsolation(c *C) {
|
||||
// Test that the context variable is properly managed
|
||||
c.Check(context, IsNil)
|
||||
|
||||
flags := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Store reference
|
||||
originalContext := context
|
||||
c.Check(originalContext, NotNil)
|
||||
|
||||
// GetContext should return the same instance
|
||||
retrievedContext := GetContext()
|
||||
c.Check(retrievedContext, Equals, originalContext)
|
||||
|
||||
// Context variable should be the same
|
||||
c.Check(context, Equals, originalContext)
|
||||
}
|
||||
|
||||
func (s *ContextSuite) TestFlagSetVariations(c *C) {
|
||||
// Test InitContext with different FlagSet configurations
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupFn func() *flag.FlagSet
|
||||
}{
|
||||
{
|
||||
name: "empty flagset",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
return flag.NewFlagSet("empty", flag.ContinueOnError)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flagset with common flags",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("common", flag.ContinueOnError)
|
||||
fs.String("config", "", "config file")
|
||||
fs.Bool("debug", false, "debug mode")
|
||||
return fs
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flagset with aptly-specific flags",
|
||||
setupFn: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("aptly", flag.ContinueOnError)
|
||||
fs.String("architectures", "", "architectures")
|
||||
fs.String("distribution", "", "distribution")
|
||||
return fs
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
// Reset context for each test case
|
||||
if context != nil {
|
||||
context.Shutdown()
|
||||
context = nil
|
||||
}
|
||||
|
||||
flags := tc.setupFn()
|
||||
err := InitContext(flags)
|
||||
c.Check(err, IsNil, Commentf("Failed for test case: %s", tc.name))
|
||||
c.Check(context, NotNil, Commentf("Context is nil for test case: %s", tc.name))
|
||||
c.Check(GetContext(), NotNil, Commentf("GetContext returned nil for test case: %s", tc.name))
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func makeCmdDb() *commander.Command {
|
||||
func makeCmdDB() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "db",
|
||||
Short: "manage aptly's internal database and package pool",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdDbCleanup(),
|
||||
makeCmdDbRecover(),
|
||||
makeCmdDBCleanup(),
|
||||
makeCmdDBRecover(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+209
-63
@@ -2,14 +2,17 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
// aptly db cleanup
|
||||
func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -17,17 +20,103 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
verbose := context.Flags().Lookup("verbose").Value.Get().(bool)
|
||||
dryRun := context.Flags().Lookup("dry-run").Value.Get().(bool)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
// collect information about references packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
|
||||
context.Progress().Printf("Loading mirrors, local repos and snapshots...\n")
|
||||
err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
// used only in verbose mode to report package use source
|
||||
packageRefSources := map[string][]string{}
|
||||
|
||||
context.Progress().ColoredPrintf("@{w!}Loading mirrors, local repos, snapshots and published repos...@|")
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading mirrors:@|")
|
||||
}
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", repo.Name)
|
||||
}
|
||||
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false)
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("mirror %s", repo.Name)
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading local repos:@|")
|
||||
}
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", repo.Name)
|
||||
}
|
||||
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false, true)
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("local repo %s", repo.Name)
|
||||
_ = repo.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading snapshots:@|")
|
||||
}
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s@|", snapshot.Name)
|
||||
}
|
||||
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
existingPackageRefs = existingPackageRefs.Merge(snapshot.RefList(), false, true)
|
||||
|
||||
if verbose {
|
||||
description := fmt.Sprintf("snapshot %s", snapshot.Name)
|
||||
_ = snapshot.RefList().ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -35,13 +124,33 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
err := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
collectionFactory.Flush()
|
||||
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{y}Loading published repositories:@|")
|
||||
}
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(published *deb.PublishedRepo) error {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("- @{g}%s:%s/%s{|}", published.Storage, published.Prefix, published.Distribution)
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
existingPackageRefs = existingPackageRefs.Merge(repo.RefList(), false)
|
||||
if published.SourceKind != deb.SourceLocalRepo {
|
||||
return nil
|
||||
}
|
||||
e := collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
for _, component := range published.Components() {
|
||||
existingPackageRefs = existingPackageRefs.Merge(published.RefList(component), false, true)
|
||||
if verbose {
|
||||
description := fmt.Sprintf("published repository %s:%s/%s component %s",
|
||||
published.Storage, published.Prefix, published.Distribution, component)
|
||||
_ = published.RefList(component).ForEach(func(key []byte) error {
|
||||
packageRefSources[string(key)] = append(packageRefSources[string(key)], description)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -49,51 +158,70 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
err := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existingPackageRefs = existingPackageRefs.Merge(snapshot.RefList(), false)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collectionFactory.Flush()
|
||||
|
||||
// ... and compare it to the list of all packages
|
||||
context.Progress().Printf("Loading list of all packages...\n")
|
||||
allPackageRefs := context.CollectionFactory().PackageCollection().AllPackageRefs()
|
||||
context.Progress().ColoredPrintf("@{w!}Loading list of all packages...@|")
|
||||
allPackageRefs := collectionFactory.PackageCollection().AllPackageRefs()
|
||||
|
||||
toDelete := allPackageRefs.Substract(existingPackageRefs)
|
||||
toDelete := allPackageRefs.Subtract(existingPackageRefs)
|
||||
|
||||
// delete packages that are no longer referenced
|
||||
context.Progress().Printf("Deleting unreferenced packages (%d)...\n", toDelete.Len())
|
||||
context.Progress().ColoredPrintf("@{r!}Deleting unreferenced packages (%d)...@|", toDelete.Len())
|
||||
|
||||
// database can't err as collection factory already constructed
|
||||
db, _ := context.Database()
|
||||
db.StartBatch()
|
||||
err = toDelete.ForEach(func(ref []byte) error {
|
||||
return context.CollectionFactory().PackageCollection().DeleteByKey(ref)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
if toDelete.Len() > 0 {
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{r}List of package keys to delete:@|")
|
||||
err = toDelete.ForEach(func(ref []byte) error {
|
||||
context.Progress().ColoredPrintf(" - @{r}%s@|", string(ref))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
batch := db.CreateBatch()
|
||||
err = toDelete.ForEach(func(ref []byte) error {
|
||||
return collectionFactory.PackageCollection().DeleteByKey(ref, batch)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete by key: %s", err)
|
||||
}
|
||||
|
||||
err = batch.Write()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to DB: %s", err)
|
||||
}
|
||||
} else {
|
||||
context.Progress().ColoredPrintf("@{y!}Skipped deletion, as -dry-run has been requested.@|")
|
||||
}
|
||||
}
|
||||
|
||||
err = db.FinishBatch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to DB: %s", err)
|
||||
}
|
||||
collectionFactory.Flush()
|
||||
|
||||
// now, build a list of files that should be present in Repository (package pool)
|
||||
context.Progress().Printf("Building list of files referenced by packages...\n")
|
||||
context.Progress().ColoredPrintf("@{w!}Building list of files referenced by packages...@|")
|
||||
referencedFiles := make([]string, 0, existingPackageRefs.Len())
|
||||
context.Progress().InitBar(int64(existingPackageRefs.Len()), false)
|
||||
context.Progress().InitBar(int64(existingPackageRefs.Len()), false, aptly.BarCleanupBuildList)
|
||||
|
||||
err = existingPackageRefs.ForEach(func(key []byte) error {
|
||||
pkg, err2 := context.CollectionFactory().PackageCollection().ByKey(key)
|
||||
pkg, err2 := collectionFactory.PackageCollection().ByKey(key)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
tail := ""
|
||||
if verbose {
|
||||
tail = fmt.Sprintf(" (sources: %s)", strings.Join(packageRefSources[string(key)], ", "))
|
||||
}
|
||||
if dryRun {
|
||||
context.Progress().ColoredPrintf("@{r!}Unresolvable package reference, skipping (-dry-run): %s: %s%s",
|
||||
string(key), err2, tail)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to load package %s: %s%s", string(key), err2, tail)
|
||||
}
|
||||
paths, err2 := pkg.FilepathList(context.PackagePool())
|
||||
if err2 != nil {
|
||||
@@ -112,7 +240,7 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
// build a list of files in the package pool
|
||||
context.Progress().Printf("Building list of files in package pool...\n")
|
||||
context.Progress().ColoredPrintf("@{w!}Building list of files in package pool...@|")
|
||||
existingFiles, err := context.PackagePool().FilepathList(context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to collect file paths: %s", err)
|
||||
@@ -122,35 +250,50 @@ func aptlyDbCleanup(cmd *commander.Command, args []string) error {
|
||||
filesToDelete := utils.StrSlicesSubstract(existingFiles, referencedFiles)
|
||||
|
||||
// delete files that are no longer referenced
|
||||
context.Progress().Printf("Deleting unreferenced files (%d)...\n", len(filesToDelete))
|
||||
context.Progress().ColoredPrintf("@{r!}Deleting unreferenced files (%d)...@|", len(filesToDelete))
|
||||
|
||||
if len(filesToDelete) > 0 {
|
||||
context.Progress().InitBar(int64(len(filesToDelete)), false)
|
||||
|
||||
var size, totalSize int64
|
||||
for _, file := range filesToDelete {
|
||||
size, err = context.PackagePool().Remove(file)
|
||||
if err != nil {
|
||||
return err
|
||||
if verbose {
|
||||
context.Progress().ColoredPrintf("@{r}List of files to be deleted:@|")
|
||||
for _, file := range filesToDelete {
|
||||
context.Progress().ColoredPrintf(" - @{r}%s@|", file)
|
||||
}
|
||||
|
||||
context.Progress().AddBar(1)
|
||||
totalSize += size
|
||||
}
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
context.Progress().Printf("Disk space freed: %s...\n", utils.HumanBytes(totalSize))
|
||||
if !dryRun {
|
||||
context.Progress().InitBar(int64(len(filesToDelete)), false, aptly.BarCleanupDeleteUnreferencedFiles)
|
||||
|
||||
var size, totalSize int64
|
||||
for _, file := range filesToDelete {
|
||||
size, err = context.PackagePool().Remove(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.Progress().AddBar(1)
|
||||
totalSize += size
|
||||
}
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
context.Progress().ColoredPrintf("@{w!}Disk space freed: %s...@|", utils.HumanBytes(totalSize))
|
||||
} else {
|
||||
context.Progress().ColoredPrintf("@{y!}Skipped file deletion, as -dry-run has been requested.@|")
|
||||
}
|
||||
}
|
||||
|
||||
context.Progress().Printf("Compacting database...\n")
|
||||
err = db.CompactDB()
|
||||
if !dryRun {
|
||||
context.Progress().ColoredPrintf("@{w!}Compacting database...@|")
|
||||
err = db.CompactDB()
|
||||
} else {
|
||||
context.Progress().ColoredPrintf("@{y!}Skipped DB compaction, as -dry-run has been requested.@|")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbCleanup() *commander.Command {
|
||||
func makeCmdDBCleanup() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbCleanup,
|
||||
Run: aptlyDBCleanup,
|
||||
UsageLine: "cleanup",
|
||||
Short: "cleanup DB and package pool",
|
||||
Long: `
|
||||
@@ -163,5 +306,8 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("verbose", false, "be verbose when loading objects/removing them")
|
||||
cmd.Flag.Bool("dry-run", false, "don't delete anything")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+47
-5
@@ -1,12 +1,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/database"
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
// aptly db recover
|
||||
func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
func aptlyDBRecover(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
if len(args) != 0 {
|
||||
@@ -15,14 +19,19 @@ func aptlyDbRecover(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
context.Progress().Printf("Recovering database...\n")
|
||||
err = database.RecoverDB(context.DBPath())
|
||||
if err = goleveldb.RecoverDB(context.DBPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.Progress().Printf("Checking database integrity...\n")
|
||||
err = checkIntegrity()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdDbRecover() *commander.Command {
|
||||
func makeCmdDBRecover() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyDbRecover,
|
||||
Run: aptlyDBRecover,
|
||||
UsageLine: "recover",
|
||||
Short: "recover DB after crash",
|
||||
Long: `
|
||||
@@ -37,3 +46,36 @@ Example:
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func checkIntegrity() error {
|
||||
return context.NewCollectionFactory().LocalRepoCollection().ForEach(checkRepo)
|
||||
}
|
||||
|
||||
func checkRepo(repo *deb.LocalRepo) error {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repos := collectionFactory.LocalRepoCollection()
|
||||
|
||||
err := repos.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load complete repo %q: %s", repo.Name, err)
|
||||
}
|
||||
|
||||
dangling, err := deb.FindDanglingReferences(repo.RefList(), collectionFactory.PackageCollection())
|
||||
if err != nil {
|
||||
return fmt.Errorf("find dangling references: %w", err)
|
||||
}
|
||||
|
||||
if len(dangling.Refs) > 0 {
|
||||
for _, ref := range dangling.Refs {
|
||||
context.Progress().Printf("Removing dangling database reference %q\n", ref)
|
||||
}
|
||||
|
||||
repo.UpdateRefList(repo.RefList().Subtract(dangling))
|
||||
|
||||
if err = repos.Update(repo); err != nil {
|
||||
return fmt.Errorf("update repo: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+57
-126
@@ -2,15 +2,17 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/gographviz"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
@@ -21,134 +23,34 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
graph := gographviz.NewEscape()
|
||||
graph.SetDir(true)
|
||||
graph.SetName("aptly")
|
||||
|
||||
existingNodes := map[string]bool{}
|
||||
|
||||
fmt.Printf("Loading mirrors...\n")
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "darkgoldenrod1",
|
||||
"label": fmt.Sprintf("{Mirror %s|url: %s|dist: %s|comp: %s|arch: %s|pkgs: %d}",
|
||||
repo.Name, repo.ArchiveRoot, repo.Distribution, strings.Join(repo.Components, ", "),
|
||||
strings.Join(repo.Architectures, ", "), repo.NumPackages()),
|
||||
})
|
||||
existingNodes[repo.UUID] = true
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Loading local repos...\n")
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
err := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "mediumseagreen",
|
||||
"label": fmt.Sprintf("{Repo %s|comment: %s|pkgs: %d}",
|
||||
repo.Name, repo.Comment, repo.NumPackages()),
|
||||
})
|
||||
existingNodes[repo.UUID] = true
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Loading snapshots...\n")
|
||||
|
||||
context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
existingNodes[snapshot.UUID] = true
|
||||
return nil
|
||||
})
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
err := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
description := snapshot.Description
|
||||
if snapshot.SourceKind == "repo" {
|
||||
description = "Snapshot from repo"
|
||||
}
|
||||
|
||||
graph.AddNode("aptly", snapshot.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "cadetblue1",
|
||||
"label": fmt.Sprintf("{Snapshot %s|%s|pkgs: %d}", snapshot.Name, description, snapshot.NumPackages()),
|
||||
})
|
||||
|
||||
if snapshot.SourceKind == "repo" || snapshot.SourceKind == "local" || snapshot.SourceKind == "snapshot" {
|
||||
for _, uuid := range snapshot.SourceIDs {
|
||||
_, exists := existingNodes[uuid]
|
||||
if exists {
|
||||
graph.AddEdge(uuid, snapshot.UUID, true, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Loading published repos...\n")
|
||||
|
||||
context.CollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
graph.AddNode("aptly", repo.UUID, map[string]string{
|
||||
"shape": "Mrecord",
|
||||
"style": "filled",
|
||||
"fillcolor": "darkolivegreen1",
|
||||
"label": fmt.Sprintf("{Published %s/%s|comp: %s|arch: %s}", repo.Prefix, repo.Distribution,
|
||||
strings.Join(repo.Components(), " "), strings.Join(repo.Architectures, ", ")),
|
||||
})
|
||||
|
||||
for _, uuid := range repo.Sources {
|
||||
_, exists := existingNodes[uuid]
|
||||
if exists {
|
||||
graph.AddEdge(uuid, repo.UUID, true, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
layout := context.Flags().Lookup("layout").Value.String()
|
||||
|
||||
fmt.Printf("Generating graph...\n")
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
graph, err := deb.BuildGraph(collectionFactory, layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString(graph.String())
|
||||
|
||||
tempfile, err := ioutil.TempFile("", "aptly-graph")
|
||||
tempfile, err := os.CreateTemp("", "aptly-graph")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempfile.Close()
|
||||
os.Remove(tempfile.Name())
|
||||
_ = tempfile.Close()
|
||||
_ = os.Remove(tempfile.Name())
|
||||
|
||||
tempfilename := tempfile.Name() + ".png"
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
output := context.Flags().Lookup("output").Value.String()
|
||||
|
||||
command := exec.Command("dot", "-Tpng", "-o"+tempfilename)
|
||||
if filepath.Ext(output) != "" {
|
||||
format = filepath.Ext(output)[1:]
|
||||
}
|
||||
|
||||
tempfilename := tempfile.Name() + "." + format
|
||||
|
||||
command := exec.Command("dot", "-T"+format, "-o"+tempfilename)
|
||||
command.Stderr = os.Stderr
|
||||
|
||||
stdin, err := command.StdinPipe()
|
||||
@@ -176,15 +78,40 @@ func aptlyGraph(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = exec.Command("open", tempfilename).Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Rendered to PNG file: %s\n", tempfilename)
|
||||
err = nil
|
||||
if output != "" {
|
||||
err = utils.CopyFile(tempfilename, output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to copy %s -> %s: %s", tempfilename, output, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Output saved to %s\n", output)
|
||||
_ = os.Remove(tempfilename)
|
||||
} else {
|
||||
command := getOpenCommand()
|
||||
fmt.Printf("Displaying %s file: %s %s\n", format, command, tempfilename)
|
||||
|
||||
args := strings.Split(command, " ")
|
||||
|
||||
viewer := exec.Command(args[0], append(args[1:], tempfilename)...)
|
||||
viewer.Stderr = os.Stderr
|
||||
err = viewer.Start()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getOpenCommand tries to guess command to open image for OS
|
||||
func getOpenCommand() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "/usr/bin/open"
|
||||
case "windows":
|
||||
return "cmd /c start"
|
||||
default:
|
||||
return "xdg-open"
|
||||
}
|
||||
}
|
||||
|
||||
func makeCmdGraph() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyGraph,
|
||||
@@ -201,5 +128,9 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.String("format", "png", "render graph to specified format (png, svg, pdf, etc.)")
|
||||
cmd.Flag.String("output", "", "specify output filename, default is to open result in viewer")
|
||||
cmd.Flag.String("layout", "horizontal", "create a more 'vertical' or a more 'horizontal' graph layout")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+10
-9
@@ -1,25 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getVerifier(flags *flag.FlagSet) (utils.Verifier, error) {
|
||||
if LookupOption(context.Config().GpgDisableVerify, flags, "ignore-signatures") {
|
||||
return nil, nil
|
||||
func getVerifier(flags *flag.FlagSet) (pgp.Verifier, error) {
|
||||
keyRings := flags.Lookup("keyring").Value.Get().([]string)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
|
||||
keyRings := flags.Lookup("keyring").Value.Get().([]string)
|
||||
|
||||
verifier := &utils.GpgVerifier{}
|
||||
verifier := context.GetVerifier()
|
||||
for _, keyRing := range keyRings {
|
||||
verifier.AddKeyring(keyRing)
|
||||
}
|
||||
|
||||
err := verifier.InitKeyring()
|
||||
err := verifier.InitKeyring(!ignoreSignatures) // be verbose only if verifying signatures is requested
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+25
-12
@@ -2,11 +2,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/query"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
@@ -16,8 +17,13 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
downloadSources := LookupOption(context.Config().DownloadSourcePackages, context.flags, "with-sources")
|
||||
downloadUdebs := context.flags.Lookup("with-udebs").Value.Get().(bool)
|
||||
downloadSources := LookupOption(context.Config().DownloadSourcePackages, context.Flags(), "with-sources")
|
||||
downloadUdebs := context.Flags().Lookup("with-udebs").Value.Get().(bool)
|
||||
downloadInstaller := context.Flags().Lookup("with-installer").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
|
||||
var (
|
||||
mirrorName, archiveURL, distribution string
|
||||
@@ -35,13 +41,15 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(mirrorName, archiveURL, distribution, components, context.ArchitecturesList(),
|
||||
downloadSources, downloadUdebs)
|
||||
downloadSources, downloadUdebs, downloadInstaller)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create mirror: %s", err)
|
||||
}
|
||||
|
||||
repo.Filter = context.flags.Lookup("filter").Value.String()
|
||||
repo.FilterWithDeps = context.flags.Lookup("filter-with-deps").Value.Get().(bool)
|
||||
repo.Filter = context.Flags().Lookup("filter").Value.String() // allows file/stdin with @
|
||||
repo.FilterWithDeps = context.Flags().Lookup("filter-with-deps").Value.Get().(bool)
|
||||
repo.SkipComponentCheck = context.Flags().Lookup("force-components").Value.Get().(bool)
|
||||
repo.SkipArchitectureCheck = context.Flags().Lookup("force-architectures").Value.Get().(bool)
|
||||
|
||||
if repo.Filter != "" {
|
||||
_, err = query.Parse(repo.Filter)
|
||||
@@ -50,17 +58,18 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
verifier, err := getVerifier(context.flags)
|
||||
verifier, err := getVerifier(context.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier)
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch mirror: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Add(repo)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
err = collectionFactory.RemoteRepoCollection().Add(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add mirror: %s", err)
|
||||
}
|
||||
@@ -91,10 +100,14 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
cmd.Flag.String("filter", "", "filter packages in mirror")
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("force-components", false, "(only with component list) skip check that requested components are listed in Release file")
|
||||
cmd.Flag.Bool("force-architectures", false, "(only with architecture list) skip check that requested architectures are listed in Release file")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
return cmd
|
||||
|
||||
+6
-4
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
@@ -14,8 +15,9 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
@@ -25,9 +27,9 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
force := context.flags.Lookup("force").Value.Get().(bool)
|
||||
force := context.Flags().Lookup("force").Value.Get().(bool)
|
||||
if !force {
|
||||
snapshots := context.CollectionFactory().SnapshotCollection().ByRemoteRepoSource(repo)
|
||||
snapshots := collectionFactory.SnapshotCollection().ByRemoteRepoSource(repo)
|
||||
|
||||
if len(snapshots) > 0 {
|
||||
fmt.Printf("Mirror `%s` was used to create following snapshots:\n", repo.Name)
|
||||
@@ -39,7 +41,7 @@ func aptlyMirrorDrop(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Drop(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Drop(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to drop: %s", err)
|
||||
}
|
||||
|
||||
+33
-8
@@ -2,7 +2,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/query"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
@@ -14,7 +16,8 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(args[0])
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
@@ -24,16 +27,25 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
|
||||
context.flags.Visit(func(flag *flag.Flag) {
|
||||
fetchMirror := false
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
context.Flags().Visit(func(flag *flag.Flag) {
|
||||
switch flag.Name {
|
||||
case "filter":
|
||||
repo.Filter = flag.Value.String()
|
||||
repo.Filter = flag.Value.String() // allows file/stdin with @
|
||||
case "filter-with-deps":
|
||||
repo.FilterWithDeps = flag.Value.Get().(bool)
|
||||
case "with-installer":
|
||||
repo.DownloadInstaller = flag.Value.Get().(bool)
|
||||
case "with-sources":
|
||||
repo.DownloadSources = flag.Value.Get().(bool)
|
||||
case "with-udebs":
|
||||
repo.DownloadUdebs = flag.Value.Get().(bool)
|
||||
case "archive-url":
|
||||
repo.SetArchiveRoot(flag.Value.String())
|
||||
fetchMirror = true
|
||||
case "ignore-signatures":
|
||||
ignoreSignatures = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -48,16 +60,25 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
if context.globalFlags.Lookup("architectures").Value.String() != "" {
|
||||
if context.GlobalFlags().Lookup("architectures").Value.String() != "" {
|
||||
repo.Architectures = context.ArchitecturesList()
|
||||
fetchMirror = true
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), nil)
|
||||
if fetchMirror {
|
||||
var verifier pgp.Verifier
|
||||
verifier, err = getVerifier(context.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to edit: %s", err)
|
||||
}
|
||||
@@ -82,10 +103,14 @@ Example:
|
||||
Flag: *flag.NewFlagSet("aptly-mirror-edit", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.String("filter", "", "filter packages in mirror")
|
||||
cmd.Flag.String("archive-url", "", "archive url is the root of archive")
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+49
-6
@@ -1,24 +1,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
repos := make([]string, context.CollectionFactory().RemoteRepoCollection().Len())
|
||||
if jsonFlag {
|
||||
return aptlyMirrorListJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyMirrorListTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyMirrorListTxt(cmd *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
repos := make([]string, collectionFactory.RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
_ = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
if raw {
|
||||
repos[i] = repo.Name
|
||||
} else {
|
||||
@@ -28,6 +42,8 @@ func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(repos)
|
||||
|
||||
if raw {
|
||||
@@ -49,6 +65,32 @@ func aptlyMirrorList(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyMirrorListJSON(_ *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
repos := make([]*deb.RemoteRepo, context.NewCollectionFactory().RemoteRepoCollection().Len())
|
||||
i := 0
|
||||
_ = context.NewCollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
repos[i] = repo
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].Name < repos[j].Name
|
||||
})
|
||||
|
||||
if output, e := json.MarshalIndent(repos, "", " "); e == nil {
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdMirrorList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyMirrorList,
|
||||
@@ -63,6 +105,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display list in JSON format")
|
||||
cmd.Flag.Bool("raw", false, "display list in machine-readable format")
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -2,7 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,8 @@ func aptlyMirrorRename(cmd *commander.Command, args []string) error {
|
||||
|
||||
oldName, newName := args[0], args[1]
|
||||
|
||||
repo, err = context.CollectionFactory().RemoteRepoCollection().ByName(oldName)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err = collectionFactory.RemoteRepoCollection().ByName(oldName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
@@ -29,13 +31,13 @@ func aptlyMirrorRename(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
_, err = context.CollectionFactory().RemoteRepoCollection().ByName(newName)
|
||||
_, err = collectionFactory.RemoteRepoCollection().ByName(newName)
|
||||
if err == nil {
|
||||
return fmt.Errorf("unable to rename: mirror %s already exists", newName)
|
||||
}
|
||||
|
||||
repo.Name = newName
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename: %s", err)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
func makeCmdMirrorSearch() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlySnapshotMirrorRepoSearch,
|
||||
UsageLine: "search <name> <package-query>",
|
||||
UsageLine: "search <name> [<package-query>]",
|
||||
Short: "search mirror for packages matching query",
|
||||
Long: `
|
||||
Command search displays list of packages in mirror that match package query
|
||||
|
||||
If query is not specified, all the packages are displayed.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly mirror search wheezy-main '$Architecture (i386), Name (% *-dev)'
|
||||
@@ -21,6 +23,7 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("with-deps", false, "include dependencies into search results")
|
||||
cmd.Flag.String("format", "", "custom format for result printing")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+73
-14
@@ -1,29 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/utils"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyMirrorShowJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyMirrorShowTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
@@ -36,21 +51,21 @@ func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
fmt.Printf("Distribution: %s\n", repo.Distribution)
|
||||
fmt.Printf("Components: %s\n", strings.Join(repo.Components, ", "))
|
||||
fmt.Printf("Architectures: %s\n", strings.Join(repo.Architectures, ", "))
|
||||
downloadSources := "no"
|
||||
downloadSources := No
|
||||
if repo.DownloadSources {
|
||||
downloadSources = "yes"
|
||||
downloadSources = Yes
|
||||
}
|
||||
fmt.Printf("Download Sources: %s\n", downloadSources)
|
||||
downloadUdebs := "no"
|
||||
downloadUdebs := No
|
||||
if repo.DownloadUdebs {
|
||||
downloadUdebs = "yes"
|
||||
downloadUdebs = Yes
|
||||
}
|
||||
fmt.Printf("Download .udebs: %s\n", downloadUdebs)
|
||||
if repo.Filter != "" {
|
||||
fmt.Printf("Filter: %s\n", repo.Filter)
|
||||
filterWithDeps := "no"
|
||||
filterWithDeps := No
|
||||
if repo.FilterWithDeps {
|
||||
filterWithDeps = "yes"
|
||||
filterWithDeps = Yes
|
||||
}
|
||||
fmt.Printf("Filter With Deps: %s\n", filterWithDeps)
|
||||
}
|
||||
@@ -66,18 +81,61 @@ func aptlyMirrorShow(cmd *commander.Command, args []string) error {
|
||||
fmt.Printf("%s: %s\n", k, repo.Meta[k])
|
||||
}
|
||||
|
||||
withPackages := context.flags.Lookup("with-packages").Value.Get().(bool)
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
if repo.LastDownloadDate.IsZero() {
|
||||
fmt.Printf("Unable to show package list, mirror hasn't been downloaded yet.\n")
|
||||
} else {
|
||||
ListPackagesRefList(repo.RefList())
|
||||
_ = ListPackagesRefList(repo.RefList(), collectionFactory)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyMirrorShowJSON(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.NewCollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
// include packages if requested
|
||||
withPackages := context.Flags().Lookup("with-packages").Value.Get().(bool)
|
||||
if withPackages {
|
||||
if repo.RefList() != nil {
|
||||
var list *deb.PackageList
|
||||
list, err = deb.NewPackageListFromRefList(repo.RefList(), context.NewCollectionFactory().PackageCollection(), context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get package list: %s", err)
|
||||
}
|
||||
|
||||
list.PrepareIndex()
|
||||
_ = list.ForEachIndexed(func(p *deb.Package) error {
|
||||
repo.Packages = append(repo.Packages, p.GetFullName())
|
||||
return nil
|
||||
})
|
||||
|
||||
sort.Strings(repo.Packages)
|
||||
}
|
||||
}
|
||||
|
||||
var output []byte
|
||||
if output, err = json.MarshalIndent(repo, "", " "); err == nil {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdMirrorShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyMirrorShow,
|
||||
@@ -93,6 +151,7 @@ Example:
|
||||
Flag: *flag.NewFlagSet("aptly-mirror-show", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
cmd.Flag.Bool("with-packages", false, "show detailed list of packages and versions stored in the mirror")
|
||||
|
||||
return cmd
|
||||
|
||||
+158
-50
@@ -2,14 +2,16 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/query"
|
||||
"github.com/smira/aptly/utils"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
@@ -21,17 +23,18 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
|
||||
name := args[0]
|
||||
|
||||
repo, err := context.CollectionFactory().RemoteRepoCollection().ByName(name)
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
force := context.flags.Lookup("force").Value.Get().(bool)
|
||||
force := context.Flags().Lookup("force").Value.Get().(bool)
|
||||
if !force {
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
@@ -39,20 +42,24 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
ignoreMismatch := context.flags.Lookup("ignore-checksums").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
}
|
||||
ignoreChecksums := context.Flags().Lookup("ignore-checksums").Value.Get().(bool)
|
||||
|
||||
verifier, err := getVerifier(context.flags)
|
||||
verifier, err := getVerifier(context.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize GPG verifier: %s", err)
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier)
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("Downloading & parsing package files...\n")
|
||||
err = repo.DownloadPackageIndexes(context.Progress(), context.Downloader(), context.CollectionFactory(), ignoreMismatch)
|
||||
err = repo.DownloadPackageIndexes(context.Progress(), context.Downloader(), verifier, collectionFactory, ignoreSignatures, ignoreChecksums)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -67,7 +74,7 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
var oldLen, newLen int
|
||||
oldLen, newLen, err = repo.ApplyFilter(context.DependencyOptions(), filterQuery)
|
||||
oldLen, newLen, err = repo.ApplyFilter(context.DependencyOptions(), filterQuery, context.Progress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -79,23 +86,27 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
queue []deb.PackageDownloadTask
|
||||
)
|
||||
|
||||
skipExistingPackages := context.Flags().Lookup("skip-existing-packages").Value.Get().(bool)
|
||||
|
||||
context.Progress().Printf("Building download queue...\n")
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool())
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// on any interruption, unlock the mirror
|
||||
err := context.ReOpenDatabase()
|
||||
err = context.ReOpenDatabase()
|
||||
if err == nil {
|
||||
repo.MarkAsIdle()
|
||||
context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
_ = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
}
|
||||
}()
|
||||
|
||||
repo.MarkAsUpdating()
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
@@ -105,64 +116,158 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
// Catch ^C
|
||||
sigch := make(chan os.Signal)
|
||||
signal.Notify(sigch, os.Interrupt)
|
||||
context.GoContextHandleSignals()
|
||||
|
||||
count := len(queue)
|
||||
context.Progress().Printf("Download queue: %d items (%s)\n", count, utils.HumanBytes(downloadSize))
|
||||
|
||||
// Download from the queue
|
||||
context.Progress().InitBar(downloadSize, true)
|
||||
context.Progress().InitBar(downloadSize, true, aptly.BarMirrorUpdateDownloadPackages)
|
||||
|
||||
// Download all package files
|
||||
ch := make(chan error, count)
|
||||
downloadQueue := make(chan int)
|
||||
|
||||
var (
|
||||
errors []string
|
||||
errLock sync.Mutex
|
||||
)
|
||||
|
||||
pushError := func(err error) {
|
||||
errLock.Lock()
|
||||
errors = append(errors, err.Error())
|
||||
errLock.Unlock()
|
||||
}
|
||||
|
||||
// In separate goroutine (to avoid blocking main), push queue to downloader
|
||||
go func() {
|
||||
for _, task := range queue {
|
||||
context.Downloader().DownloadWithChecksum(repo.PackageURL(task.RepoURI).String(), task.DestinationPath, ch, task.Checksums, ignoreMismatch)
|
||||
for idx := range queue {
|
||||
select {
|
||||
case downloadQueue <- idx:
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need queue after this point
|
||||
queue = nil
|
||||
close(downloadQueue)
|
||||
}()
|
||||
|
||||
// Wait for all downloads to finish
|
||||
errors := make([]string, 0)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for count > 0 {
|
||||
select {
|
||||
case <-sigch:
|
||||
signal.Stop(sigch)
|
||||
return fmt.Errorf("unable to update: interrupted")
|
||||
case err = <-ch:
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
for i := 0; i < context.Config().DownloadConcurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case idx, ok := <-downloadQueue:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
task := &queue[idx]
|
||||
|
||||
var e error
|
||||
|
||||
// provision download location
|
||||
if pp, ok := context.PackagePool().(aptly.LocalPackagePool); ok {
|
||||
task.TempDownPath, e = pp.GenerateTempPath(task.File.Filename)
|
||||
} else {
|
||||
var file *os.File
|
||||
file, e = os.CreateTemp("", task.File.Filename)
|
||||
if e == nil {
|
||||
task.TempDownPath = file.Name()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
// download file...
|
||||
e = context.Downloader().DownloadWithChecksum(
|
||||
context,
|
||||
repo.PackageURL(task.File.DownloadURL()).String(),
|
||||
task.TempDownPath,
|
||||
&task.File.Checksums,
|
||||
ignoreChecksums)
|
||||
if e != nil {
|
||||
pushError(e)
|
||||
continue
|
||||
}
|
||||
|
||||
task.Done = true
|
||||
case <-context.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
count--
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all download goroutines to finish
|
||||
wg.Wait()
|
||||
|
||||
context.Progress().ShutdownBar()
|
||||
signal.Stop(sigch)
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("unable to update: download errors:\n %s\n", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
err = context.ReOpenDatabase()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
repo.FinalizeDownload()
|
||||
err = context.CollectionFactory().RemoteRepoCollection().Update(repo)
|
||||
defer func() {
|
||||
for _, task := range queue {
|
||||
if task.TempDownPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(task.TempDownPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Failed to delete %s: %v\n", task.TempDownPath, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Import downloaded files
|
||||
context.Progress().InitBar(int64(len(queue)), false, aptly.BarMirrorUpdateImportFiles)
|
||||
|
||||
for idx := range queue {
|
||||
context.Progress().AddBar(1)
|
||||
|
||||
task := &queue[idx]
|
||||
|
||||
if !task.Done {
|
||||
// download not finished yet
|
||||
continue
|
||||
}
|
||||
|
||||
// and import it back to the pool
|
||||
task.File.PoolPath, err = context.PackagePool().Import(task.TempDownPath, task.File.Filename, &task.File.Checksums, true, collectionFactory.ChecksumCollection(nil))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import file: %s", err)
|
||||
}
|
||||
|
||||
// update "attached" files if any
|
||||
for _, additionalTask := range task.Additional {
|
||||
additionalTask.File.PoolPath = task.File.PoolPath
|
||||
additionalTask.File.Checksums = task.File.Checksums
|
||||
}
|
||||
}
|
||||
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
select {
|
||||
case <-context.Done():
|
||||
return fmt.Errorf("unable to update: interrupted")
|
||||
default:
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("unable to update: download errors:\n %s", strings.Join(errors, "\n "))
|
||||
}
|
||||
|
||||
_ = repo.FinalizeDownload(collectionFactory, context.Progress())
|
||||
err = collectionFactory.RemoteRepoCollection().Update(repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nMirror `%s` has been successfully updated.\n", repo.Name)
|
||||
context.Progress().Printf("\nMirror `%s` has been updated successfully.\n", repo.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -186,7 +291,10 @@ Example:
|
||||
cmd.Flag.Bool("force", false, "force update mirror even if it is locked by another process")
|
||||
cmd.Flag.Bool("ignore-checksums", false, "ignore checksum mismatches while downloading package files and metadata")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("skip-existing-packages", false, "do not check file existence for packages listed in the internal database of the mirror")
|
||||
cmd.Flag.Int64("download-limit", 0, "limit download speed (kbytes/sec)")
|
||||
cmd.Flag.String("downloader", "default", "downloader to use (e.g. grab)")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "gpg keyring to use when verifying Release file (could be specified multiple times)")
|
||||
|
||||
return cmd
|
||||
|
||||
+35
-14
@@ -2,29 +2,45 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/query"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 1 {
|
||||
var (
|
||||
err error
|
||||
q deb.PackageQuery
|
||||
)
|
||||
|
||||
if len(args) > 1 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
q, err := query.Parse(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to search: %s", err)
|
||||
if len(args) == 1 {
|
||||
value, err := GetStringOrFileContent(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[0], err)
|
||||
}
|
||||
q, err = query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to search: %s", err)
|
||||
}
|
||||
} else {
|
||||
q = &deb.MatchAllQuery{}
|
||||
}
|
||||
|
||||
result := q.Query(context.CollectionFactory().PackageCollection())
|
||||
result.ForEach(func(p *deb.Package) error {
|
||||
context.Progress().Printf("%s\n", p)
|
||||
return nil
|
||||
})
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
if result.Len() == 0 {
|
||||
return fmt.Errorf("no results")
|
||||
}
|
||||
|
||||
format := context.Flags().Lookup("format").Value.String()
|
||||
_ = PrintPackageList(result, format, "")
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -32,10 +48,13 @@ func aptlyPackageSearch(cmd *commander.Command, args []string) error {
|
||||
func makeCmdPackageSearch() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPackageSearch,
|
||||
UsageLine: "search <package-query>",
|
||||
UsageLine: "search [<package-query>]",
|
||||
Short: "search for packages matching query",
|
||||
Long: `
|
||||
Command search displays list of packages in whole DB that match package query
|
||||
Command search displays list of packages in whole DB that match package query.
|
||||
|
||||
Use '@file' to read query from file or '@-' for stdin.
|
||||
If query is not specified, all the packages are displayed.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -44,5 +63,7 @@ Example:
|
||||
Flag: *flag.NewFlagSet("aptly-package-search", flag.ExitOnError),
|
||||
}
|
||||
|
||||
cmd.Flag.String("format", "", "custom format for result printing")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+43
-30
@@ -3,18 +3,20 @@ package cmd
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/query"
|
||||
"os"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/query"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"os"
|
||||
)
|
||||
|
||||
func printReferencesTo(p *deb.Package) (err error) {
|
||||
err = context.CollectionFactory().RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := context.CollectionFactory().RemoteRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
func printReferencesTo(p *deb.Package, collectionFactory *deb.CollectionFactory) (err error) {
|
||||
err = collectionFactory.RemoteRepoCollection().ForEach(func(repo *deb.RemoteRepo) error {
|
||||
e := collectionFactory.RemoteRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
if repo.RefList().Has(p) {
|
||||
@@ -27,10 +29,10 @@ func printReferencesTo(p *deb.Package) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
err := context.CollectionFactory().LocalRepoCollection().LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
err = collectionFactory.LocalRepoCollection().ForEach(func(repo *deb.LocalRepo) error {
|
||||
e := collectionFactory.LocalRepoCollection().LoadComplete(repo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if repo.RefList() != nil {
|
||||
if repo.RefList().Has(p) {
|
||||
@@ -43,21 +45,18 @@ func printReferencesTo(p *deb.Package) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
err := context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
err = collectionFactory.SnapshotCollection().ForEach(func(snapshot *deb.Snapshot) error {
|
||||
e := collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if snapshot.RefList().Has(p) {
|
||||
fmt.Printf(" snapshot %s\n", snapshot)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
@@ -67,30 +66,42 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
q, err := query.Parse(args[0])
|
||||
value, err := GetStringOrFileContent(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read package query from file %s: %w", args[0], err)
|
||||
}
|
||||
q, err := query.Parse(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
withFiles := context.flags.Lookup("with-files").Value.Get().(bool)
|
||||
withReferences := context.flags.Lookup("with-references").Value.Get().(bool)
|
||||
withFiles := context.Flags().Lookup("with-files").Value.Get().(bool)
|
||||
withReferences := context.Flags().Lookup("with-references").Value.Get().(bool)
|
||||
|
||||
w := bufio.NewWriter(os.Stdout)
|
||||
|
||||
result := q.Query(context.CollectionFactory().PackageCollection())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
result := q.Query(collectionFactory.PackageCollection())
|
||||
|
||||
err = result.ForEach(func(p *deb.Package) error {
|
||||
p.Stanza().WriteTo(w)
|
||||
w.Flush()
|
||||
_ = p.Stanza().WriteTo(w, p.IsSource, false, false)
|
||||
_ = w.Flush()
|
||||
fmt.Printf("\n")
|
||||
|
||||
if withFiles {
|
||||
fmt.Printf("Files in the pool:\n")
|
||||
packagePool := context.PackagePool()
|
||||
for _, f := range p.Files() {
|
||||
path, err := context.PackagePool().Path(f.Filename, f.Checksums.MD5)
|
||||
var path string
|
||||
path, err = f.GetPoolPath(packagePool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pp, ok := packagePool.(aptly.LocalPackagePool); ok {
|
||||
path = pp.FullPath(path)
|
||||
}
|
||||
|
||||
fmt.Printf(" %s\n", path)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
@@ -98,7 +109,7 @@ func aptlyPackageShow(cmd *commander.Command, args []string) error {
|
||||
|
||||
if withReferences {
|
||||
fmt.Printf("References to package:\n")
|
||||
printReferencesTo(p)
|
||||
_ = printReferencesTo(p, collectionFactory)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
@@ -116,16 +127,18 @@ func makeCmdPackageShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPackageShow,
|
||||
UsageLine: "show <package-query>",
|
||||
Short: "show details about packages matcing query",
|
||||
Short: "show details about packages matching query",
|
||||
Long: `
|
||||
Command shows displays detailed meta-information about packages
|
||||
matching query. Information from Debian control file is displayed.
|
||||
Optionally information about package files and
|
||||
inclusion into mirrors/snapshots/local repos is shown.
|
||||
|
||||
Use '@file' to read query from file or '@-' for stdin.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly package show nginx-light_1.2.1-2.2+wheezy2_i386'
|
||||
$ aptly package show 'nginx-light_1.2.1-2.2+wheezy2_i386'
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-package-show", flag.ExitOnError),
|
||||
}
|
||||
|
||||
+21
-18
@@ -1,21 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/smira/aptly/utils"
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getSigner(flags *flag.FlagSet) (utils.Signer, error) {
|
||||
func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
if LookupOption(context.Config().GpgDisableSign, flags, "skip-signing") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
signer := &utils.GpgSigner{}
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(flags.Lookup("gpg-key").Value.String())
|
||||
signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String())
|
||||
signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String())
|
||||
signer.SetBatch(flags.Lookup("batch").Value.Get().(bool))
|
||||
|
||||
err := signer.Init()
|
||||
if err != nil {
|
||||
@@ -26,20 +26,6 @@ func getSigner(flags *flag.FlagSet) (utils.Signer, error) {
|
||||
|
||||
}
|
||||
|
||||
func parsePrefix(param string) (storage, prefix string) {
|
||||
i := strings.LastIndex(param, ":")
|
||||
if i != -1 {
|
||||
storage = param[:i]
|
||||
prefix = param[i+1:]
|
||||
if prefix == "" {
|
||||
prefix = "."
|
||||
}
|
||||
} else {
|
||||
prefix = param
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeCmdPublish() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "publish",
|
||||
@@ -48,9 +34,26 @@ func makeCmdPublish() *commander.Command {
|
||||
makeCmdPublishDrop(),
|
||||
makeCmdPublishList(),
|
||||
makeCmdPublishRepo(),
|
||||
makeCmdPublishShow(),
|
||||
makeCmdPublishSnapshot(),
|
||||
makeCmdPublishSource(),
|
||||
makeCmdPublishSwitch(),
|
||||
makeCmdPublishUpdate(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeCmdPublishSource() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "source",
|
||||
Short: "manage sources of published repository",
|
||||
Subcommands: []*commander.Command{
|
||||
makeCmdPublishSourceAdd(),
|
||||
makeCmdPublishSourceDrop(),
|
||||
makeCmdPublishSourceList(),
|
||||
makeCmdPublishSourceRemove(),
|
||||
makeCmdPublishSourceReplace(),
|
||||
makeCmdPublishSourceUpdate(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -2,6 +2,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
@@ -19,10 +21,13 @@ func aptlyPublishDrop(cmd *commander.Command, args []string) error {
|
||||
param = args[1]
|
||||
}
|
||||
|
||||
storage, prefix := parsePrefix(param)
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Remove(context, storage, prefix, distribution,
|
||||
context.CollectionFactory(), context.Progress())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
err = collectionFactory.PublishedRepoCollection().Remove(context, storage, prefix, distribution,
|
||||
collectionFactory, context.Progress(),
|
||||
context.Flags().Lookup("force-drop").Value.Get().(bool),
|
||||
context.Flags().Lookup("skip-cleanup").Value.Get().(bool))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to remove: %s", err)
|
||||
}
|
||||
@@ -47,5 +52,8 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("force-drop", false, "remove published repository even if some files could not be cleaned up")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+64
-8
@@ -1,27 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
if len(args) != 0 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyPublishListJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyPublishListTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyPublishListTxt(cmd *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
raw := cmd.Flag.Lookup("raw").Value.Get().(bool)
|
||||
|
||||
published := make([]string, 0, context.CollectionFactory().PublishedRepoCollection().Len())
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published := make([]string, 0, collectionFactory.PublishedRepoCollection().Len())
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
err := context.CollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.CollectionFactory())
|
||||
if err != nil {
|
||||
return err
|
||||
err = collectionFactory.PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := collectionFactory.PublishedRepoCollection().LoadShallow(repo, collectionFactory)
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error found on one publish (prefix:%s / distribution:%s / component:%s\n)",
|
||||
repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
return e
|
||||
}
|
||||
|
||||
if raw {
|
||||
@@ -36,6 +53,8 @@ func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Strings(published)
|
||||
|
||||
if raw {
|
||||
@@ -58,6 +77,42 @@ func aptlyPublishList(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyPublishListJSON(_ *commander.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
repos := make([]*deb.PublishedRepo, 0, context.NewCollectionFactory().PublishedRepoCollection().Len())
|
||||
|
||||
err = context.NewCollectionFactory().PublishedRepoCollection().ForEach(func(repo *deb.PublishedRepo) error {
|
||||
e := context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if e != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error found on one publish (prefix:%s / distribution:%s / component:%s\n)",
|
||||
repo.StoragePrefix(), repo.Distribution, repo.Components())
|
||||
return e
|
||||
}
|
||||
|
||||
repos = append(repos, repo)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load list of repos: %s", err)
|
||||
}
|
||||
|
||||
_ = context.CloseDatabase()
|
||||
|
||||
sort.Slice(repos, func(i, j int) bool {
|
||||
return repos[i].GetPath() < repos[j].GetPath()
|
||||
})
|
||||
if output, e := json.MarshalIndent(repos, "", " "); e == nil {
|
||||
fmt.Println(string(output))
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishList() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishList,
|
||||
@@ -72,6 +127,7 @@ Example:
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display list in JSON format")
|
||||
cmd.Flag.Bool("raw", false, "display list in machine-readable format")
|
||||
|
||||
return cmd
|
||||
|
||||
+11
-2
@@ -37,12 +37,21 @@ Example:
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("origin", "", "origin name to publish")
|
||||
cmd.Flag.String("notautomatic", "", "set value for NotAutomatic field")
|
||||
cmd.Flag.String("butautomaticupgrades", "", "set value for ButAutomaticUpgrades field")
|
||||
cmd.Flag.String("label", "", "label to publish")
|
||||
cmd.Flag.String("suite", "", "suite to publish (defaults to distribution)")
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
)
|
||||
|
||||
func aptlyPublishShow(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
jsonFlag := cmd.Flag.Lookup("json").Value.Get().(bool)
|
||||
|
||||
if jsonFlag {
|
||||
return aptlyPublishShowJSON(cmd, args)
|
||||
}
|
||||
|
||||
return aptlyPublishShowTxt(cmd, args)
|
||||
}
|
||||
|
||||
func aptlyPublishShowTxt(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
distribution := args[0]
|
||||
param := "."
|
||||
|
||||
if len(args) == 2 {
|
||||
param = args[1]
|
||||
}
|
||||
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
repo, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
if repo.Storage != "" {
|
||||
fmt.Printf("Storage: %s\n", repo.Storage)
|
||||
}
|
||||
fmt.Printf("Prefix: %s\n", repo.Prefix)
|
||||
if repo.Distribution != "" {
|
||||
fmt.Printf("Distribution: %s\n", repo.Distribution)
|
||||
}
|
||||
fmt.Printf("Architectures: %s\n", strings.Join(repo.Architectures, " "))
|
||||
|
||||
fmt.Printf("Sources:\n")
|
||||
for _, component := range repo.Components() {
|
||||
sourceID := repo.Sources[component]
|
||||
var name string
|
||||
if repo.SourceKind == deb.SourceSnapshot {
|
||||
source, e := collectionFactory.SnapshotCollection().ByUUID(sourceID)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
name = source.Name
|
||||
} else if repo.SourceKind == deb.SourceLocalRepo {
|
||||
source, e := collectionFactory.LocalRepoCollection().ByUUID(sourceID)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
name = source.Name
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
fmt.Printf(" %s: %s [%s]\n", component, name, repo.SourceKind)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func aptlyPublishShowJSON(_ *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
distribution := args[0]
|
||||
param := "."
|
||||
|
||||
if len(args) == 2 {
|
||||
param = args[1]
|
||||
}
|
||||
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
repo, err := context.NewCollectionFactory().PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to show: %s", err)
|
||||
}
|
||||
|
||||
err = context.NewCollectionFactory().PublishedRepoCollection().LoadComplete(repo, context.NewCollectionFactory())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var output []byte
|
||||
if output, err = json.MarshalIndent(repo, "", " "); err == nil {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishShow() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishShow,
|
||||
UsageLine: "show <distribution> [[<endpoint>:]<prefix>]",
|
||||
Short: "shows details of published repository",
|
||||
Long: `
|
||||
Command show displays full information of a published repository.
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish show wheezy
|
||||
`,
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("json", false, "display record in JSON format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
+72
-29
@@ -2,18 +2,20 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/smira/aptly/aptly"
|
||||
"github.com/smira/aptly/deb"
|
||||
"github.com/smira/aptly/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
components := strings.Split(context.flags.Lookup("component").Value.String(), ",")
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
|
||||
if len(args) < len(components) || len(args) > len(components)+1 {
|
||||
cmd.Usage()
|
||||
@@ -27,14 +29,14 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
} else {
|
||||
param = ""
|
||||
}
|
||||
storage, prefix := parsePrefix(param)
|
||||
storage, prefix := deb.ParsePrefix(param)
|
||||
|
||||
var (
|
||||
sources = []interface{}{}
|
||||
message string
|
||||
)
|
||||
|
||||
if cmd.Name() == "snapshot" {
|
||||
if cmd.Name() == "snapshot" { // nolint: goconst
|
||||
var (
|
||||
snapshot *deb.Snapshot
|
||||
emptyWarning = false
|
||||
@@ -42,12 +44,12 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
for _, name := range args {
|
||||
snapshot, err = context.CollectionFactory().SnapshotCollection().ByName(name)
|
||||
snapshot, err = collectionFactory.SnapshotCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().SnapshotCollection().LoadComplete(snapshot)
|
||||
err = collectionFactory.SnapshotCollection().LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
if emptyWarning {
|
||||
context.Progress().Printf("Warning: publishing from empty source, architectures list should be complete, it can't be changed after publishing (use -architectures flag)\n")
|
||||
}
|
||||
} else if cmd.Name() == "repo" {
|
||||
} else if cmd.Name() == "repo" { // nolint: goconst
|
||||
var (
|
||||
localRepo *deb.LocalRepo
|
||||
emptyWarning = false
|
||||
@@ -78,12 +80,12 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
for _, name := range args {
|
||||
localRepo, err = context.CollectionFactory().LocalRepoCollection().ByName(name)
|
||||
localRepo, err = collectionFactory.LocalRepoCollection().ByName(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().LocalRepoCollection().LoadComplete(localRepo)
|
||||
err = collectionFactory.LocalRepoCollection().LoadComplete(localRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
@@ -110,38 +112,70 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
panic("unknown command")
|
||||
}
|
||||
|
||||
distribution := context.flags.Lookup("distribution").Value.String()
|
||||
distribution := context.Flags().Lookup("distribution").Value.String()
|
||||
origin := context.Flags().Lookup("origin").Value.String()
|
||||
notAutomatic := context.Flags().Lookup("notautomatic").Value.String()
|
||||
butAutomaticUpgrades := context.Flags().Lookup("butautomaticupgrades").Value.String()
|
||||
multiDist := context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, distribution, context.ArchitecturesList(), components, sources, context.CollectionFactory())
|
||||
published, err := deb.NewPublishedRepo(storage, prefix, distribution, context.ArchitecturesList(), components, sources, collectionFactory, multiDist)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
published.Origin = cmd.Flag.Lookup("origin").Value.String()
|
||||
published.Label = cmd.Flag.Lookup("label").Value.String()
|
||||
if origin != "" {
|
||||
published.Origin = origin
|
||||
}
|
||||
if notAutomatic != "" {
|
||||
published.NotAutomatic = notAutomatic
|
||||
}
|
||||
if butAutomaticUpgrades != "" {
|
||||
published.ButAutomaticUpgrades = butAutomaticUpgrades
|
||||
}
|
||||
published.Label = context.Flags().Lookup("label").Value.String()
|
||||
published.Suite = context.Flags().Lookup("suite").Value.String()
|
||||
published.Codename = context.Flags().Lookup("codename").Value.String()
|
||||
|
||||
duplicate := context.CollectionFactory().PublishedRepoCollection().CheckDuplicate(published)
|
||||
published.SkipContents = context.Config().SkipContentsPublishing
|
||||
|
||||
if context.Flags().IsSet("skip-contents") {
|
||||
published.SkipContents = context.Flags().Lookup("skip-contents").Value.Get().(bool)
|
||||
}
|
||||
|
||||
published.SkipBz2 = context.Config().SkipBz2Publishing
|
||||
if context.Flags().IsSet("skip-bz2") {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("acquire-by-hash") {
|
||||
published.AcquireByHash = context.Flags().Lookup("acquire-by-hash").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
duplicate := collectionFactory.PublishedRepoCollection().CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
context.CollectionFactory().PublishedRepoCollection().LoadComplete(duplicate, context.CollectionFactory())
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
return fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
|
||||
}
|
||||
|
||||
signer, err := getSigner(context.flags)
|
||||
signer, err := getSigner(context.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize GPG signer: %s", err)
|
||||
}
|
||||
|
||||
forceOverwrite := context.flags.Lookup("force-overwrite").Value.Get().(bool)
|
||||
forceOverwrite := context.Flags().Lookup("force-overwrite").Value.Get().(bool)
|
||||
if forceOverwrite {
|
||||
context.Progress().ColoredPrintf("@rWARNING@|: force overwrite mode enabled, aptly might corrupt other published repositories sharing " +
|
||||
"the same package pool.\n")
|
||||
context.Progress().ColoredPrintf("@rWARNING@|: force overwrite mode enabled, aptly might corrupt other published repositories sharing the same package pool.\n")
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), forceOverwrite)
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
}
|
||||
|
||||
err = context.CollectionFactory().PublishedRepoCollection().Add(published)
|
||||
err = collectionFactory.PublishedRepoCollection().Add(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
@@ -156,14 +190,14 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
|
||||
context.Progress().Printf("\n%s been successfully published.\n", message)
|
||||
|
||||
if localStorage, ok := context.GetPublishedStorage(storage).(aptly.LocalPublishedStorage); ok {
|
||||
if localStorage, ok := context.GetPublishedStorage(storage).(aptly.FileSystemPublishedStorage); ok {
|
||||
context.Progress().Printf("Please setup your webserver to serve directory '%s' with autoindexing.\n",
|
||||
localStorage.PublicPath())
|
||||
}
|
||||
|
||||
context.Progress().Printf("Now you can add following line to apt sources:\n")
|
||||
context.Progress().Printf(" deb http://your-server/%s %s %s\n", prefix, distribution, repoComponents)
|
||||
if utils.StrSliceHasItem(published.Architectures, "source") {
|
||||
if utils.StrSliceHasItem(published.Architectures, deb.ArchitectureSource) {
|
||||
context.Progress().Printf(" deb-src http://your-server/%s %s %s\n", prefix, distribution, repoComponents)
|
||||
}
|
||||
context.Progress().Printf("Don't forget to add your GPG key to apt with apt-key.\n")
|
||||
@@ -199,12 +233,21 @@ Example:
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passhprase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passhprase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
cmd.Flag.String("passphrase-file", "", "GPG passphrase-file for the key (warning: could be insecure)")
|
||||
cmd.Flag.Bool("batch", false, "run GPG with detached tty")
|
||||
cmd.Flag.Bool("skip-signing", false, "don't sign Release files with GPG")
|
||||
cmd.Flag.String("origin", "", "origin name to publish")
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("origin", "", "overwrite origin name to publish")
|
||||
cmd.Flag.String("notautomatic", "", "overwrite value for NotAutomatic field")
|
||||
cmd.Flag.String("butautomaticupgrades", "", "overwrite value for ButAutomaticUpgrades field")
|
||||
cmd.Flag.String("label", "", "label to publish")
|
||||
cmd.Flag.String("suite", "", "suite to publish (defaults to distribution)")
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
)
|
||||
|
||||
func aptlyPublishSourceAdd(cmd *commander.Command, args []string) error {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
return commander.ErrCommandError
|
||||
}
|
||||
|
||||
distribution := args[0]
|
||||
names := args[1:]
|
||||
components := strings.Split(context.Flags().Lookup("component").Value.String(), ",")
|
||||
|
||||
if len(names) != len(components) {
|
||||
return fmt.Errorf("mismatch in number of components (%d) and sources (%d)", len(components), len(names))
|
||||
}
|
||||
|
||||
prefix := context.Flags().Lookup("prefix").Value.String()
|
||||
storage, prefix := deb.ParsePrefix(prefix)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
published, err := collectionFactory.PublishedRepoCollection().ByStoragePrefixDistribution(storage, prefix, distribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().LoadComplete(published, collectionFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to add: %s", err)
|
||||
}
|
||||
|
||||
revision := published.ObtainRevision()
|
||||
sources := revision.Sources
|
||||
|
||||
for i, component := range components {
|
||||
name := names[i]
|
||||
_, exists := sources[component]
|
||||
if exists {
|
||||
return fmt.Errorf("unable to add: component '%s' has already been added", component)
|
||||
}
|
||||
context.Progress().Printf("Adding component '%s' with source '%s' [%s]...\n", component, name, published.SourceKind)
|
||||
|
||||
sources[component] = name
|
||||
}
|
||||
|
||||
err = collectionFactory.PublishedRepoCollection().Update(published)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save to DB: %s", err)
|
||||
}
|
||||
|
||||
context.Progress().Printf("\nYou can run 'aptly publish update %s %s' to update the content of the published repository.\n",
|
||||
distribution, published.StoragePrefix())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func makeCmdPublishSourceAdd() *commander.Command {
|
||||
cmd := &commander.Command{
|
||||
Run: aptlyPublishSourceAdd,
|
||||
UsageLine: "add <distribution> <source>",
|
||||
Short: "add source components to a published repo",
|
||||
Long: `
|
||||
The command adds components of a snapshot or local repository to be published.
|
||||
|
||||
This does not publish the changes directly, but rather schedules them for a subsequent 'aptly publish update'.
|
||||
|
||||
The flag -component is mandatory. Use a comma-separated list of components, if
|
||||
multiple components should be modified. The number of given components must be
|
||||
equal to the number of given sources, e.g.:
|
||||
|
||||
aptly publish source add -component=main,contrib wheezy wheezy-main wheezy-contrib
|
||||
|
||||
Example:
|
||||
|
||||
$ aptly publish source add -component=contrib wheezy ppa wheezy-contrib
|
||||
|
||||
This command assigns the snapshot wheezy-contrib to the component contrib and
|
||||
adds it to published repository revision of ppa/wheezy.
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-source-add", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("prefix", ".", "publishing prefix in the form of [<endpoint>:]<prefix>")
|
||||
cmd.Flag.String("component", "", "component names to add (for multi-component publishing, separate components with commas)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user