[
  {
    "path": ".all-contributorsrc",
    "content": "{\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 100,\n  \"commit\": false,\n  \"contributors\": [\n    {\n      \"login\": \"piksel\",\n      \"name\": \"nils måsén\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/807383?v=4\",\n      \"profile\": \"https://piksel.se\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"maintenance\",\n        \"review\"\n      ]\n    },\n    {\n      \"login\": \"simskij\",\n      \"name\": \"Simon Aronsson\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1596025?v=4\",\n      \"profile\": \"http://simme.dev\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"maintenance\",\n        \"review\"\n      ]\n    },\n    {\n      \"login\": \"Codelica\",\n      \"name\": \"James\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/386101?v=4\",\n      \"profile\": \"http://codelica.com\",\n      \"contributions\": [\n        \"test\",\n        \"ideas\"\n      ]\n    },\n    {\n      \"login\": \"KopfKrieg\",\n      \"name\": \"Florian\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/5047813?v=4\",\n      \"profile\": \"https://kopfkrieg.org\",\n      \"contributions\": [\n        \"review\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"bdehamer\",\n      \"name\": \"Brian DeHamer\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/398027?v=4\",\n      \"profile\": \"https://github.com/bdehamer\",\n      \"contributions\": [\n        \"code\",\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"rosscado\",\n      \"name\": \"Ross Cadogan\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/16578183?v=4\",\n      \"profile\": \"https://github.com/rosscado\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"stffabi\",\n      \"name\": \"stffabi\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/9464631?v=4\",\n      \"profile\": \"https://github.com/stffabi\",\n      \"contributions\": [\n        \"code\",\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"ATCUSA\",\n      \"name\": \"Austin\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/3581228?v=4\",\n      \"profile\": \"https://github.com/ATCUSA\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"davidgardner11\",\n      \"name\": \"David Gardner\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/6181487?v=4\",\n      \"profile\": \"https://labs.ctl.io\",\n      \"contributions\": [\n        \"review\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"dolanor\",\n      \"name\": \"Tanguy ⧓ Herrmann\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/928722?v=4\",\n      \"profile\": \"https://github.com/dolanor\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"rdamazio\",\n      \"name\": \"Rodrigo Damazio Bovendorp\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/997641?v=4\",\n      \"profile\": \"https://github.com/rdamazio\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"thelamer\",\n      \"name\": \"Ryan Kuba\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/1852688?v=4\",\n      \"profile\": \"https://www.taisun.io/\",\n      \"contributions\": [\n        \"infra\"\n      ]\n    },\n    {\n      \"login\": \"cnrmck\",\n      \"name\": \"cnrmck\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/22061955?v=4\",\n      \"profile\": \"https://github.com/cnrmck\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"haswalt\",\n      \"name\": \"Harry Walter\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/338588?v=4\",\n      \"profile\": \"http://harrywalter.co.uk\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Robotex\",\n      \"name\": \"Robotex\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/74515?v=4\",\n      \"profile\": \"http://projectsperanza.com\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"ubergesundheit\",\n      \"name\": \"Gerald Pape\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1494211?v=4\",\n      \"profile\": \"http://geraldpape.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"fomk\",\n      \"name\": \"fomk\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/17636183?v=4\",\n      \"profile\": \"https://github.com/fomk\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"svengo\",\n      \"name\": \"Sven Gottwald\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/2502366?v=4\",\n      \"profile\": \"https://github.com/svengo\",\n      \"contributions\": [\n        \"infra\"\n      ]\n    },\n    {\n      \"login\": \"techknowlogick\",\n      \"name\": \"techknowlogick\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/164197?v=4\",\n      \"profile\": \"https://liberapay.com/techknowlogick/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"waja\",\n      \"name\": \"waja\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1449568?v=4\",\n      \"profile\": \"http://log.c5t.org/about/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"salbertson\",\n      \"name\": \"Scott Albertson\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/154463?v=4\",\n      \"profile\": \"http://scottalbertson.com\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"huddlesj\",\n      \"name\": \"Jason Huddleston\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/11966535?v=4\",\n      \"profile\": \"https://github.com/huddlesj\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"napstr\",\n      \"name\": \"Napster\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/6048348?v=4\",\n      \"profile\": \"https://npstr.space/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"darknode\",\n      \"name\": \"Maxim\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/809429?v=4\",\n      \"profile\": \"https://github.com/darknode\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mxschmitt\",\n      \"name\": \"Max Schmitt\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/17984549?v=4\",\n      \"profile\": \"https://schmitt.cat\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"cron410\",\n      \"name\": \"cron410\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/3082899?v=4\",\n      \"profile\": \"https://github.com/cron410\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"Cardoso222\",\n      \"name\": \"Paulo Henrique\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/7026517?v=4\",\n      \"profile\": \"https://github.com/Cardoso222\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"belak\",\n      \"name\": \"Kaleb Elwert\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/107097?v=4\",\n      \"profile\": \"https://coded.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"wmbutler\",\n      \"name\": \"Bill Butler\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1254810?v=4\",\n      \"profile\": \"https://github.com/wmbutler\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mariotacke\",\n      \"name\": \"Mario Tacke\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/4942019?v=4\",\n      \"profile\": \"https://www.mariotacke.io\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"mrw34\",\n      \"name\": \"Mark Woodbridge\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/1101318?v=4\",\n      \"profile\": \"https://markwoodbridge.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Ansem93\",\n      \"name\": \"Ansem93\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/6626218?v=4\",\n      \"profile\": \"https://github.com/Ansem93\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"lukapeschke\",\n      \"name\": \"Luka Peschke\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/17085536?v=4\",\n      \"profile\": \"https://github.com/lukapeschke\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"zoispag\",\n      \"name\": \"Zois Pagoulatos\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/21138205?v=4\",\n      \"profile\": \"https://github.com/zoispag\",\n      \"contributions\": [\n        \"code\",\n        \"review\",\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"alexandremenif\",\n      \"name\": \"Alexandre Menif\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/16152103?v=4\",\n      \"profile\": \"https://alexandre.menif.name\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"chugunov\",\n      \"name\": \"Andrey\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/4140479?v=4\",\n      \"profile\": \"https://github.com/chugunov\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"noplanman\",\n      \"name\": \"Armando Lüscher\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/9423417?v=4\",\n      \"profile\": \"https://noplanman.ch\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"rjbudke\",\n      \"name\": \"Ryan Budke\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/273485?v=4\",\n      \"profile\": \"https://github.com/rjbudke\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"kaloyan-raev\",\n      \"name\": \"Kaloyan Raev\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/468091?v=4\",\n      \"profile\": \"http://kaloyan.raev.name\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"sixth\",\n      \"name\": \"sixth\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/11591445?v=4\",\n      \"profile\": \"https://github.com/sixth\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"foosel\",\n      \"name\": \"Gina Häußge\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/83657?v=4\",\n      \"profile\": \"https://foosel.net\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"8ear\",\n      \"name\": \"Max H.\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/10329648?v=4\",\n      \"profile\": \"https://github.com/8ear\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"pjknkda\",\n      \"name\": \"Jungkook Park\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/4986524?v=4\",\n      \"profile\": \"https://pjknkda.github.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"jnidzwetzki\",\n      \"name\": \"Jan Kristof Nidzwetzki\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/5753622?v=4\",\n      \"profile\": \"https://achfrag.net\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mindrunner\",\n      \"name\": \"lukas\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1413542?v=4\",\n      \"profile\": \"https://www.lukaselsner.de\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"codingCoffee\",\n      \"name\": \"Ameya Shenoy\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/13611153?v=4\",\n      \"profile\": \"https://codingcoffee.dev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"raymondelooff\",\n      \"name\": \"Raymon de Looff\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/9716806?v=4\",\n      \"profile\": \"https://github.com/raymondelooff\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jsclayton\",\n      \"name\": \"John Clayton\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/704034?v=4\",\n      \"profile\": \"http://codemonkeylabs.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Germs2004\",\n      \"name\": \"Germs2004\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/5519340?v=4\",\n      \"profile\": \"https://github.com/Germs2004\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"lukwil\",\n      \"name\": \"Lukas Willburger\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/30203234?v=4\",\n      \"profile\": \"https://github.com/lukwil\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"auanasgheps\",\n      \"name\": \"Oliver Cervera\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/20586878?v=4\",\n      \"profile\": \"https://github.com/auanasgheps\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"victorcmoura\",\n      \"name\": \"Victor Moura\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/26290053?v=4\",\n      \"profile\": \"https://github.com/victorcmoura\",\n      \"contributions\": [\n        \"test\",\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mbrandau\",\n      \"name\": \"Maximilian Brandau\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/12972798?v=4\",\n      \"profile\": \"https://github.com/mbrandau\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"aneisch\",\n      \"name\": \"Andrew\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/6991461?v=4\",\n      \"profile\": \"https://github.com/aneisch\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"sixcorners\",\n      \"name\": \"sixcorners\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/585501?v=4\",\n      \"profile\": \"https://github.com/sixcorners\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"arnested\",\n      \"name\": \"Arne Jørgensen\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/190005?v=4\",\n      \"profile\": \"https://arnested.dk\",\n      \"contributions\": [\n        \"test\",\n        \"review\"\n      ]\n    },\n    {\n      \"login\": \"patski123\",\n      \"name\": \"PatSki123\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/19295295?v=4\",\n      \"profile\": \"https://github.com/patski123\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"Saicheg\",\n      \"name\": \"Valentine Zavadsky\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/624999?v=4\",\n      \"profile\": \"https://rubyroidlabs.com/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"bopoh24\",\n      \"name\": \"Alexander Voronin\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/4086631?v=4\",\n      \"profile\": \"https://github.com/bopoh24\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"ogmueller\",\n      \"name\": \"Oliver Mueller\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/788989?v=4\",\n      \"profile\": \"http://www.teqneers.de\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"tammert\",\n      \"name\": \"Sebastiaan Tammer\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/8885250?v=4\",\n      \"profile\": \"https://github.com/tammert\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"miosame\",\n      \"name\": \"miosame\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/8201077?v=4\",\n      \"profile\": \"https://github.com/Miosame\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"andrewjmetzger\",\n      \"name\": \"Andrew Metzger\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/590246?v=4\",\n      \"profile\": \"https://mtz.gr\",\n      \"contributions\": [\n        \"bug\",\n        \"example\"\n      ]\n    },\n    {\n      \"login\": \"pgrimaud\",\n      \"name\": \"Pierre Grimaud\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1866496?v=4\",\n      \"profile\": \"https://github.com/pgrimaud\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mattdoran\",\n      \"name\": \"Matt Doran\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/577779?v=4\",\n      \"profile\": \"https://github.com/mattdoran\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"MihailITPlace\",\n      \"name\": \"MihailITPlace\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/28401551?v=4\",\n      \"profile\": \"https://github.com/MihailITPlace\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"bugficks\",\n      \"name\": \"bugficks\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/2992895?v=4\",\n      \"profile\": \"https://github.com/bugficks\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"MichaelSp\",\n      \"name\": \"Michael\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/448282?v=4\",\n      \"profile\": \"https://github.com/MichaelSp\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jokay\",\n      \"name\": \"D. Domig\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/18613935?v=4\",\n      \"profile\": \"https://github.com/jokay\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"osheroff\",\n      \"name\": \"Ben Osheroff\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/260084?v=4\",\n      \"profile\": \"https://maxwells-daemon.io\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"dhet\",\n      \"name\": \"David H.\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/2668621?v=4\",\n      \"profile\": \"https://github.com/dhet\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"chander\",\n      \"name\": \"Chander Ganesan\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/671887?v=4\",\n      \"profile\": \"http://www.gridgeo.com\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"yrien30\",\n      \"name\": \"yrien30\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/26816162?v=4\",\n      \"profile\": \"https://github.com/yrien30\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"ksurl\",\n      \"name\": \"ksurl\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1371562?v=4\",\n      \"profile\": \"https://github.com/ksurl\",\n      \"contributions\": [\n        \"doc\",\n        \"code\",\n        \"infra\"\n      ]\n    },\n    {\n      \"login\": \"rg9400\",\n      \"name\": \"rg9400\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/39887349?v=4\",\n      \"profile\": \"https://github.com/rg9400\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"tkalus\",\n      \"name\": \"Turtle Kalus\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/287181?v=4\",\n      \"profile\": \"https://github.com/tkalus\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"SrihariThalla\",\n      \"name\": \"Srihari Thalla\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/7479937?v=4\",\n      \"profile\": \"https://github.com/SrihariThalla\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"nymous\",\n      \"name\": \"Thomas Gaudin\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/4216559?v=4\",\n      \"profile\": \"https://nymous.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"hydrargyrum\",\n      \"name\": \"hydrargyrum\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/2804645?v=4\",\n      \"profile\": \"https://indigo.re/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"reinout\",\n      \"name\": \"Reinout van Rees\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/121433?v=4\",\n      \"profile\": \"https://reinout.vanrees.org\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"DasSkelett\",\n      \"name\": \"DasSkelett\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/28812678?v=4\",\n      \"profile\": \"https://github.com/DasSkelett\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"zenjabba\",\n      \"name\": \"zenjabba\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/679864?v=4\",\n      \"profile\": \"https://github.com/zenjabba\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"djquan\",\n      \"name\": \"Dan Quan\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/3526705?v=4\",\n      \"profile\": \"https://quan.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"modem7\",\n      \"name\": \"modem7\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4349962?v=4\",\n      \"profile\": \"https://github.com/modem7\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"hypnoglow\",\n      \"name\": \"Igor Zibarev\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4853075?v=4\",\n      \"profile\": \"https://github.com/hypnoglow\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"patricegautier\",\n      \"name\": \"Patrice\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/38435239?v=4\",\n      \"profile\": \"https://github.com/patricegautier\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jamesmacwhite\",\n      \"name\": \"James White\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/8067792?v=4\",\n      \"profile\": \"http://jamesw.link/me\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"Foxite\",\n      \"name\": \"Dirk Kok\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/20421657?v=4\",\n      \"profile\": \"https://ko-fi.com/foxite\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"EDIflyer\",\n      \"name\": \"EDIflyer\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/13610277?v=4\",\n      \"profile\": \"https://github.com/EDIflyer\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"jauderho\",\n      \"name\": \"Jauder Ho\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/13562?v=4\",\n      \"profile\": \"https://github.com/jauderho\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"andriibratanin\",\n      \"name\": \"Andrii Bratanin\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/20169213?v=4\",\n      \"profile\": \"https://github.com/andriibratanin\"\n    },\n    {\n      \"login\": \"IAmTamal\",\n      \"name\": \"Tamal Das \",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/72851613?v=4\",\n      \"profile\": \"https://tamal.vercel.app/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"testwill\",\n      \"name\": \"guangwu\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/8717479?v=4\",\n      \"profile\": \"https://github.com/testwill\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"nothub\",\n      \"name\": \"Florian Hübner\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/48992448?v=4\",\n      \"profile\": \"http://hub.lol\",\n      \"contributions\": [\n        \"doc\",\n        \"code\"\n      ]\n    }\n  ],\n  \"contributorsPerLine\": 7,\n  \"projectName\": \"watchtower\",\n  \"projectOwner\": \"containrrr\",\n  \"repoType\": \"github\",\n  \"repoHost\": \"https://github.com\",\n  \"commitConvention\": \"none\",\n  \"skipCi\": true,\n  \"commitType\": \"docs\"\n}\n"
  },
  {
    "path": ".codacy.yml",
    "content": "---\nengines:\n  coverage:\n    exclude_paths:\n      - \"*.md\"\n      - \"**/*.md\""
  },
  {
    "path": ".devbots/lock-issue.yml",
    "content": "enabled: true\ncomment: >\n  To avoid important communication to get lost in a closed issues no one monitors, I'll go ahead and lock this issue.\n  If you want to continue the discussion, please open a new issue. Thank you! 🙏🏼\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\n\n[*.css]\nindent_style = space\nindent_size = 2\n\n[{go.mod,go.sum,*.go}]\nindent_style = tab\nindent_size = 4"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "pkg/notifications/smtp.go     @piksel\npkg/notifications/email.go    @piksel\npkg/notifications/shoutrrr.go @piksel @simskij @arnested\npkg/container/*               @simskij\npkg/api/*                     @victorcmoura\n.devbots/*                    @simskij\n.github/*                     @simskij\ndocs/*                        @containrrr/watchtower-contributors\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: 🐛 Bug report\ndescription: Create a report to help us improve\nlabels: [\"Priority: Medium, Status: Available, Type: Bug\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image. If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: Steps to reproduce\n      description: Steps to reproduce the behavior\n      value: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: A clear and concise description of what you expected to happen.\n    validations:\n      required: true\n\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screenshots\n      description: Please add screenshots if applicable\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Environment\n      description: We would want to know the following things\n      value: |\n        - Platform\n        - Architecture\n        - Docker Version\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Your logs\n      description: Paste the logs from running watchtower with the `--debug` option.\n      render: text\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask a question\n    url: https://github.com/containrrr/watchtower/discussions\n    about: Ask questions and discuss with other community members\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 💡 Feature request\ndescription: Have a new idea/feature ? Please suggest!\nlabels: [\"Priority: Low, Status: Available, Type: Enhancement\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Is your feature request related to a problem? Please describe.\n      description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: Describe the solution you'd like\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Describe alternatives you've considered\n      description: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: true\n\n  - type: textarea\n    id: extrainfo\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"gomod\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n      \n  - package-ecosystem: \"docker\" # See documentation for possible values\n    directory: \"/dockerfiles\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\n\nThank you for contributing to the watchtower project! 🙏\n\nWe truly appreciate all the contributions we get from the community.\n\nTo make your PR experience as smooth as possible, make sure that you\ninclude the following in your PR:\n\n- What your PR contributes\n- Which issues it solves (preferrably using auto closing instructions like \"closes #123\".\n- Tests that verify the code your contributing\n- Updates to the documentation\n\nThank you again! ✨\n\n-->\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "daysUntilStale: 60\ndaysUntilClose: 7\nexemptMilestones: true\nexemptLabels:\n  - \"Public Service Announcement\"\n  - \"Do not close\"\n  - \"Type: Bug\"\n  - \"Type: Security\"\nstaleLabel: \"Status: Stale\"\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\ncloseComment: false\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [main]\n  schedule:\n    - cron: '0 1 * * 4'\n  workflow_dispatch:\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        # Override automatic language detection by changing the below list\n        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']\n        language: ['go']\n        # Learn more...\n        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n      with:\n        # We must fetch at least the immediate parents so that if this is\n        # a pull request then we can checkout the head.\n        fetch-depth: 2\n\n    # If this run was triggered by a pull request event, then checkout\n    # the head of the pull request instead of the merge commit.\n    - run: git checkout HEAD^2\n      if: ${{ github.event_name == 'pull_request' }}\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n        # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v3\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 https://git.io/JvXDl\n\n    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n    #    and modify them (or add more) to build your code if your project\n    #    uses a compiled language\n\n    #- run: |\n    #   make bootstrap\n    #   make release\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/dependabot-approve.yml",
    "content": "name: Auto approve dependabot PRs\n\non: pull_request_target\n\njobs:\n  auto-approve:\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n    if: github.actor == 'dependabot[bot]'\n    steps:\n      - uses: hmarr/auto-approve-action@v3\n"
  },
  {
    "path": ".github/workflows/greetings.yml",
    "content": "name: Greetings\n\non:\n  # Runs in the context of the target (containrrr/watchtower) repository, and as such has access to GITHUB_TOKEN\n  pull_request_target:\n    types: [opened]\n  issues:\n    types: [opened]\n\njobs:\n  greeting:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/first-interaction@v1\n      with:\n        repo-token: ${{ secrets.GITHUB_TOKEN }}\n        issue-message: >\n          Hi there! 👋🏼\n          As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/.github/blob/master/CODE_OF_CONDUCT.md)\n          as well as our [contribution guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md).\n          Thanks a bunch for opening your first issue! 🙏\n        pr-message: >\n          Congratulations on opening your first pull request! We'll get back to you as soon as possible. In the meantime, please make sure you've updated the documentation to reflect your changes and have added test automation as needed. Thanks! 🙏🏼\n"
  },
  {
    "path": ".github/workflows/publish-docs.yml",
    "content": "name: Publish Docs\n\non:\n  workflow_dispatch: { }\n  workflow_run:\n    workflows: [ \"Release (Production)\" ]\n    branches: [ main ]\n    types:\n      - completed\n\njobs:\n  publish-docs:\n    name: Publish Docs\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Build tplprev\n        run: scripts/build-tplprev.sh\n      - name: Setup python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.10'\n          cache: 'pip'\n          cache-dependency-path: |\n            docs-requirements.txt\n      - name: Install mkdocs\n        run: |\n          pip install -r docs-requirements.txt\n      - name: Generate docs\n        run: mkdocs gh-deploy --strict\n"
  },
  {
    "path": ".github/workflows/pull-request.yml",
    "content": "name: Pull Request\n\non:\n  workflow_dispatch: {}\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0\n        with:\n          version: \"2023.1.6\"\n          install-go: \"false\" # StaticCheck uses go v1.17 which does not support `any`\n  test:\n    name: Test\n    strategy:\n      fail-fast: false\n      matrix:\n        go-version:\n          - 1.20.x\n        platform:\n          - macos-latest\n          - windows-latest\n          - ubuntu-latest\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Run tests\n        run: |\n          go test -v -coverprofile coverage.out -covermode atomic ./... \n      - name: Publish coverage\n        uses: codecov/codecov-action@v3\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Build\n        uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3\n        with:\n          version: v0.155.0\n          args: --snapshot --skip-publish --debug\n"
  },
  {
    "path": ".github/workflows/release-dev.yaml",
    "content": "name: Push to main\n\non:\n  workflow_dispatch: {}\n  push:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Build\n        run: ./build.sh\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Test\n        run: go test -v -coverprofile coverage.out -covermode atomic ./... \n      - name: Publish coverage\n        uses: codecov/codecov-action@v3\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n  publish:\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Publish to Docker Hub\n        uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\n          file: dockerfiles/Dockerfile.self-contained\n          repository: containrrr/watchtower\n          tags: latest-dev\n      - name: Publish to GHCR\n        uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14\n        with:\n          username: ${{ secrets.BOT_USERNAME }}\n          password: ${{ secrets.BOT_GHCR_PAT }}\n          file: dockerfiles/Dockerfile.self-contained\n          registry: ghcr.io\n          repository: containrrr/watchtower\n          tags: latest-dev\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release (Production)\n\non:\n  workflow_dispatch: {}\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+'\n      - '**/v[0-9]+.[0-9]+.[0-9]+'\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0\n        with:\n          version: \"2022.1.1\"\n          install-go: \"false\" # StaticCheck uses go v1.17 which does not support `any`\n\n  test:\n    name: Test\n    strategy:\n      matrix:\n        go-version:\n          - 1.20.x\n        platform:\n          - ubuntu-latest\n          - macos-latest\n          - windows-latest\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Run tests\n        run: |\n          go test ./... -coverprofile coverage.out\n\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    needs: \n      - test\n      - lint\n    env:\n      CGO_ENABLED: 0\n      TAG: ${{ github.ref_name }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20.x\n      - name: Login to Docker Hub\n        uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Login to GHCR\n        uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2\n        with:\n          username: ${{ secrets.BOT_USERNAME }}\n          password: ${{ secrets.BOT_GHCR_PAT }}\n          registry: ghcr.io\n      - name: Build\n        uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3\n        with:\n          version: v0.155.0\n          args: --debug\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Enable experimental docker features\n        run: |\n          mkdir -p ~/.docker/ && \\\n          echo '{\"experimental\": \"enabled\"}' > ~/.docker/config.json\n      - name: Create manifest for version\n        run: |\n          export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//')\n          docker manifest create \\\n            containrrr/watchtower:$DH_TAG \\\n            containrrr/watchtower:amd64-$DH_TAG \\\n            containrrr/watchtower:i386-$DH_TAG \\\n            containrrr/watchtower:armhf-$DH_TAG \\\n            containrrr/watchtower:arm64v8-$DH_TAG\n          docker manifest create \\\n            ghcr.io/containrrr/watchtower:$DH_TAG \\\n            ghcr.io/containrrr/watchtower:amd64-$DH_TAG \\\n            ghcr.io/containrrr/watchtower:i386-$DH_TAG \\\n            ghcr.io/containrrr/watchtower:armhf-$DH_TAG \\\n            ghcr.io/containrrr/watchtower:arm64v8-$DH_TAG\n      - name: Annotate manifest for version\n        run: |\n          for REPO in '' ghcr.io/ ; do\n          \n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \\\n            ${REPO}containrrr/watchtower:i386-$(echo $TAG | sed 's/^v*//') \\\n            --os linux \\\n            --arch 386\n          \n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \\\n            ${REPO}containrrr/watchtower:armhf-$(echo $TAG | sed 's/^v*//') \\\n            --os linux \\\n            --arch arm\n      \n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \\\n            ${REPO}containrrr/watchtower:arm64v8-$(echo $TAG | sed 's/^v*//') \\\n            --os linux \\\n            --arch arm64 \\\n            --variant v8\n            \n            done\n      - name: Create manifest for latest\n        run: |\n          docker manifest create \\\n            containrrr/watchtower:latest \\\n            containrrr/watchtower:amd64-latest \\\n            containrrr/watchtower:i386-latest \\\n            containrrr/watchtower:armhf-latest \\\n            containrrr/watchtower:arm64v8-latest\n          docker manifest create \\\n            ghcr.io/containrrr/watchtower:latest \\\n            ghcr.io/containrrr/watchtower:amd64-latest \\\n            ghcr.io/containrrr/watchtower:i386-latest \\\n            ghcr.io/containrrr/watchtower:armhf-latest \\\n            ghcr.io/containrrr/watchtower:arm64v8-latest\n      - name: Annotate manifest for latest\n        run: |\n          for REPO in '' ghcr.io/ ; do\n\n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:latest \\\n            ${REPO}containrrr/watchtower:i386-latest \\\n            --os linux \\\n            --arch 386\n      \n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:latest \\\n            ${REPO}containrrr/watchtower:armhf-latest \\\n            --os linux \\\n            --arch arm\n            \n          docker manifest annotate \\\n            ${REPO}containrrr/watchtower:latest \\\n            ${REPO}containrrr/watchtower:arm64v8-latest \\\n            --os linux \\\n            --arch arm64 \\\n            --variant v8\n\n          done\n      - name: Push manifests to Dockerhub\n        env:\n          DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }}\n          DOCKER_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}\n        run: |\n          docker login -u $DOCKER_USER -p $DOCKER_TOKEN && \\\n            docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \\\n            docker manifest push containrrr/watchtower:latest\n      - name: Push manifests to GitHub Container Registry\n        env:\n          DOCKER_USER: ${{ secrets.BOT_USERNAME }}\n          DOCKER_TOKEN: ${{ secrets.BOT_GHCR_PAT }}\n        run: |\n          docker login -u $DOCKER_USER -p $DOCKER_TOKEN ghcr.io && \\\n            docker manifest push ghcr.io/containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \\\n            docker manifest push ghcr.io/containrrr/watchtower:latest\n\n  renew-docs:\n    name: Refresh pkg.go.dev\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n    - name: Pull new module version\n      uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14\n\n  \n  \n\n  \n"
  },
  {
    "path": ".gitignore",
    "content": "watchtower\nwatchtower.exe\nvendor\n.glide\ndist\n.idea\n.DS_Store\n/site\ncoverage.out\n*.coverprofile\n\ndocs/assets/wasm_exec.js\ndocs/assets/*.wasm"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Prerequisites\nTo contribute code changes to this project you will need the following development kits.\n * [Go](https://golang.org/doc/install)\n * [Docker](https://docs.docker.com/engine/installation/)\n \nAs watchtower utilizes go modules for vendor locking, you'll need at least Go 1.11.\nYou can check your current version of the go language as follows:\n```bash\n  ~ $ go version\n  go version go1.12.1 darwin/amd64\n```\n\n\n## Checking out the code\nDo not place your code in the go source path.\n```bash\ngit clone git@github.com:<yourfork>/watchtower.git\ncd watchtower\n```\n\n## Building and testing\nwatchtower is a go application and is built with go commands. The following commands assume that you are at the root level of your repo.\n```bash\ngo build                               # compiles and packages an executable binary, watchtower\ngo test ./... -v                       # runs tests with verbose output\n./watchtower                           # runs the application (outside of a container)\n```\n\nIf you dont have it enabled, you'll either have to prefix each command with `GO111MODULE=on` or run `export GO111MODULE=on` before running the commands. [You can read more about modules here.](https://github.com/golang/go/wiki/Modules)\n\nTo build a Watchtower image of your own, use the self-contained Dockerfiles. As the main Dockerfile, they can be found in `dockerfiles/`:\n- `dockerfiles/Dockerfile.dev-self-contained` will build an image based on your current local Watchtower files.\n- `dockerfiles/Dockerfile.self-contained` will build an image based on current Watchtower's repository on GitHub.\n\ne.g.:\n```bash\nsudo docker build . -f dockerfiles/Dockerfile.dev-self-contained -t containrrr/watchtower # to build an image from local files\n```"
  },
  {
    "path": "LICENSE.md",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2015 Watchtower contributors\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\nWatchtower contains code that is licensed under a BSD-license:\n - Copyright (c) 2009 The Go Authors. All rights reserved.\n   \n   For details see https://golang.org/LICENSE\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n  ### ⚠️ This project is no longer maintained\n  See https://github.com/containrrr/watchtower/discussions/2135 for details.\n\n  ---\n  \n  <img src=\"./logo.png\" width=\"450\" />\n  \n  # Watchtower\n  \n  A process for automating Docker container base image updates.\n  <br/><br/>\n  \n  [![Circle CI](https://circleci.com/gh/containrrr/watchtower.svg?style=shield)](https://circleci.com/gh/containrrr/watchtower)\n  [![codecov](https://codecov.io/gh/containrrr/watchtower/branch/main/graph/badge.svg)](https://codecov.io/gh/containrrr/watchtower)\n  [![GoDoc](https://godoc.org/github.com/containrrr/watchtower?status.svg)](https://godoc.org/github.com/containrrr/watchtower)\n  [![Go Report Card](https://goreportcard.com/badge/github.com/containrrr/watchtower)](https://goreportcard.com/report/github.com/containrrr/watchtower)\n  [![latest version](https://img.shields.io/github/tag/containrrr/watchtower.svg)](https://github.com/containrrr/watchtower/releases)\n  [![Apache-2.0 License](https://img.shields.io/github/license/containrrr/watchtower.svg)](https://www.apache.org/licenses/LICENSE-2.0)\n  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8)](https://www.codacy.com/gh/containrrr/watchtower/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=containrrr/watchtower&amp;utm_campaign=Badge_Grade)\n  [![All Contributors](https://img.shields.io/github/all-contributors/containrrr/watchtower)](#contributors)\n  [![Pulls from DockerHub](https://img.shields.io/docker/pulls/containrrr/watchtower.svg)](https://hub.docker.com/r/containrrr/watchtower)\n\n</div>\n\n## Quick Start\n\nWith watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. \n\nWatchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially. Run the watchtower container with the following command:\n\n```\n$ docker run --detach \\\n    --name watchtower \\\n    --volume /var/run/docker.sock:/var/run/docker.sock \\\n    containrrr/watchtower\n```\n\nWatchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do **not** recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like [MicroK8s](https://microk8s.io/) and [k3s](https://k3s.io/) that take away a lot of the toil of running a Kubernetes cluster. \n\n## Documentation\nThe full documentation is available at https://containrrr.dev/watchtower.\n\n## Contributors\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://piksel.se\"><img src=\"https://avatars2.githubusercontent.com/u/807383?v=4?s=100\" width=\"100px;\" alt=\"nils måsén\"/><br /><sub><b>nils måsén</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=piksel\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=piksel\" title=\"Documentation\">📖</a> <a href=\"#maintenance-piksel\" title=\"Maintenance\">🚧</a> <a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Apiksel\" title=\"Reviewed Pull Requests\">👀</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://simme.dev\"><img src=\"https://avatars0.githubusercontent.com/u/1596025?v=4?s=100\" width=\"100px;\" alt=\"Simon Aronsson\"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=simskij\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=simskij\" title=\"Documentation\">📖</a> <a href=\"#maintenance-simskij\" title=\"Maintenance\">🚧</a> <a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij\" title=\"Reviewed Pull Requests\">👀</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://codelica.com\"><img src=\"https://avatars3.githubusercontent.com/u/386101?v=4?s=100\" width=\"100px;\" alt=\"James\"/><br /><sub><b>James</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Codelica\" title=\"Tests\">⚠️</a> <a href=\"#ideas-Codelica\" title=\"Ideas, Planning, & Feedback\">🤔</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://kopfkrieg.org\"><img src=\"https://avatars2.githubusercontent.com/u/5047813?v=4?s=100\" width=\"100px;\" alt=\"Florian\"/><br /><sub><b>Florian</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=KopfKrieg\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/bdehamer\"><img src=\"https://avatars1.githubusercontent.com/u/398027?v=4?s=100\" width=\"100px;\" alt=\"Brian DeHamer\"/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=bdehamer\" title=\"Code\">💻</a> <a href=\"#maintenance-bdehamer\" title=\"Maintenance\">🚧</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/rosscado\"><img src=\"https://avatars1.githubusercontent.com/u/16578183?v=4?s=100\" width=\"100px;\" alt=\"Ross Cadogan\"/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=rosscado\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/stffabi\"><img src=\"https://avatars0.githubusercontent.com/u/9464631?v=4?s=100\" width=\"100px;\" alt=\"stffabi\"/><br /><sub><b>stffabi</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=stffabi\" title=\"Code\">💻</a> <a href=\"#maintenance-stffabi\" title=\"Maintenance\">🚧</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/ATCUSA\"><img src=\"https://avatars3.githubusercontent.com/u/3581228?v=4?s=100\" width=\"100px;\" alt=\"Austin\"/><br /><sub><b>Austin</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=ATCUSA\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://labs.ctl.io\"><img src=\"https://avatars2.githubusercontent.com/u/6181487?v=4?s=100\" width=\"100px;\" alt=\"David Gardner\"/><br /><sub><b>David Gardner</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=davidgardner11\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/dolanor\"><img src=\"https://avatars3.githubusercontent.com/u/928722?v=4?s=100\" width=\"100px;\" alt=\"Tanguy ⧓ Herrmann\"/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=dolanor\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/rdamazio\"><img src=\"https://avatars3.githubusercontent.com/u/997641?v=4?s=100\" width=\"100px;\" alt=\"Rodrigo Damazio Bovendorp\"/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=rdamazio\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=rdamazio\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.taisun.io/\"><img src=\"https://avatars3.githubusercontent.com/u/1852688?v=4?s=100\" width=\"100px;\" alt=\"Ryan Kuba\"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href=\"#infra-thelamer\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/cnrmck\"><img src=\"https://avatars2.githubusercontent.com/u/22061955?v=4?s=100\" width=\"100px;\" alt=\"cnrmck\"/><br /><sub><b>cnrmck</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=cnrmck\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://harrywalter.co.uk\"><img src=\"https://avatars3.githubusercontent.com/u/338588?v=4?s=100\" width=\"100px;\" alt=\"Harry Walter\"/><br /><sub><b>Harry Walter</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=haswalt\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://projectsperanza.com\"><img src=\"https://avatars3.githubusercontent.com/u/74515?v=4?s=100\" width=\"100px;\" alt=\"Robotex\"/><br /><sub><b>Robotex</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Robotex\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://geraldpape.io\"><img src=\"https://avatars0.githubusercontent.com/u/1494211?v=4?s=100\" width=\"100px;\" alt=\"Gerald Pape\"/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=ubergesundheit\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/fomk\"><img src=\"https://avatars0.githubusercontent.com/u/17636183?v=4?s=100\" width=\"100px;\" alt=\"fomk\"/><br /><sub><b>fomk</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=fomk\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/svengo\"><img src=\"https://avatars3.githubusercontent.com/u/2502366?v=4?s=100\" width=\"100px;\" alt=\"Sven Gottwald\"/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href=\"#infra-svengo\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://liberapay.com/techknowlogick/\"><img src=\"https://avatars1.githubusercontent.com/u/164197?v=4?s=100\" width=\"100px;\" alt=\"techknowlogick\"/><br /><sub><b>techknowlogick</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=techknowlogick\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://log.c5t.org/about/\"><img src=\"https://avatars1.githubusercontent.com/u/1449568?v=4?s=100\" width=\"100px;\" alt=\"waja\"/><br /><sub><b>waja</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=waja\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://scottalbertson.com\"><img src=\"https://avatars2.githubusercontent.com/u/154463?v=4?s=100\" width=\"100px;\" alt=\"Scott Albertson\"/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=salbertson\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/huddlesj\"><img src=\"https://avatars1.githubusercontent.com/u/11966535?v=4?s=100\" width=\"100px;\" alt=\"Jason Huddleston\"/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=huddlesj\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://npstr.space/\"><img src=\"https://avatars3.githubusercontent.com/u/6048348?v=4?s=100\" width=\"100px;\" alt=\"Napster\"/><br /><sub><b>Napster</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=napstr\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/darknode\"><img src=\"https://avatars1.githubusercontent.com/u/809429?v=4?s=100\" width=\"100px;\" alt=\"Maxim\"/><br /><sub><b>Maxim</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=darknode\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=darknode\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://schmitt.cat\"><img src=\"https://avatars0.githubusercontent.com/u/17984549?v=4?s=100\" width=\"100px;\" alt=\"Max Schmitt\"/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mxschmitt\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/cron410\"><img src=\"https://avatars1.githubusercontent.com/u/3082899?v=4?s=100\" width=\"100px;\" alt=\"cron410\"/><br /><sub><b>cron410</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=cron410\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Cardoso222\"><img src=\"https://avatars3.githubusercontent.com/u/7026517?v=4?s=100\" width=\"100px;\" alt=\"Paulo Henrique\"/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Cardoso222\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://coded.io\"><img src=\"https://avatars0.githubusercontent.com/u/107097?v=4?s=100\" width=\"100px;\" alt=\"Kaleb Elwert\"/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=belak\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/wmbutler\"><img src=\"https://avatars1.githubusercontent.com/u/1254810?v=4?s=100\" width=\"100px;\" alt=\"Bill Butler\"/><br /><sub><b>Bill Butler</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=wmbutler\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.mariotacke.io\"><img src=\"https://avatars2.githubusercontent.com/u/4942019?v=4?s=100\" width=\"100px;\" alt=\"Mario Tacke\"/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mariotacke\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://markwoodbridge.com\"><img src=\"https://avatars2.githubusercontent.com/u/1101318?v=4?s=100\" width=\"100px;\" alt=\"Mark Woodbridge\"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mrw34\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Ansem93\"><img src=\"https://avatars3.githubusercontent.com/u/6626218?v=4?s=100\" width=\"100px;\" alt=\"Ansem93\"/><br /><sub><b>Ansem93</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Ansem93\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/lukapeschke\"><img src=\"https://avatars1.githubusercontent.com/u/17085536?v=4?s=100\" width=\"100px;\" alt=\"Luka Peschke\"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=lukapeschke\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=lukapeschke\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/zoispag\"><img src=\"https://avatars0.githubusercontent.com/u/21138205?v=4?s=100\" width=\"100px;\" alt=\"Zois Pagoulatos\"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=zoispag\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag\" title=\"Reviewed Pull Requests\">👀</a> <a href=\"#maintenance-zoispag\" title=\"Maintenance\">🚧</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://alexandre.menif.name\"><img src=\"https://avatars0.githubusercontent.com/u/16152103?v=4?s=100\" width=\"100px;\" alt=\"Alexandre Menif\"/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=alexandremenif\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/chugunov\"><img src=\"https://avatars1.githubusercontent.com/u/4140479?v=4?s=100\" width=\"100px;\" alt=\"Andrey\"/><br /><sub><b>Andrey</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=chugunov\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://noplanman.ch\"><img src=\"https://avatars3.githubusercontent.com/u/9423417?v=4?s=100\" width=\"100px;\" alt=\"Armando Lüscher\"/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=noplanman\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/rjbudke\"><img src=\"https://avatars2.githubusercontent.com/u/273485?v=4?s=100\" width=\"100px;\" alt=\"Ryan Budke\"/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=rjbudke\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://kaloyan.raev.name\"><img src=\"https://avatars2.githubusercontent.com/u/468091?v=4?s=100\" width=\"100px;\" alt=\"Kaloyan Raev\"/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=kaloyan-raev\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=kaloyan-raev\" title=\"Tests\">⚠️</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/sixth\"><img src=\"https://avatars3.githubusercontent.com/u/11591445?v=4?s=100\" width=\"100px;\" alt=\"sixth\"/><br /><sub><b>sixth</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=sixth\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://foosel.net\"><img src=\"https://avatars0.githubusercontent.com/u/83657?v=4?s=100\" width=\"100px;\" alt=\"Gina Häußge\"/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=foosel\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/8ear\"><img src=\"https://avatars0.githubusercontent.com/u/10329648?v=4?s=100\" width=\"100px;\" alt=\"Max H.\"/><br /><sub><b>Max H.</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=8ear\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://pjknkda.github.io\"><img src=\"https://avatars0.githubusercontent.com/u/4986524?v=4?s=100\" width=\"100px;\" alt=\"Jungkook Park\"/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=pjknkda\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://achfrag.net\"><img src=\"https://avatars1.githubusercontent.com/u/5753622?v=4?s=100\" width=\"100px;\" alt=\"Jan Kristof Nidzwetzki\"/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=jnidzwetzki\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.lukaselsner.de\"><img src=\"https://avatars0.githubusercontent.com/u/1413542?v=4?s=100\" width=\"100px;\" alt=\"lukas\"/><br /><sub><b>lukas</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mindrunner\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://codingcoffee.dev\"><img src=\"https://avatars3.githubusercontent.com/u/13611153?v=4?s=100\" width=\"100px;\" alt=\"Ameya Shenoy\"/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=codingCoffee\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/raymondelooff\"><img src=\"https://avatars0.githubusercontent.com/u/9716806?v=4?s=100\" width=\"100px;\" alt=\"Raymon de Looff\"/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=raymondelooff\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://codemonkeylabs.com\"><img src=\"https://avatars2.githubusercontent.com/u/704034?v=4?s=100\" width=\"100px;\" alt=\"John Clayton\"/><br /><sub><b>John Clayton</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=jsclayton\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Germs2004\"><img src=\"https://avatars2.githubusercontent.com/u/5519340?v=4?s=100\" width=\"100px;\" alt=\"Germs2004\"/><br /><sub><b>Germs2004</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Germs2004\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/lukwil\"><img src=\"https://avatars1.githubusercontent.com/u/30203234?v=4?s=100\" width=\"100px;\" alt=\"Lukas Willburger\"/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=lukwil\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/auanasgheps\"><img src=\"https://avatars2.githubusercontent.com/u/20586878?v=4?s=100\" width=\"100px;\" alt=\"Oliver Cervera\"/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=auanasgheps\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/victorcmoura\"><img src=\"https://avatars1.githubusercontent.com/u/26290053?v=4?s=100\" width=\"100px;\" alt=\"Victor Moura\"/><br /><sub><b>Victor Moura</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=victorcmoura\" title=\"Tests\">⚠️</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=victorcmoura\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=victorcmoura\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/mbrandau\"><img src=\"https://avatars3.githubusercontent.com/u/12972798?v=4?s=100\" width=\"100px;\" alt=\"Maximilian Brandau\"/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mbrandau\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=mbrandau\" title=\"Tests\">⚠️</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/aneisch\"><img src=\"https://avatars1.githubusercontent.com/u/6991461?v=4?s=100\" width=\"100px;\" alt=\"Andrew\"/><br /><sub><b>Andrew</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=aneisch\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/sixcorners\"><img src=\"https://avatars0.githubusercontent.com/u/585501?v=4?s=100\" width=\"100px;\" alt=\"sixcorners\"/><br /><sub><b>sixcorners</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=sixcorners\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://arnested.dk\"><img src=\"https://avatars2.githubusercontent.com/u/190005?v=4?s=100\" width=\"100px;\" alt=\"Arne Jørgensen\"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=arnested\" title=\"Tests\">⚠️</a> <a href=\"https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested\" title=\"Reviewed Pull Requests\">👀</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/patski123\"><img src=\"https://avatars1.githubusercontent.com/u/19295295?v=4?s=100\" width=\"100px;\" alt=\"PatSki123\"/><br /><sub><b>PatSki123</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=patski123\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://rubyroidlabs.com/\"><img src=\"https://avatars2.githubusercontent.com/u/624999?v=4?s=100\" width=\"100px;\" alt=\"Valentine Zavadsky\"/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Saicheg\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=Saicheg\" title=\"Documentation\">📖</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=Saicheg\" title=\"Tests\">⚠️</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/bopoh24\"><img src=\"https://avatars2.githubusercontent.com/u/4086631?v=4?s=100\" width=\"100px;\" alt=\"Alexander Voronin\"/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=bopoh24\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24\" title=\"Bug reports\">🐛</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://www.teqneers.de\"><img src=\"https://avatars0.githubusercontent.com/u/788989?v=4?s=100\" width=\"100px;\" alt=\"Oliver Mueller\"/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=ogmueller\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/tammert\"><img src=\"https://avatars0.githubusercontent.com/u/8885250?v=4?s=100\" width=\"100px;\" alt=\"Sebastiaan Tammer\"/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=tammert\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Miosame\"><img src=\"https://avatars1.githubusercontent.com/u/8201077?v=4?s=100\" width=\"100px;\" alt=\"miosame\"/><br /><sub><b>miosame</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=miosame\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://mtz.gr\"><img src=\"https://avatars3.githubusercontent.com/u/590246?v=4?s=100\" width=\"100px;\" alt=\"Andrew Metzger\"/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger\" title=\"Bug reports\">🐛</a> <a href=\"#example-andrewjmetzger\" title=\"Examples\">💡</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/pgrimaud\"><img src=\"https://avatars1.githubusercontent.com/u/1866496?v=4?s=100\" width=\"100px;\" alt=\"Pierre Grimaud\"/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=pgrimaud\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/mattdoran\"><img src=\"https://avatars0.githubusercontent.com/u/577779?v=4?s=100\" width=\"100px;\" alt=\"Matt Doran\"/><br /><sub><b>Matt Doran</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=mattdoran\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/MihailITPlace\"><img src=\"https://avatars2.githubusercontent.com/u/28401551?v=4?s=100\" width=\"100px;\" alt=\"MihailITPlace\"/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=MihailITPlace\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/bugficks\"><img src=\"https://avatars1.githubusercontent.com/u/2992895?v=4?s=100\" width=\"100px;\" alt=\"bugficks\"/><br /><sub><b>bugficks</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=bugficks\" title=\"Code\">💻</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=bugficks\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/MichaelSp\"><img src=\"https://avatars0.githubusercontent.com/u/448282?v=4?s=100\" width=\"100px;\" alt=\"Michael\"/><br /><sub><b>Michael</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=MichaelSp\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jokay\"><img src=\"https://avatars0.githubusercontent.com/u/18613935?v=4?s=100\" width=\"100px;\" alt=\"D. Domig\"/><br /><sub><b>D. Domig</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=jokay\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://maxwells-daemon.io\"><img src=\"https://avatars1.githubusercontent.com/u/260084?v=4?s=100\" width=\"100px;\" alt=\"Ben Osheroff\"/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=osheroff\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/dhet\"><img src=\"https://avatars3.githubusercontent.com/u/2668621?v=4?s=100\" width=\"100px;\" alt=\"David H.\"/><br /><sub><b>David H.</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=dhet\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://www.gridgeo.com\"><img src=\"https://avatars1.githubusercontent.com/u/671887?v=4?s=100\" width=\"100px;\" alt=\"Chander Ganesan\"/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=chander\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/yrien30\"><img src=\"https://avatars1.githubusercontent.com/u/26816162?v=4?s=100\" width=\"100px;\" alt=\"yrien30\"/><br /><sub><b>yrien30</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=yrien30\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/ksurl\"><img src=\"https://avatars1.githubusercontent.com/u/1371562?v=4?s=100\" width=\"100px;\" alt=\"ksurl\"/><br /><sub><b>ksurl</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=ksurl\" title=\"Documentation\">📖</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=ksurl\" title=\"Code\">💻</a> <a href=\"#infra-ksurl\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/rg9400\"><img src=\"https://avatars2.githubusercontent.com/u/39887349?v=4?s=100\" width=\"100px;\" alt=\"rg9400\"/><br /><sub><b>rg9400</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=rg9400\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/tkalus\"><img src=\"https://avatars2.githubusercontent.com/u/287181?v=4?s=100\" width=\"100px;\" alt=\"Turtle Kalus\"/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=tkalus\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/SrihariThalla\"><img src=\"https://avatars1.githubusercontent.com/u/7479937?v=4?s=100\" width=\"100px;\" alt=\"Srihari Thalla\"/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=SrihariThalla\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://nymous.io\"><img src=\"https://avatars1.githubusercontent.com/u/4216559?v=4?s=100\" width=\"100px;\" alt=\"Thomas Gaudin\"/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=nymous\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://indigo.re/\"><img src=\"https://avatars.githubusercontent.com/u/2804645?v=4?s=100\" width=\"100px;\" alt=\"hydrargyrum\"/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=hydrargyrum\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://reinout.vanrees.org\"><img src=\"https://avatars.githubusercontent.com/u/121433?v=4?s=100\" width=\"100px;\" alt=\"Reinout van Rees\"/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=reinout\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/DasSkelett\"><img src=\"https://avatars.githubusercontent.com/u/28812678?v=4?s=100\" width=\"100px;\" alt=\"DasSkelett\"/><br /><sub><b>DasSkelett</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=DasSkelett\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/zenjabba\"><img src=\"https://avatars.githubusercontent.com/u/679864?v=4?s=100\" width=\"100px;\" alt=\"zenjabba\"/><br /><sub><b>zenjabba</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=zenjabba\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://quan.io\"><img src=\"https://avatars.githubusercontent.com/u/3526705?v=4?s=100\" width=\"100px;\" alt=\"Dan Quan\"/><br /><sub><b>Dan Quan</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=djquan\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/modem7\"><img src=\"https://avatars.githubusercontent.com/u/4349962?v=4?s=100\" width=\"100px;\" alt=\"modem7\"/><br /><sub><b>modem7</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=modem7\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/hypnoglow\"><img src=\"https://avatars.githubusercontent.com/u/4853075?v=4?s=100\" width=\"100px;\" alt=\"Igor Zibarev\"/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=hypnoglow\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/patricegautier\"><img src=\"https://avatars.githubusercontent.com/u/38435239?v=4?s=100\" width=\"100px;\" alt=\"Patrice\"/><br /><sub><b>Patrice</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=patricegautier\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://jamesw.link/me\"><img src=\"https://avatars.githubusercontent.com/u/8067792?v=4?s=100\" width=\"100px;\" alt=\"James White\"/><br /><sub><b>James White</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=jamesmacwhite\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://ko-fi.com/foxite\"><img src=\"https://avatars.githubusercontent.com/u/20421657?v=4?s=100\" width=\"100px;\" alt=\"Dirk Kok\"/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=Foxite\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/EDIflyer\"><img src=\"https://avatars.githubusercontent.com/u/13610277?v=4?s=100\" width=\"100px;\" alt=\"EDIflyer\"/><br /><sub><b>EDIflyer</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=EDIflyer\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jauderho\"><img src=\"https://avatars.githubusercontent.com/u/13562?v=4?s=100\" width=\"100px;\" alt=\"Jauder Ho\"/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=jauderho\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://tamal.vercel.app/\"><img src=\"https://avatars.githubusercontent.com/u/72851613?v=4?s=100\" width=\"100px;\" alt=\"Tamal Das \"/><br /><sub><b>Tamal Das </b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=IAmTamal\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/testwill\"><img src=\"https://avatars.githubusercontent.com/u/8717479?v=4?s=100\" width=\"100px;\" alt=\"guangwu\"/><br /><sub><b>guangwu</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=testwill\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://hub.lol\"><img src=\"https://avatars.githubusercontent.com/u/48992448?v=4?s=100\" width=\"100px;\" alt=\"Florian Hübner\"/><br /><sub><b>Florian Hübner</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=nothub\" title=\"Documentation\">📖</a> <a href=\"https://github.com/containrrr/watchtower/commits?author=nothub\" title=\"Code\">💻</a></td>\n      <td align=\"center\"><a href=\"https://github.com/andriibratanin\"><img src=\"https://avatars.githubusercontent.com/u/20169213?v=4?s=100\" width=\"100px;\" alt=\"\"/><br /><sub><b>Andrii Bratanin</b></sub></a><br /><a href=\"https://github.com/containrrr/watchtower/commits?author=andriibratanin\" title=\"Documentation\">📖</a></td>\n    </tr>\n  </tbody>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nSecurity updates will always only be applied to the latest version of Watchtower.\nAs the software by default is set to auto-update if you use the `latest` tag, you will get these security updates automatically as soon as they are released.\n\n## Reporting a Vulnerability\n\nCritical vulnerabilities that might open up for external attacks are best reported directly either to simme@arcticbit.se or nils@piksel.se.\nWe'll always try to get back to you as swiftly as possible, but keep in mind that since this is a community project, we can't really leave any guarantees about the speed.\n\nNon-critical vulnerabilities may be reported as regular GitHub issues.\n"
  },
  {
    "path": "build.sh",
    "content": "#!/bin/bash\n\nBINFILE=watchtower\nif [ -n \"$MSYSTEM\" ]; then\n    BINFILE=watchtower.exe\nfi\nVERSION=$(git describe --tags)\necho \"Building $VERSION...\"\ngo build -o $BINFILE -ldflags \"-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION\"\n"
  },
  {
    "path": "cmd/notify-upgrade.go",
    "content": "// Package cmd contains the watchtower (sub-)commands\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/flags\"\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\t\"github.com/containrrr/watchtower/pkg/notifications\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar notifyUpgradeCommand = NewNotifyUpgradeCommand()\n\n// NewNotifyUpgradeCommand creates the notify upgrade command for watchtower\nfunc NewNotifyUpgradeCommand() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"notify-upgrade\",\n\t\tShort: \"Upgrade legacy notification configuration to shoutrrr URLs\",\n\t\tRun:   runNotifyUpgrade,\n\t}\n}\n\nfunc runNotifyUpgrade(cmd *cobra.Command, args []string) {\n\tif err := runNotifyUpgradeE(cmd, args); err != nil {\n\t\tlogf(\"Notification upgrade failed: %v\", err)\n\t}\n}\n\nfunc runNotifyUpgradeE(cmd *cobra.Command, _ []string) error {\n\tf := cmd.Flags()\n\tflags.ProcessFlagAliases(f)\n\n\tnotifier = notifications.NewNotifier(cmd)\n\turls := notifier.GetURLs()\n\n\tlogf(\"Found notification configurations for: %v\", strings.Join(notifier.GetNames(), \", \"))\n\n\toutFile, err := os.CreateTemp(\"/\", \"watchtower-notif-urls-*\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create output file: %v\", err)\n\t}\n\tlogf(\"Writing notification URLs to %v\", outFile.Name())\n\tlogf(\"\")\n\n\tsb := strings.Builder{}\n\tsb.WriteString(\"WATCHTOWER_NOTIFICATION_URL=\")\n\n\tfor i, u := range urls {\n\t\tif i != 0 {\n\t\t\tsb.WriteRune(' ')\n\t\t}\n\t\tsb.WriteString(u)\n\t}\n\n\t_, err = fmt.Fprint(outFile, sb.String())\n\ttryOrLog(err, \"Failed to write to output file\")\n\n\ttryOrLog(outFile.Sync(), \"Failed to sync output file\")\n\ttryOrLog(outFile.Close(), \"Failed to close output file\")\n\n\tcontainerID := \"<CONTAINER>\"\n\tcid, err := container.GetRunningContainerID()\n\ttryOrLog(err, \"Failed to get running container ID\")\n\tif cid != \"\" {\n\t\tcontainerID = cid.ShortID()\n\t}\n\tlogf(\"To get the environment file, use:\")\n\tlogf(\"cp %v:%v ./watchtower-notifications.env\", containerID, outFile.Name())\n\tlogf(\"\")\n\tlogf(\"Note: This file will be removed in 5 minutes or when this container is stopped!\")\n\n\tsignalChannel := make(chan os.Signal, 1)\n\ttime.AfterFunc(5*time.Minute, func() {\n\t\tsignalChannel <- syscall.SIGALRM\n\t})\n\n\tsignal.Notify(signalChannel, os.Interrupt)\n\tsignal.Notify(signalChannel, syscall.SIGTERM)\n\n\tswitch <-signalChannel {\n\tcase syscall.SIGALRM:\n\t\tlogf(\"Timed out!\")\n\tcase os.Interrupt, syscall.SIGTERM:\n\t\tlogf(\"Stopping...\")\n\tdefault:\n\t}\n\n\tif err := os.Remove(outFile.Name()); err != nil {\n\t\tlogf(\"Failed to remove file, it may still be present in the container image! Error: %v\", err)\n\t} else {\n\t\tlogf(\"Environment file has been removed.\")\n\t}\n\n\treturn nil\n}\n\nfunc tryOrLog(err error, message string) {\n\tif err != nil {\n\t\tlogf(\"%v: %v\\n\", message, err)\n\t}\n}\n\nfunc logf(format string, v ...interface{}) {\n\tfmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...))\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/actions\"\n\t\"github.com/containrrr/watchtower/internal/flags\"\n\t\"github.com/containrrr/watchtower/internal/meta\"\n\t\"github.com/containrrr/watchtower/pkg/api\"\n\tapiMetrics \"github.com/containrrr/watchtower/pkg/api/metrics\"\n\t\"github.com/containrrr/watchtower/pkg/api/update\"\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\t\"github.com/containrrr/watchtower/pkg/filters\"\n\t\"github.com/containrrr/watchtower/pkg/metrics\"\n\t\"github.com/containrrr/watchtower/pkg/notifications\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\t\"github.com/robfig/cron\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tclient            container.Client\n\tscheduleSpec      string\n\tcleanup           bool\n\tnoRestart         bool\n\tnoPull            bool\n\tmonitorOnly       bool\n\tenableLabel       bool\n\tdisableContainers []string\n\tnotifier          t.Notifier\n\ttimeout           time.Duration\n\tlifecycleHooks    bool\n\trollingRestart    bool\n\tscope             string\n\tlabelPrecedence   bool\n)\n\nvar rootCmd = NewRootCommand()\n\n// NewRootCommand creates the root command for watchtower\nfunc NewRootCommand() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"watchtower\",\n\t\tShort: \"Automatically updates running Docker containers\",\n\t\tLong: `\n\tWatchtower automatically updates running Docker containers whenever a new image is released.\n\tMore information available at https://github.com/containrrr/watchtower/.\n\t`,\n\t\tRun:    Run,\n\t\tPreRun: PreRun,\n\t\tArgs:   cobra.ArbitraryArgs,\n\t}\n}\n\nfunc init() {\n\tflags.SetDefaults()\n\tflags.RegisterDockerFlags(rootCmd)\n\tflags.RegisterSystemFlags(rootCmd)\n\tflags.RegisterNotificationFlags(rootCmd)\n}\n\n// Execute the root func and exit in case of errors\nfunc Execute() {\n\trootCmd.AddCommand(notifyUpgradeCommand)\n\tif err := rootCmd.Execute(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// PreRun is a lifecycle hook that runs before the command is executed.\nfunc PreRun(cmd *cobra.Command, _ []string) {\n\tf := cmd.PersistentFlags()\n\tflags.ProcessFlagAliases(f)\n\tif err := flags.SetupLogging(f); err != nil {\n\t\tlog.Fatalf(\"Failed to initialize logging: %s\", err.Error())\n\t}\n\n\tscheduleSpec, _ = f.GetString(\"schedule\")\n\n\tflags.GetSecretsFromFiles(cmd)\n\tcleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)\n\n\tif timeout < 0 {\n\t\tlog.Fatal(\"Please specify a positive value for timeout value.\")\n\t}\n\n\tenableLabel, _ = f.GetBool(\"label-enable\")\n\tdisableContainers, _ = f.GetStringSlice(\"disable-containers\")\n\tlifecycleHooks, _ = f.GetBool(\"enable-lifecycle-hooks\")\n\trollingRestart, _ = f.GetBool(\"rolling-restart\")\n\tscope, _ = f.GetString(\"scope\")\n\tlabelPrecedence, _ = f.GetBool(\"label-take-precedence\")\n\n\tif scope != \"\" {\n\t\tlog.Debugf(`Using scope %q`, scope)\n\t}\n\n\t// configure environment vars for client\n\terr := flags.EnvConfig(cmd)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tnoPull, _ = f.GetBool(\"no-pull\")\n\tincludeStopped, _ := f.GetBool(\"include-stopped\")\n\tincludeRestarting, _ := f.GetBool(\"include-restarting\")\n\treviveStopped, _ := f.GetBool(\"revive-stopped\")\n\tremoveVolumes, _ := f.GetBool(\"remove-volumes\")\n\twarnOnHeadPullFailed, _ := f.GetString(\"warn-on-head-failure\")\n\n\tif monitorOnly && noPull {\n\t\tlog.Warn(\"Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.\")\n\t}\n\n\tclient = container.NewClient(container.ClientOptions{\n\t\tIncludeStopped:    includeStopped,\n\t\tReviveStopped:     reviveStopped,\n\t\tRemoveVolumes:     removeVolumes,\n\t\tIncludeRestarting: includeRestarting,\n\t\tWarnOnHeadFailed:  container.WarningStrategy(warnOnHeadPullFailed),\n\t})\n\n\tnotifier = notifications.NewNotifier(cmd)\n\tnotifier.AddLogHook()\n}\n\n// Run is the main execution flow of the command\nfunc Run(c *cobra.Command, names []string) {\n\tfilter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)\n\trunOnce, _ := c.PersistentFlags().GetBool(\"run-once\")\n\tenableUpdateAPI, _ := c.PersistentFlags().GetBool(\"http-api-update\")\n\tenableMetricsAPI, _ := c.PersistentFlags().GetBool(\"http-api-metrics\")\n\tunblockHTTPAPI, _ := c.PersistentFlags().GetBool(\"http-api-periodic-polls\")\n\tapiToken, _ := c.PersistentFlags().GetString(\"http-api-token\")\n\thealthCheck, _ := c.PersistentFlags().GetBool(\"health-check\")\n\n\tif healthCheck {\n\t\t// health check should not have pid 1\n\t\tif os.Getpid() == 1 {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tlog.Fatal(\"The health check flag should never be passed to the main watchtower container process\")\n\t\t}\n\t\tos.Exit(0)\n\t}\n\n\tif rollingRestart && monitorOnly {\n\t\tlog.Fatal(\"Rolling restarts is not compatible with the global monitor only flag\")\n\t}\n\n\tawaitDockerClient()\n\n\tif err := actions.CheckForSanity(client, filter, rollingRestart); err != nil {\n\t\tlogNotifyExit(err)\n\t}\n\n\tif runOnce {\n\t\twriteStartupMessage(c, time.Time{}, filterDesc)\n\t\trunUpdatesWithNotifications(filter)\n\t\tnotifier.Close()\n\t\tos.Exit(0)\n\t\treturn\n\t}\n\n\tif err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil {\n\t\tlogNotifyExit(err)\n\t}\n\n\t// The lock is shared between the scheduler and the HTTP API. It only allows one update to run at a time.\n\tupdateLock := make(chan bool, 1)\n\tupdateLock <- true\n\n\thttpAPI := api.New(apiToken)\n\n\tif enableUpdateAPI {\n\t\tupdateHandler := update.New(func(images []string) {\n\t\t\tmetric := runUpdatesWithNotifications(filters.FilterByImage(images, filter))\n\t\t\tmetrics.RegisterScan(metric)\n\t\t}, updateLock)\n\t\thttpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)\n\t\t// If polling isn't enabled the scheduler is never started, and\n\t\t// we need to trigger the startup messages manually.\n\t\tif !unblockHTTPAPI {\n\t\t\twriteStartupMessage(c, time.Time{}, filterDesc)\n\t\t}\n\t}\n\n\tif enableMetricsAPI {\n\t\tmetricsHandler := apiMetrics.New()\n\t\thttpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)\n\t}\n\n\tif err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\tlog.Error(\"failed to start API\", err)\n\t}\n\n\tif err := runUpgradesOnSchedule(c, filter, filterDesc, updateLock); err != nil {\n\t\tlog.Error(err)\n\t}\n\n\tos.Exit(1)\n}\n\nfunc logNotifyExit(err error) {\n\tlog.Error(err)\n\tnotifier.Close()\n\tos.Exit(1)\n}\n\nfunc awaitDockerClient() {\n\tlog.Debug(\"Sleeping for a second to ensure the docker api client has been properly initialized.\")\n\ttime.Sleep(1 * time.Second)\n}\n\nfunc formatDuration(d time.Duration) string {\n\tsb := strings.Builder{}\n\n\thours := int64(d.Hours())\n\tminutes := int64(math.Mod(d.Minutes(), 60))\n\tseconds := int64(math.Mod(d.Seconds(), 60))\n\n\tif hours == 1 {\n\t\tsb.WriteString(\"1 hour\")\n\t} else if hours != 0 {\n\t\tsb.WriteString(strconv.FormatInt(hours, 10))\n\t\tsb.WriteString(\" hours\")\n\t}\n\n\tif hours != 0 && (seconds != 0 || minutes != 0) {\n\t\tsb.WriteString(\", \")\n\t}\n\n\tif minutes == 1 {\n\t\tsb.WriteString(\"1 minute\")\n\t} else if minutes != 0 {\n\t\tsb.WriteString(strconv.FormatInt(minutes, 10))\n\t\tsb.WriteString(\" minutes\")\n\t}\n\n\tif minutes != 0 && (seconds != 0) {\n\t\tsb.WriteString(\", \")\n\t}\n\n\tif seconds == 1 {\n\t\tsb.WriteString(\"1 second\")\n\t} else if seconds != 0 || (hours == 0 && minutes == 0) {\n\t\tsb.WriteString(strconv.FormatInt(seconds, 10))\n\t\tsb.WriteString(\" seconds\")\n\t}\n\n\treturn sb.String()\n}\n\nfunc writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {\n\tnoStartupMessage, _ := c.PersistentFlags().GetBool(\"no-startup-message\")\n\tenableUpdateAPI, _ := c.PersistentFlags().GetBool(\"http-api-update\")\n\n\tvar startupLog *log.Entry\n\tif noStartupMessage {\n\t\tstartupLog = notifications.LocalLog\n\t} else {\n\t\tstartupLog = log.NewEntry(log.StandardLogger())\n\t\t// Batch up startup messages to send them as a single notification\n\t\tnotifier.StartNotification()\n\t}\n\n\tstartupLog.Info(\"Watchtower \", meta.Version)\n\n\tnotifierNames := notifier.GetNames()\n\tif len(notifierNames) > 0 {\n\t\tstartupLog.Info(\"Using notifications: \" + strings.Join(notifierNames, \", \"))\n\t} else {\n\t\tstartupLog.Info(\"Using no notifications\")\n\t}\n\n\tstartupLog.Info(filtering)\n\n\tif !sched.IsZero() {\n\t\tuntil := formatDuration(time.Until(sched))\n\t\tstartupLog.Info(\"Scheduling first run: \" + sched.Format(\"2006-01-02 15:04:05 -0700 MST\"))\n\t\tstartupLog.Info(\"Note that the first check will be performed in \" + until)\n\t} else if runOnce, _ := c.PersistentFlags().GetBool(\"run-once\"); runOnce {\n\t\tstartupLog.Info(\"Running a one time update.\")\n\t} else {\n\t\tstartupLog.Info(\"Periodic runs are not enabled.\")\n\t}\n\n\tif enableUpdateAPI {\n\t\t// TODO: make listen port configurable\n\t\tstartupLog.Info(\"The HTTP API is enabled at :8080.\")\n\t}\n\n\tif !noStartupMessage {\n\t\t// Send the queued up startup messages, not including the trace warning below (to make sure it's noticed)\n\t\tnotifier.SendNotification(nil)\n\t}\n\n\tif log.IsLevelEnabled(log.TraceLevel) {\n\t\tstartupLog.Warn(\"Trace level enabled: log will include sensitive information as credentials and tokens\")\n\t}\n}\n\nfunc runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, lock chan bool) error {\n\tif lock == nil {\n\t\tlock = make(chan bool, 1)\n\t\tlock <- true\n\t}\n\n\tscheduler := cron.New()\n\terr := scheduler.AddFunc(\n\t\tscheduleSpec,\n\t\tfunc() {\n\t\t\tselect {\n\t\t\tcase v := <-lock:\n\t\t\t\tdefer func() { lock <- v }()\n\t\t\t\tmetric := runUpdatesWithNotifications(filter)\n\t\t\t\tmetrics.RegisterScan(metric)\n\t\t\tdefault:\n\t\t\t\t// Update was skipped\n\t\t\t\tmetrics.RegisterScan(nil)\n\t\t\t\tlog.Debug(\"Skipped another update already running.\")\n\t\t\t}\n\n\t\t\tnextRuns := scheduler.Entries()\n\t\t\tif len(nextRuns) > 0 {\n\t\t\t\tlog.Debug(\"Scheduled next run: \" + nextRuns[0].Next.String())\n\t\t\t}\n\t\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twriteStartupMessage(c, scheduler.Entries()[0].Schedule.Next(time.Now()), filtering)\n\n\tscheduler.Start()\n\n\t// Graceful shut-down on SIGINT/SIGTERM\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\tsignal.Notify(interrupt, syscall.SIGTERM)\n\n\t<-interrupt\n\tscheduler.Stop()\n\tlog.Info(\"Waiting for running update to be finished...\")\n\t<-lock\n\treturn nil\n}\n\nfunc runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {\n\tnotifier.StartNotification()\n\tupdateParams := t.UpdateParams{\n\t\tFilter:          filter,\n\t\tCleanup:         cleanup,\n\t\tNoRestart:       noRestart,\n\t\tTimeout:         timeout,\n\t\tMonitorOnly:     monitorOnly,\n\t\tLifecycleHooks:  lifecycleHooks,\n\t\tRollingRestart:  rollingRestart,\n\t\tLabelPrecedence: labelPrecedence,\n\t\tNoPull:          noPull,\n\t}\n\tresult, err := actions.Update(client, updateParams)\n\tif err != nil {\n\t\tlog.Error(err)\n\t}\n\tnotifier.SendNotification(result)\n\tmetricResults := metrics.NewMetric(result)\n\tnotifications.LocalLog.WithFields(log.Fields{\n\t\t\"Scanned\": metricResults.Scanned,\n\t\t\"Updated\": metricResults.Updated,\n\t\t\"Failed\":  metricResults.Failed,\n\t}).Info(\"Session done\")\n\treturn metricResults\n}\n"
  },
  {
    "path": "code_of_conduct.md",
    "content": "### Containrrr Community Code of Conduct\n\nPlease refer to out [Containrrr Community Code of Conduct](https://github.com/containrrr/.github/blob/master/CODE_OF_CONDUCT.md)\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.7'\n\nservices:\n  watchtower:\n    container_name: watchtower\n    build:\n      context: ./\n      dockerfile: dockerfiles/Dockerfile.dev-self-contained\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    ports:\n      - 8080:8080\n    command: --interval 10 --http-api-metrics --http-api-token demotoken --debug prometheus grafana parent child\n  prometheus:\n    container_name: prometheus\n    image: prom/prometheus\n    volumes:\n      - ./prometheus/:/etc/prometheus/\n      - prometheus:/prometheus/\n    ports:\n      - 9090:9090\n  grafana:\n    container_name: grafana\n    image: grafana/grafana\n    ports:\n      - 3000:3000\n    environment:\n      GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-simple-json-datasource\n    volumes:\n      - grafana:/var/lib/grafana\n      - ./grafana:/etc/grafana/provisioning\n  parent:\n    image: nginx\n    container_name: parent\n  child:\n    image: nginx:alpine\n    labels:\n      com.centurylinklabs.watchtower.depends-on: parent\n    container_name: child\n\nvolumes:\n  prometheus: {}\n  grafana: {}\n"
  },
  {
    "path": "dockerfiles/Dockerfile",
    "content": "FROM --platform=$BUILDPLATFORM alpine:3.19.0 as alpine\n\nRUN apk add --no-cache \\\n    ca-certificates \\\n    tzdata\n\nFROM scratch\nLABEL \"com.centurylinklabs.watchtower\"=\"true\"\n\nCOPY --from=alpine \\\n    /etc/ssl/certs/ca-certificates.crt \\\n    /etc/ssl/certs/ca-certificates.crt\nCOPY --from=alpine \\\n    /usr/share/zoneinfo \\\n    /usr/share/zoneinfo\n\nEXPOSE 8080\n\nCOPY watchtower /\n\nHEALTHCHECK CMD [ \"/watchtower\", \"--health-check\"]\n\nENTRYPOINT [\"/watchtower\"]\n"
  },
  {
    "path": "dockerfiles/Dockerfile.dev-self-contained",
    "content": "#\n# Builder\n#\n\nFROM golang:alpine as builder\n\n# use version (for example \"v0.3.3\") or \"main\"\nARG WATCHTOWER_VERSION=main\n\n# Pre download required modules to avoid redownloading at each build thanks to docker layer caching.\n# Copying go.mod and go.sum ensure to invalid the layer/build cache if there is a change in module requirement\nWORKDIR /watchtower\nCOPY go.mod .\nCOPY go.sum .\nRUN go mod download\n\nRUN apk add --no-cache \\\n    alpine-sdk \\\n    ca-certificates \\\n    git \\\n    tzdata\n\nCOPY . /watchtower\n\nRUN \\\n  cd /watchtower && \\\n  \\\n  GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags \"-extldflags '-static' -X github.com/containrrr/watchtower/internal/meta.Version=$(git describe --tags)\" . && \\\n  GO111MODULE=on go test ./... -v\n\n\n#\n# watchtower\n#\n\nFROM scratch\n\nLABEL \"com.centurylinklabs.watchtower\"=\"true\"\n\n# copy files from other container\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\nCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\nCOPY --from=builder /watchtower/watchtower /watchtower\n\nHEALTHCHECK CMD [ \"/watchtower\", \"--health-check\"]\n\nENTRYPOINT [\"/watchtower\"]\n"
  },
  {
    "path": "dockerfiles/Dockerfile.self-contained",
    "content": "#\n# Builder\n#\n\nFROM golang:alpine as builder\n\n# use version (for example \"v0.3.3\") or \"main\"\nARG WATCHTOWER_VERSION=main\n\nRUN apk add --no-cache \\\n    alpine-sdk \\\n    ca-certificates \\\n    git \\\n    tzdata\n\nRUN git clone --branch \"${WATCHTOWER_VERSION}\" https://github.com/containrrr/watchtower.git\n\nRUN \\\n  cd watchtower && \\\n  \\\n  GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags \"-extldflags '-static' -X github.com/containrrr/watchtower/internal/meta.Version=$(git describe --tags)\" . && \\\n  GO111MODULE=on go test ./... -v\n\n\n#\n# watchtower\n#\n\nFROM scratch\n\nLABEL \"com.centurylinklabs.watchtower\"=\"true\"\n\n# copy files from other container\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\nCOPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\nCOPY --from=builder /go/watchtower/watchtower /watchtower\n\nHEALTHCHECK CMD [ \"/watchtower\", \"--health-check\"]\n\nENTRYPOINT [\"/watchtower\"]\n"
  },
  {
    "path": "dockerfiles/container-networking/docker-compose.yml",
    "content": "services:\n  producer:\n    image: qmcgaw/gluetun:v3.35.0\n    cap_add:\n      - NET_ADMIN\n    environment:\n      - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}\n      - OPENVPN_USER=${OPENVPN_USER}\n      - OPENVPN_PASSWORD=${OPENVPN_PASSWORD}\n      - SERVER_COUNTRIES=${SERVER_COUNTRIES}\n  consumer:\n    depends_on:\n      - producer\n    image: nginx:1.25.1\n    network_mode: \"service:producer\"\n    labels:\n      - \"com.centurylinklabs.watchtower.depends-on=/wt-contnet-producer-1\"\n"
  },
  {
    "path": "docs/arguments.md",
    "content": "By default, watchtower will monitor all containers running within the Docker daemon to which it is pointed (in most cases this\nwill be the local Docker daemon, but you can override it with the `--host` option described in the next section). However, you\ncan restrict watchtower to monitoring a subset of the running containers by specifying the container names as arguments when\nlaunching watchtower.\n\n```bash\n$ docker run -d \\\n    --name watchtower \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    containrrr/watchtower \\\n    nginx redis\n```\n\nIn the example above, watchtower will only monitor the containers named \"nginx\" and \"redis\" for updates -- all of the other\nrunning containers will be ignored. If you do not want watchtower to run as a daemon you can pass the `--run-once` flag and remove\nthe watchtower container after its execution.\n\n```bash\n$ docker run --rm \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    containrrr/watchtower \\\n    --run-once \\\n    nginx redis\n```\n\nIn the example above, watchtower will execute an upgrade attempt on the containers named \"nginx\" and \"redis\". Using this mode will enable debugging output showing all actions performed, as usage is intended for interactive users. Once the attempt is completed, the container will exit and remove itself due to the `--rm` flag.\n\nWhen no arguments are specified, watchtower will monitor all running containers.\n\n## Secrets/Files\n\nSome arguments can also reference a file, in which case the contents of the file are used as the value.\nThis can be used to avoid putting secrets in the configuration file or command line.\n\nThe following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables):\n - `notification-url`\n - `notification-email-server-password`\n - `notification-slack-hook-url`\n - `notification-msteams-hook`\n - `notification-gotify-token`\n - `http-api-token`\n\n### Example docker-compose usage\n```yaml\nsecrets:\n  access_token:\n    file: access_token\n\nservices:\n  watchtower:\n    secrets:\n      - access_token\n    environment:\n      - WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token\n```\n\n## Help\nShows documentation about the supported flags.\n\n```text\n            Argument: --help\nEnvironment Variable: N/A\n                Type: N/A\n             Default: N/A\n```\n\n## Time Zone\nSets the time zone to be used by WatchTower's logs and the optional Cron scheduling argument (--schedule). If this environment variable is not set, Watchtower will use the default time zone: UTC.\nTo find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/localtime file. `-v /etc/localtime:/etc/localtime:ro`\n\n```text\n            Argument: N/A\nEnvironment Variable: TZ\n                Type: String\n             Default: \"UTC\"\n```\n\n## Cleanup\nRemoves old images after updating. When this flag is specified, watchtower will remove the old image after restarting a container with a new image. Use this option to prevent the accumulation of orphaned images on your system as containers are updated.\n\n```text\n            Argument: --cleanup\nEnvironment Variable: WATCHTOWER_CLEANUP\n                Type: Boolean\n             Default: false\n```\n\n## Remove anonymous volumes\nRemoves anonymous volumes after updating. When this flag is specified, watchtower will remove all anonymous volumes from the container before restarting with a new image. Named volumes will not be removed!\n\n```text\n            Argument: --remove-volumes\nEnvironment Variable: WATCHTOWER_REMOVE_VOLUMES\n                Type: Boolean\n             Default: false\n```\n\n## Debug\nEnable debug mode with verbose logging.\n\n!!! note \"Notes\"  \n    Alias for `--log-level debug`. See [Maximum log level](#maximum-log-level).  \n    Does _not_ take an argument when used as an argument. Using `--debug true` will **not** work.\n\n```text\n            Argument: --debug, -d\nEnvironment Variable: WATCHTOWER_DEBUG\n                Type: Boolean\n             Default: false\n```\n\n## Trace\nEnable trace mode with very verbose logging. Caution: exposes credentials!\n\n!!! note \"Notes\"  \n    Alias for `--log-level trace`. See [Maximum log level](#maximum-log-level).  \n    Does _not_ take an argument when used as an argument. Using `--trace true` will **not** work.\n\n```text\n            Argument: --trace\nEnvironment Variable: WATCHTOWER_TRACE\n                Type: Boolean\n             Default: false\n```\n\n## Maximum log level\n\nThe maximum log level that will be written to STDERR (shown in `docker log` when used in a container).\n\n```text\n            Argument: --log-level\nEnvironment Variable: WATCHTOWER_LOG_LEVEL\n     Possible values: panic, fatal, error, warn, info, debug or trace\n             Default: info\n```\n\n## Logging format\n\nSets what logging format to use for console output.\n\n```text\n            Argument: --log-format, -l\nEnvironment Variable: WATCHTOWER_LOG_FORMAT\n     Possible values: Auto, LogFmt, Pretty or JSON\n             Default: Auto\n```\n\n## ANSI colors\nDisable ANSI color escape codes in log output.\n\n```text\n            Argument: --no-color\nEnvironment Variable: NO_COLOR\n                Type: Boolean\n             Default: false\n```\n\n## Docker host\nDocker daemon socket to connect to. Can be pointed at a remote Docker host by specifying a TCP endpoint as \"tcp://hostname:port\".\n\n```text\n            Argument: --host, -H\nEnvironment Variable: DOCKER_HOST\n                Type: String\n             Default: \"unix:///var/run/docker.sock\"\n```\n\n## Docker API version\nThe API version to use by the Docker client for connecting to the Docker daemon. The minimum supported version is 1.24.\n\n```text\n            Argument: --api-version, -a\nEnvironment Variable: DOCKER_API_VERSION\n                Type: String\n             Default: \"1.24\"\n```\n\n## Include restarting\nWill also include restarting containers.\n\n```text\n            Argument: --include-restarting\nEnvironment Variable: WATCHTOWER_INCLUDE_RESTARTING\n                Type: Boolean\n             Default: false\n```\n\n## Include stopped\nWill also include created and exited containers.\n\n```text\n            Argument: --include-stopped, -S\nEnvironment Variable: WATCHTOWER_INCLUDE_STOPPED\n                Type: Boolean\n             Default: false\n```\n\n## Revive stopped\nStart any stopped containers that have had their image updated. This argument is only usable with the `--include-stopped` argument.\n\n```text\n            Argument: --revive-stopped\nEnvironment Variable: WATCHTOWER_REVIVE_STOPPED\n                Type: Boolean\n             Default: false\n```\n\n## Poll interval\nPoll interval (in seconds). This value controls how frequently watchtower will poll for new images. Either `--schedule` or a poll interval can be defined, but not both.\n\n```text\n            Argument: --interval, -i\nEnvironment Variable: WATCHTOWER_POLL_INTERVAL\n                Type: Integer\n             Default: 86400 (24 hours)\n```\n\n## Filter by enable label\nMonitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.\n\n```text\n            Argument: --label-enable\nEnvironment Variable: WATCHTOWER_LABEL_ENABLE\n                Type: Boolean\n             Default: false\n```\n\n## Filter by disable label\n__Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and \nno `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be \nused at the same time to target containers.\n\n## Filter by disabling specific container names\nMonitor and update containers whose names are not in a given set of names.\n\nThis can be used to exclude specific containers, when setting labels is not an option.\nThe listed containers will be excluded even if they have the enable filter set to true.\n\n```text\n            Argument: --disable-containers, -x\nEnvironment Variable: WATCHTOWER_DISABLE_CONTAINERS\n                Type: Comma- or space-separated string list\n             Default: \"\"\n```\n\n## Without updating containers\nWill only monitor for new images, send notifications and invoke\nthe [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the\ncontainers.\n\n!!! note\n    Due to Docker API limitations the latest image will still be pulled from the registry.\n    The HEAD digest checks allows watchtower to skip pulling when there are no changes, but to know _what_ has changed it\n    will still do a pull whenever the repository digest doesn't match the local image digest.\n\n```text\n            Argument: --monitor-only\nEnvironment Variable: WATCHTOWER_MONITOR_ONLY\n                Type: Boolean\n             Default: false\n```\n\nNote that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers.\n\nSee [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set\n\n## With label taking precedence over arguments\n\nBy default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image\n\n```text\n            Argument: --label-take-precedence\nEnvironment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE\n                Type: Boolean\n             Default: false\n```\n\n## Without restarting containers\nDo not restart containers after updating. This option can be useful when the start of the containers\nis managed by an external system such as systemd.\n```text\n            Argument: --no-restart\nEnvironment Variable: WATCHTOWER_NO_RESTART\n                Type: Boolean\n             Default: false\n```\n\n## Without pulling new images\nDo not pull new images. When this flag is specified, watchtower will not attempt to pull\nnew images from the registry. Instead it will only monitor the local image cache for changes.\nUse this option if you are building new images directly on the Docker host without pushing\nthem to a registry.\n\n```text\n            Argument: --no-pull\nEnvironment Variable: WATCHTOWER_NO_PULL\n                Type: Boolean\n             Default: false\n```\n\nNote that no-pull can also be specified on a per-container basis with the\n`com.centurylinklabs.watchtower.no-pull` label set on those containers.\n\nSee [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set\n\n## Without sending a startup message\nDo not send a message after watchtower started. Otherwise there will be an info-level notification.\n\n```text\n            Argument: --no-startup-message\nEnvironment Variable: WATCHTOWER_NO_STARTUP_MESSAGE\n                Type: Boolean\n             Default: false\n```\n\n## Run once\nRun an update attempt against a container name list one time immediately and exit.\n\n```text\n            Argument: --run-once, -R\nEnvironment Variable: WATCHTOWER_RUN_ONCE\n                Type: Boolean\n             Default: false\n```\n\n## HTTP API Mode\nRuns Watchtower in HTTP API mode, only allowing image updates to be triggered by an HTTP request. \nFor details see [HTTP API](https://containrrr.dev/watchtower/http-api-mode).\n\n```text\n            Argument: --http-api-update\nEnvironment Variable: WATCHTOWER_HTTP_API_UPDATE\n                Type: Boolean\n             Default: false\n```\n\n## HTTP API Token\nSets an authentication token to HTTP API requests.\nCan also reference a file, in which case the contents of the file are used.\n\n```text\n            Argument: --http-api-token\nEnvironment Variable: WATCHTOWER_HTTP_API_TOKEN\n                Type: String\n             Default: -\n```\n\n## HTTP API periodic polls\nKeep running periodic updates if the HTTP API mode is enabled, otherwise the HTTP API would prevent periodic polls.  \n\n```text\n            Argument: --http-api-periodic-polls\nEnvironment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS\n                Type: Boolean\n             Default: false\n```\n\n## Filter by scope\nUpdate containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument. \nThis enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances).\n\n!!! note \"Filter by lack of scope\"\n    If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`.\n    When omitted, watchtower will update all containers regardless of scope.\n\n\n```text\n            Argument: --scope\nEnvironment Variable: WATCHTOWER_SCOPE\n                Type: String\n             Default: -\n``` \n\n## HTTP API Metrics\nEnables a metrics endpoint, exposing prometheus metrics via HTTP. See [Metrics](metrics.md) for details.  \n\n```text\n            Argument: --http-api-metrics\nEnvironment Variable: WATCHTOWER_HTTP_API_METRICS\n                Type: Boolean\n             Default: false\n```\n\n## Scheduling\n[Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression\ncan be defined, but not both. An example: `--schedule \"0 0 4 * * *\"`\n\n```text\n            Argument: --schedule, -s\nEnvironment Variable: WATCHTOWER_SCHEDULE\n                Type: String\n             Default: -\n```\n\n## Rolling restart\nRestart one image at time instead of stopping and starting all at once.  Useful in conjunction with lifecycle hooks\nto implement zero-downtime deploy.\n\n```text\n            Argument: --rolling-restart\nEnvironment Variable: WATCHTOWER_ROLLING_RESTART\n                Type: Boolean\n             Default: false\n```\n\n## Wait until timeout\nTimeout before the container is forcefully stopped. When set, this option will change the default (`10s`) wait time to the given value. An example: `--stop-timeout 30s` will set the timeout to 30 seconds.\n\n```text\n            Argument: --stop-timeout\nEnvironment Variable: WATCHTOWER_TIMEOUT\n                Type: Duration\n             Default: 10s\n```\n\n## TLS Verification\n\nUse TLS when connecting to the Docker socket and verify the server's certificate. See below for options used to\nconfigure notifications.\n\n```text\n            Argument: --tlsverify\nEnvironment Variable: DOCKER_TLS_VERIFY\n                Type: Boolean\n             Default: false\n```\n\n## HEAD failure warnings\n\nWhen to warn about HEAD pull requests failing. Auto means that it will warn when the registry is known to handle the\nrequests and may rate limit pull requests (mainly docker.io).\n\n```text\n            Argument: --warn-on-head-failure\nEnvironment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE\n     Possible values: always, auto, never\n             Default: auto\n```\n\n## Health check\n\nReturns a success exit code to enable usage with docker `HEALTHCHECK`. This check is naive and only returns checks whether there is another process running inside the container, as it is the only known form of failure state for watchtowers container.\n\n!!! note \"Only for HEALTHCHECK use\"\n    Never put this on the main container executable command line as it is only meant to be run from docker HEALTHCHECK.\n\n```text\n            Argument: --health-check\n```\n\n## Programatic Output (porcelain)\n\nWrites the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION).  \n  \nAlias for:\n\n```text\n\t\t--notification-url logger://\n\t\t--notification-log-stdout\n\t\t--notification-report\n\t\t--notification-template porcelain.VERSION.summary-no-log\n\n            Argument: --porcelain, -P\nEnvironment Variable: WATCHTOWER_PORCELAIN\n     Possible values: v1\n             Default: -\n```\n"
  },
  {
    "path": "docs/container-selection.md",
    "content": "By default, watchtower will watch all containers. However, sometimes only some containers should be updated.\n\nThere are two options:\n\n-   **Fully exclude**: You can choose to exclude containers entirely from being watched by watchtower.\n-   **Monitor only**: In this mode, watchtower checks for container updates, sends notifications and invokes the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/) on the containers but does **not** perform the update.\n\n## Full Exclude \n\nIf you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`.  For clarity this should be set **on the container(s)** you wish to be ignored, this is not set on watchtower.\n\n=== \"dockerfile\"\n\n    ```docker\n    LABEL com.centurylinklabs.watchtower.enable=\"false\"\n    ```\n=== \"docker run\"\n\n    ```bash\n    docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage\n    ```\n\n=== \"docker-compose\"\n\n    ``` yaml\n    version: \"3\"\n    services:\n      someimage:\n        container_name: someimage\n        labels:\n          - \"com.centurylinklabs.watchtower.enable=false\"\n    ```\n\nIf instead you want to [only include containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCHTOWER_LABEL_ENABLE` environment variable on startup for watchtower and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` on the containers you want to watch.\n\n=== \"dockerfile\"\n\n    ```docker\n    LABEL com.centurylinklabs.watchtower.enable=\"true\"\n    ```\n=== \"docker run\"\n\n    ```bash\n    docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage\n    ```\n\n=== \"docker-compose\"\n\n    ``` yaml\n    version: \"3\"\n    services:\n      someimage:\n        container_name: someimage\n        labels:\n          - \"com.centurylinklabs.watchtower.enable=true\"\n    ```\n\nIf you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances).\n\nWatchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example:\n\n-   If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored;\n-   If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored;\n\n## Monitor Only\n\nIndividual containers can be marked to only be monitored (without being updated).\n\nTo do so, set the *com.centurylinklabs.watchtower.monitor-only* label to `true` on that container.\n\n```docker\nLABEL com.centurylinklabs.watchtower.monitor-only=\"true\"\n```\n\nOr, it can be specified as part of the `docker run` command line:\n\n```bash\ndocker run -d --label=com.centurylinklabs.watchtower.monitor-only=true someimage\n```\n\nWhen the label is specified on a container, watchtower treats that container exactly as if [`WATCHTOWER_MONITOR_ONLY`](https://containrrr.dev/watchtower/arguments/#without_updating_containers) was set, but the effect is limited to the individual container. \n"
  },
  {
    "path": "docs/http-api-mode.md",
    "content": "Watchtower provides an HTTP API mode that enables an HTTP endpoint that can be requested to trigger container updating. The current available endpoint list is:\n\n-   `/v1/update` - triggers an update for all of the containers monitored by this Watchtower instance.\n\n---\n\nTo enable this mode, use the flag `--http-api-update`. For example, in a Docker Compose config file:\n\n```yaml\nversion: '3'\n\nservices:\n  app-monitored-by-watchtower:\n    image: myapps/monitored-by-watchtower\n    labels:\n      - \"com.centurylinklabs.watchtower.enable=true\"\n\n  watchtower:\n    image: containrrr/watchtower\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    command: --debug --http-api-update\n    environment:\n      - WATCHTOWER_HTTP_API_TOKEN=mytoken\n    labels:\n      - \"com.centurylinklabs.watchtower.enable=false\"\n    ports:\n      - 8080:8080\n```\n\nBy default, enabling this mode prevents periodic polls (i.e. what is specified using `--interval` or `--schedule`). To run periodic updates regardless, pass `--http-api-periodic-polls`.\n\nNotice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To prevent external services from accidentally triggering image updates, all of the requests have to contain a \"Token\" field, valued as the token defined in WATCHTOWER_HTTP_API_TOKEN, in their headers. In this case, there is a port bind to the host machine, allowing to request localhost:8080 to reach Watchtower. The following `curl` command would trigger an image update:\n\n```bash\ncurl -H \"Authorization: Bearer mytoken\" localhost:8080/v1/update\n```\n\n---\n\nIn order to update only certain images, the image names can be provided as URL query parameters. The following `curl` command would trigger an update for the images `foo/bar` and `foo/baz`:\n\n```bash\ncurl -H \"Authorization: Bearer mytoken\" localhost:8080/v1/update?image=foo/bar,foo/baz\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "<p style=\"text-align: center; margin-left: 1.6rem;\">\n  <img alt=\"Logotype depicting a lighthouse\" src=\"./images/logo-450px.png\" width=\"450\" />\n</p>\n<h1 align=\"center\">\n  Watchtower\n</h1>\n\n<p align=\"center\">\n  A container-based solution for automating Docker container base image updates.\n  <br/><br/>\n  <a href=\"https://circleci.com/gh/containrrr/watchtower\">\n    <img alt=\"Circle CI\" src=\"https://circleci.com/gh/containrrr/watchtower.svg?style=shield\" />\n  </a>\n  <a href=\"https://codecov.io/gh/containrrr/watchtower\">\n    <img alt=\"Codecov\" src=\"https://codecov.io/gh/containrrr/watchtower/branch/main/graph/badge.svg\">\n  </a>\n  <a href=\"https://godoc.org/github.com/containrrr/watchtower\">\n    <img alt=\"GoDoc\" src=\"https://godoc.org/github.com/containrrr/watchtower?status.svg\" />\n  </a>\n  <a href=\"https://goreportcard.com/report/github.com/containrrr/watchtower\">\n    <img alt=\"Go Report Card\" src=\"https://goreportcard.com/badge/github.com/containrrr/watchtower\" />\n  </a>\n  <a href=\"https://github.com/containrrr/watchtower/releases\">\n    <img alt=\"latest version\" src=\"https://img.shields.io/github/tag/containrrr/watchtower.svg\" />\n  </a>\n  <a href=\"https://www.apache.org/licenses/LICENSE-2.0\">\n    <img alt=\"Apache-2.0 License\" src=\"https://img.shields.io/github/license/containrrr/watchtower.svg\" />\n  </a>\n  <a href=\"https://www.codacy.com/gh/containrrr/watchtower/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=containrrr/watchtower&amp;utm_campaign=Badge_Grade\">\n    <img alt=\"Codacy Badge\" src=\"https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8\"/>\n  </a>\n  <a href=\"https://github.com/containrrr/watchtower/#contributors\">\n    <img alt=\"All Contributors\" src=\"https://img.shields.io/github/all-contributors/containrrr/watchtower\" />\n  </a>\n  <a href=\"https://hub.docker.com/r/containrrr/watchtower\">\n    <img alt=\"Pulls from DockerHub\" src=\"https://img.shields.io/docker/pulls/containrrr/watchtower.svg\" />\n  </a>\n</p>\n\n## Quick Start\n\nWith watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker\nHub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container\nand restart it with the same options that were used when it was deployed initially. Run the watchtower container with\nthe following command:\n\n=== \"docker run\"\n\n    ```bash\n    $ docker run -d \\\n    --name watchtower \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    containrrr/watchtower\n    ```\n\n=== \"docker-compose.yml\"\n\n    ```yaml\n    version: \"3\"\n    services:\n      watchtower:\n        image: containrrr/watchtower\n        volumes:\n          - /var/run/docker.sock:/var/run/docker.sock\n    ```\n"
  },
  {
    "path": "docs/introduction.md",
    "content": "Watchtower is an application that will monitor your running Docker containers and watch for changes to the images that those containers were originally started from. If watchtower detects that an image has changed, it will automatically restart the container using the new image.\n\nWith watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially.\n\nFor example, let's say you were running watchtower along with an instance of _centurylink/wetty-cli_ image:\n\n```text\n$ docker ps\nCONTAINER ID   IMAGE                   STATUS          PORTS                    NAMES\n967848166a45   centurylink/wetty-cli   Up 10 minutes   0.0.0.0:8080->3000/tcp   wetty\n6cc4d2a9d1a5   containrrr/watchtower   Up 15 minutes                            watchtower\n```\n\nEvery day watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the \"wetty\" container. If it sees that the image has changed it will stop/remove the \"wetty\" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).\n\n"
  },
  {
    "path": "docs/lifecycle-hooks.md",
    "content": "## Executing commands before and after updating\n\n!!! note \n    These are shell commands executed with `sh`, and therefore require the container to provide the `sh`\n    executable.\n\n> **DO NOTE**: If the container is not running then lifecycle hooks can not run and therefore \n> the update is executed without running any lifecycle hooks.\n\nIt is possible to execute _pre/post\\-check_ and _pre/post\\-update_ commands\n**inside** every container updated by watchtower.\n\n-   The _pre-check_ command is executed for each container prior to every update cycle.\n-   The _pre-update_ command is executed before stopping the container when an update is about to start.\n-   The _post-update_ command is executed after restarting the updated container\n-   The _post-check_ command is executed for each container post every update cycle.\n\nThis feature is disabled by default. To enable it, you need to set the option\n`--enable-lifecycle-hooks` on the command line, or set the environment variable\n`WATCHTOWER_LIFECYCLE_HOOKS` to `true`.\n\n### Specifying update commands\n\nThe commands are specified using docker container labels, the following are currently available:\n\n| Type        | Docker Container Label                                 |\n| ----------- | ------------------------------------------------------ | \n| Pre Check   | `com.centurylinklabs.watchtower.lifecycle.pre-check`   |\n| Pre Update  | `com.centurylinklabs.watchtower.lifecycle.pre-update`  | \n| Post Update | `com.centurylinklabs.watchtower.lifecycle.post-update` |\n| Post Check  | `com.centurylinklabs.watchtower.lifecycle.post-check`  |\n\nThese labels can be declared as instructions in a Dockerfile (with some example .sh files) or be specified as part of\nthe `docker run` command line:\n\n=== \"Dockerfile\"\n    ```docker \n    LABEL com.centurylinklabs.watchtower.lifecycle.pre-check=\"/sync.sh\"\n    LABEL com.centurylinklabs.watchtower.lifecycle.pre-update=\"/dump-data.sh\"\n    LABEL com.centurylinklabs.watchtower.lifecycle.post-update=\"/restore-data.sh\"\n    LABEL com.centurylinklabs.watchtower.lifecycle.post-check=\"/send-heartbeat.sh\"\n    ```\n\n=== \"docker run\"\n    ```bash \n    docker run -d \\\n    --label=com.centurylinklabs.watchtower.lifecycle.pre-check=\"/sync.sh\" \\\n    --label=com.centurylinklabs.watchtower.lifecycle.pre-update=\"/dump-data.sh\" \\\n    --label=com.centurylinklabs.watchtower.lifecycle.post-update=\"/restore-data.sh\" \\\n    someimage --label=com.centurylinklabs.watchtower.lifecycle.post-check=\"/send-heartbeat.sh\" \\\n    ```\n\n### Timeouts\nThe timeout for all lifecycle commands is 60 seconds. After that, a timeout will\noccur, forcing Watchtower to continue the update loop.\n\n#### Pre- or Post-update timeouts\n\nFor the `pre-update` or `post-update` lifecycle command, it is possible to override this timeout to\nallow the script to finish before forcefully killing it. This is done by adding the\nlabel `com.centurylinklabs.watchtower.lifecycle.pre-update-timeout` or post-update-timeout respectively followed by\nthe timeout expressed in minutes.\n\nIf the label value is explicitly set to `0`, the timeout will be disabled.  \n\n### Execution failure\n\nThe failure of a command to execute, identified by an exit code different than\n0 or 75 (EX_TEMPFAIL), will not prevent watchtower from updating the container. Only an error\nlog statement containing the exit code will be reported.\n"
  },
  {
    "path": "docs/linked-containers.md",
    "content": "Watchtower will detect if there are links between any of the running containers and ensures that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.\n\nFor example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.\n\nIf you want to override existing links, or if you are not using links, you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma.\n\nWhen you have a depending container that is using `network_mode: service:container` then watchtower will treat that container as an implicit link.\n"
  },
  {
    "path": "docs/metrics.md",
    "content": "!!! warning \"Experimental feature\"\n    This feature was added in v1.0.4 and is still considered experimental. If you notice any strange behavior, please raise\n    a ticket in the repository issues.\n\nMetrics can be used to track how Watchtower behaves over time.\n\nTo use this feature, you have to set an [API token](arguments.md#http_api_token) and [enable the metrics API](arguments.md#http_api_metrics),\nas well as creating a port mapping for your container for port `8080`.\n\nThe metrics API endpoint is `/v1/metrics`.\n\n## Available Metrics \n\n| Name                            | Type    | Description                                                                 |\n| ------------------------------- | ------- | --------------------------------------------------------------------------- |\n| `watchtower_containers_scanned` | Gauge   | Number of containers scanned for changes by watchtower during the last scan |\n| `watchtower_containers_updated` | Gauge   | Number of containers updated by watchtower during the last scan             |\n| `watchtower_containers_failed`  | Gauge   | Number of containers where update failed during the last scan               |\n| `watchtower_scans_total`        | Counter | Number of scans since the watchtower started                                |\n| `watchtower_scans_skipped`      | Counter | Number of skipped scans since watchtower started                            |\n\n## Example Prometheus `scrape_config`\n\n```yaml\nscrape_configs:\n  - job_name: watchtower\n    scrape_interval: 5s\n    metrics_path: /v1/metrics\n    bearer_token: demotoken\n    static_configs:\n      - targets:\n        - 'watchtower:8080'\n```\n\nReplace `demotoken` with the Bearer token you have set accordingly.\n\n## Demo\n\nThe repository contains a demo with prometheus and grafana, available through `docker-compose.yml`. This demo\nis preconfigured with a dashboard, which will look something like this:\n\n![grafana metrics](assets/grafana-dashboard.png)\n"
  },
  {
    "path": "docs/notifications.md",
    "content": "# Notifications\n\nWatchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging\nsystem, [logrus](http://github.com/sirupsen/logrus). \n\n!!! note \"Using multiple notifications with environment variables\"\n    There is currently a bug in Viper (https://github.com/spf13/viper/issues/380), which prevents comma-separated slices to\n    be used when using the environment variable.  \n    A workaround is available where we instead put quotes around the environment variable value and replace the commas with\n    spaces:\n    ```\n    WATCHTOWER_NOTIFICATIONS=\"slack msteams\"\n    ```\n    If you're a `docker-compose` user, make sure to specify environment variables' values in your `.yml` file without double\n    quotes (`\"`). This prevents unexpected errors when watchtower starts.\n\n## Settings\n\n-   `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`.\n-   `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.\n-   `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds.\n-   Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.\n-   `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.\n-   `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.\n-   `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout.\n\n## [Shoutrrr](https://github.com/containrrr/shoutrrr) notifications\n\nTo send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:\n\n-   `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used.  This option can also reference a file, in which case the contents of the file are used.\n\n\nGo to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/services/overview) to\nlearn more about the different service URLs you can use. You can define multiple services by space separating the\nURLs. (See example below)\n\nYou can customize the message posted by setting a template.\n\n-   `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message.\n\nThe template is a Go [template](https://golang.org/pkg/text/template/) that either format a list\nof [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry) or a `notification.Data` struct.\n\nSimple templates are used unless the `notification-report` flag is specified:\n\n-   `--notification-report` (env. `WATCHTOWER_NOTIFICATION_REPORT`): Use the session report as the notification template data.\n\n## Simple templates\n\nThe default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also\noutputs timestamp and log level.\n\n!!! tip \"Custom date format\"\n    If you want to adjust the date/time format it must show how the\n    [reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your\n    custom format.  \n    i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.\n\n!!! note \"Skipping notifications\"\n    To skip sending notifications that do not contain any information, you can wrap your template with `{{if .}}` and `{{end}}`.\n\n\nExample:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -e WATCHTOWER_NOTIFICATION_URL=\"discord://token@channel slack://watchtower@token-a/token-b/token-c\" \\\n  -e WATCHTOWER_NOTIFICATION_TEMPLATE=\"{{range .}}{{.Time.Format \\\"2006-01-02 15:04:05\\\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}\" \\\n  containrrr/watchtower\n```\n\n## Report templates\n\nThe default template for report notifications are the following:\n```go\n{{- if .Report -}}\n  {{- with .Report -}}\n    {{- if ( or .Updated .Failed ) -}}\n{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed\n      {{- range .Updated}}\n- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}\n      {{- end -}}\n      {{- range .Fresh}}\n- {{.Name}} ({{.ImageName}}): {{.State}}\n\t  {{- end -}}\n\t  {{- range .Skipped}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n\t  {{- end -}}\n\t  {{- range .Failed}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n\t  {{- end -}}\n    {{- end -}}\n  {{- end -}}\n{{- else -}}\n  {{range .Entries -}}{{.Message}}{{\"\\n\"}}{{- end -}}\n{{- end -}}\n```\n\nIt will be used to send a summary of every session if there are any containers that were updated or which failed to update.\n\n!!! note \"Skipping notifications\"\n    Whenever the result of applying the template results in an empty string, no notifications will\n    be sent. This is by default used to limit the notifications to only be sent when there something noteworthy occurred.\n\n    You can replace `{{- if ( or .Updated .Failed ) -}}` with any logic you want to decide when to send the notifications.\n\nExample using a custom report template that always sends a session report after each run:\n\n=== \"docker run\"\n\n    ```bash\n    docker run -d \\\n      --name watchtower \\\n      -v /var/run/docker.sock:/var/run/docker.sock \\\n      -e WATCHTOWER_NOTIFICATION_REPORT=\"true\" \\\n      -e WATCHTOWER_NOTIFICATION_URL=\"discord://token@channel slack://watchtower@token-a/token-b/token-c\" \\\n      -e WATCHTOWER_NOTIFICATION_TEMPLATE=\"\n      {{- if .Report -}}\n        {{- with .Report -}}\n      {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed\n            {{- range .Updated}}\n      - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}\n            {{- end -}}\n            {{- range .Fresh}}\n      - {{.Name}} ({{.ImageName}}): {{.State}}\n          {{- end -}}\n          {{- range .Skipped}}\n      - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n          {{- end -}}\n          {{- range .Failed}}\n      - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n          {{- end -}}\n        {{- end -}}\n      {{- else -}}\n        {{range .Entries -}}{{.Message}}{{\\\"\\n\\\"}}{{- end -}}\n      {{- end -}}\n      \" \\\n      containrrr/watchtower\n    ```\n\n=== \"docker-compose\"\n\n    ``` yaml\n    version: \"3\"\n    services:\n      watchtower:\n        image: containrrr/watchtower\n        volumes:\n          - /var/run/docker.sock:/var/run/docker.sock\n        env:\n          WATCHTOWER_NOTIFICATION_REPORT: \"true\"\n          WATCHTOWER_NOTIFICATION_URL: >\n            discord://token@channel\n            slack://watchtower@token-a/token-b/token-c\n          WATCHTOWER_NOTIFICATION_TEMPLATE: |\n            {{- if .Report -}}\n              {{- with .Report -}}\n            {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed\n                  {{- range .Updated}}\n            - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}\n                  {{- end -}}\n                  {{- range .Fresh}}\n            - {{.Name}} ({{.ImageName}}): {{.State}}\n                {{- end -}}\n                {{- range .Skipped}}\n            - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n                {{- end -}}\n                {{- range .Failed}}\n            - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n                {{- end -}}\n              {{- end -}}\n            {{- else -}}\n              {{range .Entries -}}{{.Message}}{{\"\\n\"}}{{- end -}}\n            {{- end -}}\n    ```\n\n## Legacy notifications\n\nFor backwards compatibility, the notifications can also be configured using legacy notification options. These will automatically be converted to shoutrrr URLs when used.  \nThe types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option\n(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:\n\n-   `email` to send notifications via e-mail\n-   `slack` to send notifications through a Slack webhook\n-   `msteams` to send notifications via MSTeams webhook\n-   `gotify` to send notifications via Gotify\n\n### `notify-upgrade`\nIf watchtower is started with `notify-upgrade` as it's first argument, it will generate a .env file with your current legacy notification options converted to shoutrrr URLs.\n\n=== \"docker run\"\n\n    ```bash\n    $ docker run -d \\\n    --name watchtower \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    -e WATCHTOWER_NOTIFICATIONS=slack \\\n    -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=\"https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy\" \\\n    containrrr/watchtower \\\n    notify-upgrade\n    ```\n\n=== \"docker-compose.yml\"\n\n    ```yaml\n    version: \"3\"\n    services:\n      watchtower:\n        image: containrrr/watchtower\n        volumes:\n          - /var/run/docker.sock:/var/run/docker.sock\n        env:\n          WATCHTOWER_NOTIFICATIONS: slack\n          WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy\n        command: notify-upgrade\n    ```\n\n\nYou can then copy this file from the container (a message with the full command to do so will be logged) and use it with your current setup:\n\n=== \"docker run\"\n\n    ```bash\n    $ docker run -d \\\n    --name watchtower \\\n    -v /var/run/docker.sock:/var/run/docker.sock \\\n    --env-file watchtower-notifications.env \\\n    containrrr/watchtower\n    ```\n\n=== \"docker-compose.yml\"\n\n    ```yaml\n    version: \"3\"\n    services:\n      watchtower:\n        image: containrrr/watchtower\n        volumes:\n          - /var/run/docker.sock:/var/run/docker.sock\n        env_file:\n          - watchtower-notifications.env\n    ```\n\n### Email\n\nTo receive notifications by email, the following command-line options, or their corresponding environment variables, can be set:\n\n-   `--notification-email-from` (env. `WATCHTOWER_NOTIFICATION_EMAIL_FROM`): The e-mail address from which notifications will be sent.\n-   `--notification-email-to` (env. `WATCHTOWER_NOTIFICATION_EMAIL_TO`): The e-mail address to which notifications will be sent.\n-   `--notification-email-server` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER`): The SMTP server to send e-mails through.\n-   `--notification-email-server-tls-skip-verify` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY`): Do not verify the TLS certificate of the mail server. This should be used only for testing.\n-   `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`.\n-   `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.\n-   `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used.\n-   `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.\n-   `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. **NOTE:** This will affect all notification types.\n\nExample:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -e WATCHTOWER_NOTIFICATIONS=email \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587 \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \\\n  -e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \\\n  containrrr/watchtower\n```\n\nThe previous example assumes, that you already have an SMTP server up and running you can connect to. If you don't or you want to bring up watchtower with your own simple SMTP relay the following `docker-compose.yml` might be a good start for you.\n\nThe following example assumes, that your domain is called `your-domain.com` and that you are going to use a certificate valid for `smtp.your-domain.com`. This hostname has to be used as `WATCHTOWER_NOTIFICATION_EMAIL_SERVER` otherwise the TLS connection is going to fail with `Failed to send notification email` or `connect: connection refused`. We also have to add a network for this setup in order to add an alias to it. If you also want to enable DKIM or other features on the SMTP server, you will find more information at [freinet/postfix-relay](https://hub.docker.com/r/freinet/postfix-relay).\n\nExample including an SMTP relay:\n\n```yaml\nversion: '3.8'\nservices:\n  watchtower:\n    image: containrrr/watchtower:latest\n    container_name: watchtower\n    environment:\n      WATCHTOWER_MONITOR_ONLY: 'true'\n      WATCHTOWER_NOTIFICATIONS: email\n      WATCHTOWER_NOTIFICATION_EMAIL_FROM: from-address@your-domain.com\n      WATCHTOWER_NOTIFICATION_EMAIL_TO: to-address@your-domain.com\n      # you have to use a network alias here, if you use your own certificate\n      WATCHTOWER_NOTIFICATION_EMAIL_SERVER: smtp.your-domain.com\n      WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT: 25\n      WATCHTOWER_NOTIFICATION_EMAIL_DELAY: 2\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    networks:\n      - watchtower\n    depends_on:\n      - postfix\n\n  # SMTP needed to send out status emails\n  postfix:\n    image: freinet/postfix-relay:latest\n    expose:\n      - 25\n    environment:\n      MAILNAME: somename.your-domain.com\n      TLS_KEY: '/etc/ssl/domains/your-domain.com/your-domain.com.key'\n      TLS_CRT: '/etc/ssl/domains/your-domain.com/your-domain.com.crt'\n      TLS_CA: '/etc/ssl/domains/your-domain.com/intermediate.crt'\n    volumes:\n      - /etc/ssl/domains/your-domain.com/:/etc/ssl/domains/your-domain.com/:ro\n    networks:\n      watchtower:\n        # this alias is really important to make your certificate work\n        aliases:\n          - smtp.your-domain.com\nnetworks:\n  watchtower:\n    external: false\n```\n\n### Slack\n\nTo receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.\n\nAdditionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used.\n\nBy default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable.\n\nOther, optional, variables include:\n\n-   `--notification-slack-channel` (env. `WATCHTOWER_NOTIFICATION_SLACK_CHANNEL`): A string which overrides the webhook's default channel. Example: #my-custom-channel.\n\nExample:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -e WATCHTOWER_NOTIFICATIONS=slack \\\n  -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=\"https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy\" \\\n  -e WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower-server-1 \\\n  -e WATCHTOWER_NOTIFICATION_SLACK_CHANNEL=#my-custom-channel \\\n  containrrr/watchtower\n```\n\n### Microsoft Teams\n\nTo receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.\n\nAdditionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used.\n\nMSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable.\n\nExample:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -e WATCHTOWER_NOTIFICATIONS=msteams \\\n  -e WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL=\"https://outlook.office.com/webhook/xxxxxxxx@xxxxxxx/IncomingWebhook/yyyyyyyy/zzzzzzzzzz\" \\\n  -e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \\\n  containrrr/watchtower\n```\n\n### Gotify\n\nTo push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  -e WATCHTOWER_NOTIFICATIONS=gotify \\\n  -e WATCHTOWER_NOTIFICATION_GOTIFY_URL=\"https://my.gotify.tld/\" \\\n  -e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN=\"SuperSecretToken\" \\\n  containrrr/watchtower\n```\n\n`-e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN` or `--notification-gotify-token` can also reference a file, in which case the contents of the file are used.\n\nIf you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`.\n\n"
  },
  {
    "path": "docs/private-registries.md",
    "content": "Watchtower supports private Docker image registries. In many cases, accessing a private registry\nrequires a valid username and password (i.e., _credentials_). In order to operate in such an\nenvironment, watchtower needs to know the credentials to access the registry. \n\nThe credentials can be provided to watchtower in a configuration file called `config.json`.\nThere are two ways to generate this configuration file:\n\n*   The configuration file can be created manually.\n*   Call `docker login <REGISTRY_NAME>` and share the resulting configuration file.\n\n### Create the configuration file manually\nCreate a new configuration file with the following syntax and a base64 encoded username and\npassword `auth` string:\n\n```json\n{\n    \"auths\": {\n        \"<REGISTRY_NAME>\": {\n            \"auth\": \"XXXXXXX\"\n        }\n    }\n}\n```\n\n`<REGISTRY_NAME>` needs to be replaced by the name of your private registry\n(e.g., `my-private-registry.example.org`).\n\n!!! info \"Using private images on Docker Hub\"\n    To access private repositories on Docker Hub,\n    `<REGISTRY_NAME>` should be `https://index.docker.io/v1/`.\n    In this special case, the registry domain does not have to be specified\n    in `docker run` or `docker-compose`. Like Docker, Watchtower will use the\n    Docker Hub registry and its credentials when no registry domain is specified.\n    \n    <sub>Watchtower will recognize credentials with `<REGISTRY_NAME>` `index.docker.io`,\n    but the Docker CLI will not.</sub>\n\n!!! important \"Using a private registry on a local host\"\n    To use a private registry hosted locally, make sure to correctly specify the registry host\n    in both `config.json` and the `docker run` command or `docker-compose` file.\n    Valid hosts are `localhost[:PORT]`, `HOST:PORT`,\n    or any multi-part `domain.name` or IP-address with or without a port.\n    \n    Examples:\n    * `localhost` -> `localhost/myimage`\n    * `127.0.0.1` -> `127.0.0.1/myimage:mytag`\n    * `host.domain` -> `host.domain/myorganization/myimage`\n    * `other-lan-host:80` -> `other-lan-host:80/imagename:latest`\n\nThe required `auth` string can be generated as follows:\n\n```bash\necho -n 'username:password' | base64\n```\n\n!!! info \"Username and Password for GCloud\"\n    For gcloud, we'll use `_json_key` as our username and the content of `gcloudauth.json` as the password.\n    ```\n    bash echo -n \"_json_key:$(cat gcloudauth.json)\" | base64 -w0\n    ```\n\nWhen the watchtower Docker container is started, the created configuration file\n(`<PATH>/config.json` in this example) needs to be passed to the container:\n\n```bash\ndocker run [...] -v <PATH>/config.json:/config.json containrrr/watchtower\n```\n\n### Share the Docker configuration file\n\nTo pull an image from a private registry, `docker login` needs to be called first, to get access\nto the registry. The provided credentials are stored in a configuration file called `<PATH_TO_HOME_DIR>/.docker/config.json`.\nThis configuration file can be directly used by watchtower. In this case, the creation of an\nadditional configuration file is not necessary.\n\nWhen the Docker container is started, pass the configuration file to watchtower:\n\n```bash\ndocker run [...] -v <PATH_TO_HOME_DIR>/.docker/config.json:/config.json containrrr/watchtower\n```\n\nWhen creating the watchtower container via docker-compose, use the following lines:\n\n```yaml\nversion: \"3.4\"\nservices:\n  watchtower:\n    image: containrrr/watchtower:latest\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - <PATH_TO_HOME_DIR>/.docker/config.json:/config.json\n  ...\n```\n\n#### Docker Config path\nBy default, watchtower will look for the `config.json` file in `/`, but this can be changed by setting the `DOCKER_CONFIG` environment variable to the directory path where your config is located. This is useful for setups where the config.json file is changed while the watchtower instance is running, as the changes will not be picked up for a mounted file if the inode changes.\nExample usage:\n\n```yaml\nversion: \"3.4\"\n\nservices: \n  watchtower:\n    image: containrrr/watchtower\n    environment:\n        DOCKER_CONFIG: /config\n    volumes:\n      - /etc/watchtower/config/:/config/\n      - /var/run/docker.sock:/var/run/docker.sock\n```\n\n## Credential helpers\nSome private Docker registries (the most prominent probably being AWS ECR) use non-standard ways of authentication.\nTo be able to use this together with watchtower, we need to use a credential helper.\n\nTo keep the image size small we've decided to not include any helpers in the watchtower image, instead we'll put the\nhelper in a separate container and mount it using volumes.\n\n### Example\nExample implementation for use with [amazon-ecr-credential-helper](https://github.com/awslabs/amazon-ecr-credential-helper):\n\nUse the dockerfile below to build the [amazon-ecr-credential-helper](https://github.com/awslabs/amazon-ecr-credential-helper),\nin a volume that may be mounted onto your watchtower container.\n\n1.  Create the Dockerfile (contents below):\n    ```Dockerfile\n    FROM golang:1.20\n    \n    ENV GO111MODULE off\n    ENV CGO_ENABLED 0\n    ENV REPO github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login\n    \n    RUN go get -u $REPO\n    \n    RUN rm /go/bin/docker-credential-ecr-login\n    \n    RUN go build \\\n     -o /go/bin/docker-credential-ecr-login \\\n     /go/src/$REPO\n    \n    WORKDIR /go/bin/\n    ```\n\n2.  Use the following commands to build the aws-ecr-dock-cred-helper and store it's output in a volume:\n    ```bash\n    # Create a volume to store the command (once built)\n    docker volume create helper \n    \n    # Build the container\n    docker build -t aws-ecr-dock-cred-helper .\n    \n    # Build the command and store it in the new volume in the /go/bin directory.\n    docker run  -d --rm --name aws-cred-helper \\\n      --volume helper:/go/bin aws-ecr-dock-cred-helper\n    ```\n\n3.  Create a configuration file for docker, and store it in $HOME/.docker/config.json (replace the <AWS_ACCOUNT_ID>\n   placeholders with your AWS Account ID and <AWS_ECR_REGION> with your AWS ECR Region):\n    ```json\n    {\n       \"credsStore\" : \"ecr-login\",\n       \"HttpHeaders\" : {\n         \"User-Agent\" : \"Docker-Client/19.03.1 (XXXXXX)\"\n       },\n       \"auths\" : {\n         \"<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_ECR_REGION>.amazonaws.com\" : {}\n       },\n       \"credHelpers\": {\n         \"<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_ECR_REGION>.amazonaws.com\" : \"ecr-login\"\n       }\n    }\n    ```\n\n4.  Create a docker-compose file (as an example) to help launch the container:\n    ```yaml\n    version: \"3.4\"\n    services:\n     # Check for new images and restart things if a new image exists\n     # for any of our containers.\n     watchtower:\n       image: containrrr/watchtower:latest\n       volumes:\n         - /var/run/docker.sock:/var/run/docker.sock\n         - .docker/config.json:/config.json\n         - helper:/go/bin\n       environment:\n         - HOME=/\n         - PATH=$PATH:/go/bin\n         - AWS_REGION=us-west-1\n    volumes:\n     helper: \n       external: true\n    ```\n\nA few additional notes:\n\n1.  With docker-compose the volume (helper, in this case) MUST be set to `external: true`, otherwise docker-compose \n    will preface it with the directory name.\n\n2.  Note that \"credsStore\" : \"ecr-login\" is needed - and in theory if you have that you can remove the \n    credHelpers section\n\n3.  I have this running on an EC2 instance that has credentials assigned to it - so no keys are needed; however, \n    you may need to include the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables as well.\n\n4.  An alternative to adding the various variables is to create a ~/.aws/config and ~/.aws/credentials files and \n    place the settings there, then mount the ~/.aws directory to / in the container.\n"
  },
  {
    "path": "docs/remote-hosts.md",
    "content": "By default, watchtower is set-up to monitor the local Docker daemon (the same daemon running the watchtower container itself). However, it is possible to configure watchtower to monitor a remote Docker endpoint. When starting the watchtower container you can specify a remote Docker endpoint with either the `--host` flag or the `DOCKER_HOST` environment variable:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  containrrr/watchtower --host \"tcp://10.0.1.2:2375\"\n```\n\nor\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -e DOCKER_HOST=\"tcp://10.0.1.2:2375\" \\\n  containrrr/watchtower\n```\n\nNote in both of the examples above that it is unnecessary to mount the _/var/run/docker.sock_ into the watchtower container.\n"
  },
  {
    "path": "docs/running-multiple-instances.md",
    "content": "By default, Watchtower will clean up other instances and won't allow multiple instances running on the same Docker host or swarm. It is possible to override this behavior by defining a [scope](https://containrrr.github.io/watchtower/arguments/#filter_by_scope) to each running instance. \n\n!!! note\n    - Multiple instances can't run with the same scope;\n    - An instance without a scope will clean up other running instances, even if they have a defined scope;\n    - Supplying `none` as the scope will treat `com.centurylinklabs.watchtower.scope=none`, `com.centurylinklabs.watchtower.scope=` and the lack of a `com.centurylinklabs.watchtower.scope` label as the scope `none`. This effectly enables you to run both scoped and unscoped watchtower instances on the same machine.\n\nTo define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the `com.centurylinklabs.watchtower.scope` label with the same value for the containers you want to include in this instance's scope (including the instance itself).\n\nFor example, in a Docker Compose config file:\n\n```yaml\nversion: '3'\n\nservices:\n  app-with-scope:\n    image: myapps/monitored-by-watchtower\n    labels: [ \"com.centurylinklabs.watchtower.scope=myscope\" ]\n\n  scoped-watchtower:\n    image: containrrr/watchtower\n    volumes: [ \"/var/run/docker.sock:/var/run/docker.sock\" ]\n    command: --interval 30 --scope myscope\n    labels: [ \"com.centurylinklabs.watchtower.scope=myscope\" ] \n\n  unscoped-app-a:\n    image: myapps/app-a\n\n  unscoped-app-b:\n    image: myapps/app-b\n    labels: [ \"com.centurylinklabs.watchtower.scope=none\" ]\n    \n  unscoped-app-c:\n    image: myapps/app-b\n    labels: [ \"com.centurylinklabs.watchtower.scope=\" ]\n    \n  unscoped-watchtower:\n    image: containrrr/watchtower\n    volumes: [ \"/var/run/docker.sock:/var/run/docker.sock\" ]\n    command: --interval 30 --scope none\n```\n"
  },
  {
    "path": "docs/secure-connections.md",
    "content": "Watchtower is also capable of connecting to Docker endpoints which are protected by SSL/TLS. If you've used _docker-machine_ to provision your remote Docker host, you simply need to volume mount the certificates generated by _docker-machine_ into the watchtower container and optionally specify `--tlsverify` flag.\n\nThe _docker-machine_ certificates for a particular host can be located by executing the `docker-machine env` command for the desired host (note the values for the `DOCKER_HOST` and `DOCKER_CERT_PATH` environment variables that are returned from this command). The directory containing the certificates for the remote host needs to be mounted into the watchtower container at _/etc/ssl/docker_.\n\nWith the certificates mounted into the watchtower container you need to specify the `--tlsverify` flag to enable verification of the certificate:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -e DOCKER_HOST=$DOCKER_HOST \\\n  -e DOCKER_CERT_PATH=/etc/ssl/docker \\\n  -v $DOCKER_CERT_PATH:/etc/ssl/docker \\\n  containrrr/watchtower --tlsverify\n```\n"
  },
  {
    "path": "docs/stop-signals.md",
    "content": "When watchtower detects that a running container needs to be updated it will stop the container by sending it a SIGTERM signal.\nIf your container should be shutdown with a different signal you can communicate this to watchtower by setting a label named _com.centurylinklabs.watchtower.stop-signal_ with the value of the desired signal.\n\nThis label can be coded directly into your image by using the `LABEL` instruction in your Dockerfile:\n\n```docker\nLABEL com.centurylinklabs.watchtower.stop-signal=\"SIGHUP\"\n```\n\nOr, it can be specified as part of the `docker run` command line:\n\n```bash\ndocker run -d --label=com.centurylinklabs.watchtower.stop-signal=SIGHUP someimage\n```\n"
  },
  {
    "path": "docs/stylesheets/theme.css",
    "content": "[data-md-color-scheme=\"containrrr\"] {\n  /* Primary and accent */\n  --md-primary-fg-color: #406170;\n  --md-primary-fg-color--light:#acbfc7;\n  --md-primary-fg-color--dark: #003343;\n  --md-accent-fg-color: #003343;\n  --md-accent-fg-color--transparent: #00334310;\n\n  /* Typeset overrides */\n  --md-typeset-a-color: var(--md-primary-fg-color);\n}\n\n[data-md-color-scheme=\"containrrr-dark\"] {\n  --md-hue: 199;\n\n  /* Primary and accent */\n  --md-primary-fg-color:             hsl(199deg  27%  35% / 100%);\n  --md-primary-fg-color--link:       hsl(199deg  45%  65% / 100%);\n  --md-primary-fg-color--light:      hsl(198deg  19%  73% / 100%);\n  --md-primary-fg-color--dark:       hsl(194deg 100%  13% / 100%);\n  --md-accent-fg-color:              hsl(194deg  45%  50% / 100%);\n  --md-accent-fg-color--transparent: hsl(194deg  45%  50% / 6.3%);\n\n  /* Default */\n  --md-default-fg-color:             hsl(var(--md-hue)  75%  95%  / 100%);\n  --md-default-fg-color--light:      hsl(var(--md-hue)  75%  90%  /  62%);\n  --md-default-fg-color--lighter:    hsl(var(--md-hue)  75%  90%  /  32%);\n  --md-default-fg-color--lightest:   hsl(var(--md-hue)  75%  90%  /  12%);\n  --md-default-bg-color:             hsl(var(--md-hue)  15%  21%  / 100%);\n  --md-default-bg-color--light:      hsl(var(--md-hue)  15%  21%  /  54%);\n  --md-default-bg-color--lighter:    hsl(var(--md-hue)  15%  21%  /  26%);\n  --md-default-bg-color--lightest:   hsl(var(--md-hue)  15%  21%  /   7%);\n\n  /* Code */\n  --md-code-fg-color:                hsl(var(--md-hue) 18% 86%  / 100%);\n  --md-code-bg-color:                hsl(var(--md-hue) 15% 15%  / 100%);\n  --md-code-hl-color:                hsl(218deg 100%  63%  /  15%);\n  --md-code-hl-number-color:         hsl(346deg  74%  63%  / 100%);\n  --md-code-hl-special-color:        hsl(320deg  83%  66%  / 100%);\n  --md-code-hl-function-color:       hsl(271deg  57%  65%  / 100%);\n  --md-code-hl-constant-color:       hsl(230deg  62%  70%  / 100%);\n  --md-code-hl-keyword-color:        hsl(199deg  33%  64%  / 100%);\n  --md-code-hl-string-color:         hsl( 50deg  34%  74%  / 100%);\n  --md-code-hl-name-color:           var(--md-code-fg-color);\n  --md-code-hl-operator-color:       var(--md-default-fg-color--light);\n  --md-code-hl-punctuation-color:    var(--md-default-fg-color--light);\n  --md-code-hl-comment-color:        var(--md-default-fg-color--light);\n  --md-code-hl-generic-color:        var(--md-default-fg-color--light);\n  --md-code-hl-variable-color:       hsl(241deg  22%  60%  / 100%);\n\n  /* Typeset */\n  --md-typeset-color:                var(--md-default-fg-color);\n  --md-typeset-a-color:              var(--md-primary-fg-color--link);\n  --md-typeset-mark-color:           hsl(218deg 100%  63%  / 30%);\n  --md-typeset-kbd-color:            hsl(var(--md-hue)  15%  94%  /  12%);\n  --md-typeset-kbd-accent-color:     hsl(var(--md-hue)  15%  94%  /  20%);\n  --md-typeset-kbd-border-color:     hsl(var(--md-hue)  15%  14%  / 100%);\n  --md-typeset-table-color:          hsl(var(--md-hue)  75%  95%  /  12%);\n\n  /* Admonition */\n  --md-admonition-fg-color:          var(--md-default-fg-color);\n  --md-admonition-bg-color:          var(--md-default-bg-color);\n\n  /* Footer */\n  --md-footer-bg-color:              hsl(var(--md-hue)  15%  12% /  87%);\n  --md-footer-bg-color--dark:        hsl(var(--md-hue)  15%  10% / 100%);\n\n  /* Shadows */\n  --md-shadow-z1:\n    0 0.2rem 0.50rem rgba(0 0 0 20%),\n    0 0      0.05rem rgba(0 0 0 10%);\n  --md-shadow-z2:\n    0 0.2rem 0.50rem rgba(0 0 0 30%),\n    0 0      0.05rem rgba(0 0 0 25%);\n  --md-shadow-z3:\n    0 0.2rem 0.50rem rgba(0 0 0 40%),\n    0 0      0.05rem rgba(0 0 0 35%);\n}\n\n.md-header-nav__button.md-logo {\n  padding: 0;\n}\n\n.md-header-nav__button.md-logo img {\n  width: 1.6rem;\n  height: 1.6rem;\n}\n"
  },
  {
    "path": "docs/template-preview.md",
    "content": "<style>\n    #tplprev {\n        margin: 0;\n        display: flex; \n        flex-direction: column; \n        row-gap: 1rem; \n        box-sizing: border-box; \n        position: relative; \n        margin-right: -13.3rem\n    }\n    #tplprev textarea {\n        box-decoration-break: slice;\n        overflow: auto;\n        padding: 0.77em 1.18em;\n        scrollbar-color: var(--md-default-fg-color--lighter) transparent;\n        scrollbar-width: thin;\n        touch-action: auto;\n        word-break: normal;\n        height: 420px;\n        flex: 1;\n    }\n    #tplprev .controls {\n        display: flex; \n        flex-direction: row; \n        column-gap: 0.5rem\n    }\n    #tplprev textarea, #tplprev input {\n        background-color: var(--md-code-bg-color);\n        border-width: 0;\n        border-radius: 0.1rem;\n        color: var(--md-code-fg-color);\n        font-feature-settings: \"kern\";\n        font-family: var(--md-code-font-family);\n    }\n    .numfield {\n        font-size: .7rem;\n        display: flex;\n        flex-direction: column;\n        justify-content: space-between;\n    }\n    #tplprev button {\n        border-radius: 0.1rem;\n        color: var(--md-primary-bg-color);\n        background-color: var(--md-primary-fg-color);\n        flex:1; \n        min-width: 12ch; \n        padding: 0.5rem\n    }\n    #tplprev button:hover {\n        background-color: var(--md-accent-fg-color);\n    }\n    #tplprev input[type=\"number\"] { width: 5ch; flex: 1; font-size: 1rem; }\n    #tplprev fieldset {\n        margin-top: -0.5rem;\n        display: flex;\n        flex: 1;\n        column-gap: 0.5rem;\n    }\n    #tplprev .template-wrapper {\n        display: flex; \n        flex:1; \n        column-gap: 1rem;\n    }\n    #tplprev .result-wrapper {\n        flex: 1; \n        display: flex\n    }\n    #result {\n        font-size: 0.7rem;\n        background-color: var(--md-code-bg-color);\n        scrollbar-color: var(--md-default-fg-color--lighter) transparent;\n        scrollbar-width: thin;\n        touch-action: auto;\n        overflow: auto;\n        padding: 0.77em 1.18em;\n        margin:0;\n        height: 540px;\n        flex:1; \n        width:100%\n    }\n    #result b {color: var(--md-code-hl-special-color)}\n    #result i {color: var(--md-code-hl-keyword-color)}\n    #tplprev .loading {\n        position: absolute; \n        inset: 0; \n        display: flex; \n        padding: 1rem; \n        box-sizing: border-box; \n        background: var(--md-code-bg-color); \n        margin-top: 0\n    }\n</style>\n<script src=\"../assets/wasm_exec.js\"></script>\n<script>\n    let wasmLoaded = false;\n    const updatePreview = () => {\n        if (!wasmLoaded) return;\n        const form = document.querySelector('#tplprev');\n        const input = form.template.value;\n        console.log('Input: %o', input);\n        const arrFromCount = (key) => Array.from(Array(form[key]?.valueAsNumber ?? 0), () => key);\n        const states = form.report.value === \"yes\" ? [\n            ...arrFromCount(\"skipped\"),\n            ...arrFromCount(\"scanned\"),\n            ...arrFromCount(\"updated\"),\n            ...arrFromCount(\"failed\" ),\n            ...arrFromCount(\"fresh\"  ),\n            ...arrFromCount(\"stale\"  ),\n        ] : [];\n        console.log(\"States: %o\", states);\n        const levels = form.log.value === \"yes\" ? [\n            ...arrFromCount(\"error\"),\n            ...arrFromCount(\"warning\"),\n            ...arrFromCount(\"info\"),\n            ...arrFromCount(\"debug\"),\n        ] : [];\n        console.log(\"Levels: %o\", levels);\n        const output = WATCHTOWER.tplprev(input, states, levels);\n        console.log('Output: \\n%o', output);\n        if (output.startsWith('Error: ')) {\n            document.querySelector('#result').innerHTML = `<b>Error</b>: ${output.substring(7)}`;\n        } else if (output.length) {\n            document.querySelector('#result').innerText = output;\n        } else {\n            document.querySelector('#result').innerHTML = '<i>empty (would not be sent as a notification)</i>';\n        }\n    }\n    const formSubmitted = (e) => {\n        //e.preventDefault();\n        //updatePreview();\n    }\n    let debounce;\n    const inputUpdated = () => {\n        if(debounce) clearTimeout(debounce);\n        debounce = setTimeout(() => updatePreview(), 400);\n    }\n    const formChanged = (e) =>  {\n        console.log('form changed: %o', e);\n        const targetToggle = e.target.dataset['toggle'];\n        if (targetToggle) {\n            e.target.form[targetToggle].value = e.target.checked ? \"yes\" : \"no\";\n        }\n        updatePreview()\n    }\n    const go = new Go();\n    WebAssembly.instantiateStreaming(fetch(\"../assets/tplprev.wasm\"), go.importObject).then((result) => {\n        go.run(result.instance);\n        document.querySelector('#tplprev .loading').style.display = \"none\";\n        wasmLoaded = true;\n        updatePreview();\n    });\n</script>\n<form id=\"tplprev\" onchange=\"formChanged(event)\" onsubmit=\"formSubmitted(event)\">\n<pre class=\"loading\">loading wasm...</pre>\n<div class=\"template-wrapper\">\n<textarea name=\"template\" type=\"text\" onkeyup=\"inputUpdated()\">{{- with .Report -}}\n  {{- if ( or .Updated .Failed ) -}}\n{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed\n    {{- range .Updated}}\n- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}\n    {{- end -}}\n    {{- range .Fresh}}\n- {{.Name}} ({{.ImageName}}): {{.State}}\n    {{- end -}}\n    {{- range .Skipped}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n    {{- end -}}\n    {{- range .Failed}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n      {{- end -}}\n  {{- end -}}\n{{- end -}}\n{{- if (and .Entries .Report) }}\n\nLogs:\n{{ end -}}\n{{range .Entries -}}{{.Time.Format \"2006-01-02T15:04:05Z07:00\"}} [{{.Level}}] {{.Message}}{{\"\\n\"}}{{- end -}}</textarea>\n</div>\n<div class=\"controls\">\n<fieldset>\n    <input type=\"hidden\" name=\"report\" value=\"yes\" />\n    <legend><label><input type=\"checkbox\" data-toggle=\"report\" checked /> Container report</label></legend>\n    <label class=\"numfield\">\n        Skipped:\n        <input type=\"number\" name=\"skipped\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Scanned:\n        <input type=\"number\" name=\"scanned\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Updated:\n        <input type=\"number\" name=\"updated\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Failed:\n        <input type=\"number\" name=\"failed\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Fresh:\n        <input type=\"number\" name=\"fresh\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Stale:\n        <input type=\"number\" name=\"stale\" value=\"3\" />\n    </label>\n</fieldset>\n<fieldset>\n    <input type=\"hidden\" name=\"log\" value=\"yes\" />\n    <legend><label><input type=\"checkbox\" data-toggle=\"log\" checked /> Log entries</label></legend>\n    <label class=\"numfield\">\n        Error: \n        <input type=\"number\" name=\"error\" value=\"1\" />\n    </label>\n    <label class=\"numfield\">\n        Warning:\n        <input type=\"number\" name=\"warning\" value=\"2\" />\n    </label>\n    <label class=\"numfield\">\n        Info:\n        <input type=\"number\" name=\"info\" value=\"3\" />\n    </label>\n    <label class=\"numfield\">\n        Debug:\n        <input type=\"number\" name=\"debug\" value=\"4\" />\n    </label>\n</fieldset>\n<button type=\"submit\">Update preview</button>\n</div>\n<div style=\"result-wrapper\">\n    <pre id=\"result\"></pre>\n</div>\n</form>\n<script>\nconst loadQueryVals = () => {\n    const form = document.querySelector('#tplprev');\n    const params =  new URLSearchParams(location.search);\n    for(const [key, value] of params){\n        form[key].value = value;\n        const toggleInput = form.querySelector(`[data-toggle=\"${key}\"]`);\n        if (toggleInput) {\n            toggleInput.checked = value === \"yes\";\n        }\n    }\n}\nif (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", loadQueryVals());\n} else {\n    loadQueryVals();\n}\n</script>"
  },
  {
    "path": "docs/updating.md",
    "content": "## Updating Watchtower\n\nIf watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you \nvolume-mounted `/var/run/docker.sock` into the watchtower container) then it has the ability to update itself.  \nIf a new version of the `containrrr/watchtower` image is pushed to the Docker Hub, your watchtower will pull down the \nnew image and restart itself automatically.\n"
  },
  {
    "path": "docs/usage-overview.md",
    "content": "Watchtower is itself packaged as a Docker container so installation is as simple as pulling the `containrrr/watchtower` image. If you are using ARM based architecture, pull the appropriate `containrrr/watchtower:armhf-<tag>` image from the [containrrr Docker Hub](https://hub.docker.com/r/containrrr/watchtower/tags/).\n\nSince the watchtower code needs to interact with the Docker API in order to monitor the running containers, you need to mount _/var/run/docker.sock_ into the container with the `-v` flag when you run it.\n\nRun the `watchtower` container with the following command:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  containrrr/watchtower\n```\n\nIf pulling images from private Docker registries, supply registry authentication credentials with the environment variables `REPO_USER` and `REPO_PASS`\nor by mounting the host's docker config file into the container (at the root of the container filesystem `/`).\n\nPassing environment variables:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -e REPO_USER=username \\\n  -e REPO_PASS=password \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  containrrr/watchtower container_to_watch --debug\n```\n\nAlso check out [this Stack Overflow answer](https://stackoverflow.com/a/30494145/7872793) for more options on how to pass environment variables.\n\nAlternatively if you 2FA authentication setup on Docker Hub then passing username and password will be insufficient.  Instead you can run `docker login` to store your credentials in `$HOME/.docker/config.json` and then mount this config file to make it available to the Watchtower container:\n\n```bash\ndocker run -d \\\n  --name watchtower \\\n  -v $HOME/.docker/config.json:/config.json \\\n  -v /var/run/docker.sock:/var/run/docker.sock \\\n  containrrr/watchtower container_to_watch --debug\n```\n\n!!! note \"Changes to config.json while running\"\n    If you mount `config.json` in the manner above, changes from the host system will (generally) not be propagated to the\n    running container. Mounting files into the Docker daemon uses bind mounts, which are based on inodes. Most\n    applications (including `docker login` and `vim`) will not directly edit the file, but instead make a copy and replace\n    the original file, which results in a new inode which in turn _breaks_ the bind mount.  \n    **As a workaround**, you can create a symlink to your `config.json` file and then mount the symlink in the container. \n    The symlinked file will always have the same inode, which keeps the bind mount intact and will ensure changes\n    to the original file are propagated to the running container (regardless of the inode of the source file!).\n\nIf you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your\nwatched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container\nfrom a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval\nto 30s rather than the default 24 hours.\n\n```yaml\nversion: \"3\"\nservices:\n  cavo:\n    image: ghcr.io/<org>/<image>:<tag>\n    ports:\n      - \"443:3443\"\n      - \"80:3080\"\n  watchtower:\n    image: containrrr/watchtower\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n      - /root/.docker/config.json:/config.json\n    command: --interval 30\n```\n"
  },
  {
    "path": "docs-requirements.txt",
    "content": "mkdocs\nmkdocs-material\nmd-toc\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/containrrr/watchtower\n\ngo 1.20\n\nrequire (\n\tgithub.com/containrrr/shoutrrr v0.8.0\n\tgithub.com/distribution/reference v0.5.0\n\tgithub.com/docker/cli v24.0.7+incompatible\n\tgithub.com/docker/docker v24.0.7+incompatible\n\tgithub.com/docker/go-connections v0.4.0\n\tgithub.com/onsi/ginkgo v1.16.5\n\tgithub.com/onsi/gomega v1.30.0\n\tgithub.com/prometheus/client_golang v1.18.0\n\tgithub.com/robfig/cron v1.2.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/cobra v1.8.0\n\tgithub.com/spf13/pflag v1.0.5\n\tgithub.com/spf13/viper v1.18.2\n\tgithub.com/stretchr/testify v1.8.4\n\tgolang.org/x/net v0.19.0\n)\n\nrequire github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect\n\nrequire (\n\tgithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect\n\tgithub.com/Microsoft/go-winio v0.4.17 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.6.1 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/fatih/color v1.15.0 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/google/go-cmp v0.6.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.17 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect\n\tgithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect\n\tgithub.com/nxadm/tail v1.4.8 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.1.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/client_model v0.5.0 // indirect\n\tgithub.com/prometheus/common v0.45.0 // indirect\n\tgithub.com/prometheus/procfs v0.12.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.4.0 // indirect\n\tgithub.com/sagikazarmark/slog-shim v0.1.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.0 // indirect\n\tgithub.com/spf13/afero v1.11.0 // indirect\n\tgithub.com/spf13/cast v1.6.0 // indirect\n\tgithub.com/stretchr/objx v0.5.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgo.uber.org/multierr v1.9.0 // indirect\n\tgolang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect\n\tgolang.org/x/sys v0.15.0 // indirect\n\tgolang.org/x/text v0.14.0\n\tgolang.org/x/time v0.5.0 // indirect\n\tgoogle.golang.org/protobuf v1.31.0 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgotest.tools/v3 v3.0.3 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w=\ngithub.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=\ngithub.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=\ngithub.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=\ngithub.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=\ngithub.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g=\ngithub.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=\ngithub.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=\ngithub.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=\ngithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=\ngithub.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=\ngithub.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=\ngithub.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=\ngithub.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=\ngithub.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=\ngithub.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=\ngithub.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=\ngithub.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=\ngithub.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=\ngithub.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=\ngithub.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=\ngithub.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=\ngithub.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=\ngithub.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=\ngithub.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=\ngithub.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=\ngo.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=\ngolang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=\ngolang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=\ngolang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\n"
  },
  {
    "path": "goreleaser.yml",
    "content": "build:\n  main: ./main.go\n  binary: watchtower\n  goos:\n    - linux\n    - windows\n  goarch:\n    - amd64\n    - 386\n    - arm\n    - arm64\n  ldflags:\n    - -s -w -X github.com/containrrr/watchtower/internal/meta.Version={{.Version}}\narchives:\n  - \n    name_template: \"{{.ProjectName}}_{{.Os}}_{{.Arch}}\"\n    format: tar.gz\n    replacements:\n      arm: armhf\n      arm64: arm64v8\n      amd64: amd64\n      386: 386\n      darwin: macOS\n      linux: linux\n    format_overrides:\n      - goos: windows\n        format: zip\n    files:\n      - LICENSE.md\ndockers:\n  -\n    use_buildx: true\n    build_flag_templates: [ \"--platform=linux/amd64\" ]\n    goos: linux\n    goarch: amd64\n    goarm: ''\n    dockerfile: dockerfiles/Dockerfile\n    image_templates:\n      - containrrr/watchtower:amd64-{{ .Version }}\n      - containrrr/watchtower:amd64-latest\n      - ghcr.io/containrrr/watchtower:amd64-{{ .Version }}\n      - ghcr.io/containrrr/watchtower:amd64-latest\n    binaries:\n      - watchtower\n  - \n    use_buildx: true\n    build_flag_templates: [ \"--platform=linux/386\" ]\n    goos: linux\n    goarch: 386\n    goarm: ''\n    dockerfile: dockerfiles/Dockerfile\n    image_templates:\n      - containrrr/watchtower:i386-{{ .Version }}\n      - containrrr/watchtower:i386-latest\n      - ghcr.io/containrrr/watchtower:i386-{{ .Version }}\n      - ghcr.io/containrrr/watchtower:i386-latest\n    binaries:\n      - watchtower\n  - \n    use_buildx: true\n    build_flag_templates: [ \"--platform=linux/arm/v6\" ]\n    goos: linux\n    goarch: arm\n    goarm: 6\n    dockerfile: dockerfiles/Dockerfile\n    image_templates:\n      - containrrr/watchtower:armhf-{{ .Version }}\n      - containrrr/watchtower:armhf-latest\n      - ghcr.io/containrrr/watchtower:armhf-{{ .Version }}\n      - ghcr.io/containrrr/watchtower:armhf-latest\n    binaries:\n      - watchtower\n  - \n    use_buildx: true\n    build_flag_templates: [ \"--platform=linux/arm64/v8\" ]\n    goos: linux\n    goarch: arm64\n    goarm: ''\n    dockerfile: dockerfiles/Dockerfile\n    image_templates:\n      - containrrr/watchtower:arm64v8-{{ .Version }}\n      - containrrr/watchtower:arm64v8-latest\n      - ghcr.io/containrrr/watchtower:arm64v8-{{ .Version }}\n      - ghcr.io/containrrr/watchtower:arm64v8-latest\n    binaries:\n      - watchtower\n"
  },
  {
    "path": "grafana/dashboards/dashboard.json",
    "content": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\": true,\n        \"hide\": true,\n        \"iconColor\": \"rgba(0, 211, 255, 1)\",\n        \"name\": \"Annotations & Alerts\",\n        \"type\": \"dashboard\"\n      }\n    ]\n  },\n  \"editable\": true,\n  \"gnetId\": null,\n  \"graphTooltip\": 0,\n  \"id\": 1,\n  \"links\": [],\n  \"panels\": [\n    {\n      \"datasource\": \"Prometheus\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 4,\n        \"w\": 1,\n        \"x\": 0,\n        \"y\": 0\n      },\n      \"id\": 2,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"7.3.6\",\n      \"targets\": [\n        {\n          \"expr\": \"watchtower_scans_total\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Total Scans\",\n      \"type\": \"stat\"\n    },\n    {\n      \"aliasColors\": {},\n      \"bars\": false,\n      \"dashLength\": 10,\n      \"dashes\": false,\n      \"datasource\": null,\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {}\n        },\n        \"overrides\": [\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"watchtower_containers_scanned{instance=\\\"watchtower:8080\\\", job=\\\"watchtower\\\"}\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"displayName\",\n                \"value\": \"Scanned\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"watchtower_containers_failed{instance=\\\"watchtower:8080\\\", job=\\\"watchtower\\\"}\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"displayName\",\n                \"value\": \"Failed\"\n              }\n            ]\n          },\n          {\n            \"matcher\": {\n              \"id\": \"byName\",\n              \"options\": \"watchtower_containers_updated{instance=\\\"watchtower:8080\\\", job=\\\"watchtower\\\"}\"\n            },\n            \"properties\": [\n              {\n                \"id\": \"displayName\",\n                \"value\": \"Updated\"\n              }\n            ]\n          }\n        ]\n      },\n      \"fill\": 1,\n      \"fillGradient\": 0,\n      \"gridPos\": {\n        \"h\": 8,\n        \"w\": 6,\n        \"x\": 1,\n        \"y\": 0\n      },\n      \"hiddenSeries\": false,\n      \"id\": 5,\n      \"legend\": {\n        \"avg\": false,\n        \"current\": false,\n        \"max\": false,\n        \"min\": false,\n        \"show\": true,\n        \"total\": false,\n        \"values\": false\n      },\n      \"lines\": true,\n      \"linewidth\": 1,\n      \"nullPointMode\": \"null as zero\",\n      \"options\": {\n        \"alertThreshold\": true\n      },\n      \"percentage\": false,\n      \"pluginVersion\": \"7.3.6\",\n      \"pointradius\": 2,\n      \"points\": false,\n      \"renderer\": \"flot\",\n      \"seriesOverrides\": [],\n      \"spaceLength\": 10,\n      \"stack\": false,\n      \"steppedLine\": false,\n      \"targets\": [\n        {\n          \"expr\": \"watchtower_containers_scanned\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        },\n        {\n          \"expr\": \"watchtower_containers_failed\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"B\"\n        },\n        {\n          \"expr\": \"watchtower_containers_updated\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"C\"\n        }\n      ],\n      \"thresholds\": [],\n      \"timeFrom\": null,\n      \"timeRegions\": [],\n      \"timeShift\": null,\n      \"title\": \"Container Updates\",\n      \"tooltip\": {\n        \"shared\": true,\n        \"sort\": 0,\n        \"value_type\": \"individual\"\n      },\n      \"type\": \"graph\",\n      \"xaxis\": {\n        \"buckets\": null,\n        \"mode\": \"time\",\n        \"name\": null,\n        \"show\": true,\n        \"values\": []\n      },\n      \"yaxes\": [\n        {\n          \"decimals\": 0,\n          \"format\": \"short\",\n          \"label\": \"\",\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": \"0\",\n          \"show\": true\n        },\n        {\n          \"format\": \"short\",\n          \"label\": null,\n          \"logBase\": 1,\n          \"max\": null,\n          \"min\": null,\n          \"show\": true\n        }\n      ],\n      \"yaxis\": {\n        \"align\": false,\n        \"alignLevel\": null\n      }\n    },\n    {\n      \"datasource\": \"Prometheus\",\n      \"fieldConfig\": {\n        \"defaults\": {\n          \"custom\": {},\n          \"mappings\": [],\n          \"thresholds\": {\n            \"mode\": \"absolute\",\n            \"steps\": [\n              {\n                \"color\": \"green\",\n                \"value\": null\n              },\n              {\n                \"color\": \"red\",\n                \"value\": 80\n              }\n            ]\n          }\n        },\n        \"overrides\": []\n      },\n      \"gridPos\": {\n        \"h\": 4,\n        \"w\": 1,\n        \"x\": 0,\n        \"y\": 4\n      },\n      \"id\": 3,\n      \"options\": {\n        \"colorMode\": \"value\",\n        \"graphMode\": \"none\",\n        \"justifyMode\": \"auto\",\n        \"orientation\": \"auto\",\n        \"reduceOptions\": {\n          \"calcs\": [\n            \"lastNotNull\"\n          ],\n          \"fields\": \"\",\n          \"values\": false\n        },\n        \"textMode\": \"auto\"\n      },\n      \"pluginVersion\": \"7.3.6\",\n      \"targets\": [\n        {\n          \"expr\": \"watchtower_scans_skipped\",\n          \"interval\": \"\",\n          \"legendFormat\": \"\",\n          \"refId\": \"A\"\n        }\n      ],\n      \"timeFrom\": null,\n      \"timeShift\": null,\n      \"title\": \"Skipped Scans\",\n      \"type\": \"stat\"\n    }\n  ],\n  \"refresh\": false,\n  \"schemaVersion\": 26,\n  \"style\": \"dark\",\n  \"tags\": [],\n  \"templating\": {\n    \"list\": []\n  },\n  \"time\": {\n    \"from\": \"now-1h\",\n    \"to\": \"now\"\n  },\n  \"timepicker\": {},\n  \"timezone\": \"\",\n  \"title\": \"Watchtower\",\n  \"uid\": \"d7bdoT-Gz\",\n  \"version\": 1\n}\n"
  },
  {
    "path": "grafana/dashboards/dashboard.yml",
    "content": "apiVersion: 1\n\nproviders:\n  - name: 'Prometheus'\n    orgId: 1\n    folder: ''\n    type: file\n    disableDeletion: false\n    editable: true\n    options:\n      path: /etc/grafana/provisioning/dashboards"
  },
  {
    "path": "grafana/datasources/datasource.yml",
    "content": "apiVersion: 1\n\ndatasources:\n  - name: Prometheus\n    type: prometheus\n    access: proxy\n    url: http://prometheus:9090\n    isDefault: true"
  },
  {
    "path": "internal/actions/actions_suite_test.go",
    "content": "package actions_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/containrrr/watchtower/internal/actions\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\n\t. \"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestActions(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tlogrus.SetOutput(GinkgoWriter)\n\tRunSpecs(t, \"Actions Suite\")\n}\n\nvar _ = Describe(\"the actions package\", func() {\n\tDescribe(\"the check prerequisites method\", func() {\n\t\tWhen(\"given an empty array\", func() {\n\t\t\tIt(\"should not do anything\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{},\n\t\t\t\t\t// pullImages:\n\t\t\t\t\tfalse,\n\t\t\t\t\t// removeVolumes:\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\tExpect(actions.CheckForMultipleWatchtowerInstances(client, false, \"\")).To(Succeed())\n\t\t\t})\n\t\t})\n\t\tWhen(\"given an array of one\", func() {\n\t\t\tIt(\"should not do anything\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container\",\n\t\t\t\t\t\t\t\t\"test-container\",\n\t\t\t\t\t\t\t\t\"watchtower\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// pullImages:\n\t\t\t\t\tfalse,\n\t\t\t\t\t// removeVolumes:\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\tExpect(actions.CheckForMultipleWatchtowerInstances(client, false, \"\")).To(Succeed())\n\t\t\t})\n\t\t})\n\t\tWhen(\"given multiple containers\", func() {\n\t\t\tvar client MockClient\n\t\t\tBeforeEach(func() {\n\t\t\t\tclient = CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\tNameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"watchtower\",\n\t\t\t\t\t\t\t\ttime.Now().AddDate(0, 0, -1)),\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"watchtower\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// pullImages:\n\t\t\t\t\tfalse,\n\t\t\t\t\t// removeVolumes:\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t})\n\n\t\t\tIt(\"should stop all but the latest one\", func() {\n\t\t\t\terr := actions.CheckForMultipleWatchtowerInstances(client, false, \"\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t})\n\t\t})\n\t\tWhen(\"deciding whether to cleanup images\", func() {\n\t\t\tvar client MockClient\n\t\t\tBeforeEach(func() {\n\t\t\t\tclient = CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"watchtower\",\n\t\t\t\t\t\t\t\ttime.Now().AddDate(0, 0, -1)),\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"watchtower\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t// pullImages:\n\t\t\t\t\tfalse,\n\t\t\t\t\t// removeVolumes:\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t})\n\t\t\tIt(\"should try to delete the image if the cleanup flag is true\", func() {\n\t\t\t\terr := actions.CheckForMultipleWatchtowerInstances(client, true, \"\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImage()).To(BeTrue())\n\t\t\t})\n\t\t\tIt(\"should not try to delete the image if the cleanup flag is false\", func() {\n\t\t\t\terr := actions.CheckForMultipleWatchtowerInstances(client, false, \"\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImage()).To(BeFalse())\n\t\t\t})\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "internal/actions/check.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\t\"github.com/containrrr/watchtower/pkg/filters\"\n\t\"github.com/containrrr/watchtower/pkg/sorter\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// CheckForSanity makes sure everything is sane before starting\nfunc CheckForSanity(client container.Client, filter types.Filter, rollingRestarts bool) error {\n\tlog.Debug(\"Making sure everything is sane before starting\")\n\n\tif rollingRestarts {\n\t\tcontainers, err := client.ListContainers(filter)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, c := range containers {\n\t\t\tif len(c.Links()) > 0 {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"%q is depending on at least one other container. This is not compatible with rolling restarts\",\n\t\t\t\t\tc.Name(),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the\n// watchtower running simultaneously. If multiple watchtower containers are detected, this function\n// will stop and remove all but the most recently started container. This behaviour can be bypassed\n// if a scope UID is defined.\nfunc CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error {\n\tfilter := filters.WatchtowerContainersFilter\n\tif scope != \"\" {\n\t\tfilter = filters.FilterByScope(scope, filter)\n\t}\n\tcontainers, err := client.ListContainers(filter)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(containers) <= 1 {\n\t\tlog.Debug(\"There are no additional watchtower containers\")\n\t\treturn nil\n\t}\n\n\tlog.Info(\"Found multiple running watchtower instances. Cleaning up.\")\n\treturn cleanupExcessWatchtowers(containers, client, cleanup)\n}\n\nfunc cleanupExcessWatchtowers(containers []types.Container, client container.Client, cleanup bool) error {\n\tvar stopErrors int\n\n\tsort.Sort(sorter.ByCreated(containers))\n\tallContainersExceptLast := containers[0 : len(containers)-1]\n\n\tfor _, c := range allContainersExceptLast {\n\t\tif err := client.StopContainer(c, 10*time.Minute); err != nil {\n\t\t\t// logging the original here as we're just returning a count\n\t\t\tlog.WithError(err).Error(\"Could not stop a previous watchtower instance.\")\n\t\t\tstopErrors++\n\t\t\tcontinue\n\t\t}\n\n\t\tif cleanup {\n\t\t\tif err := client.RemoveImageByID(c.ImageID()); err != nil {\n\t\t\t\tlog.WithError(err).Warning(\"Could not cleanup watchtower images, possibly because of other watchtowers instances in other scopes.\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif stopErrors > 0 {\n\t\treturn fmt.Errorf(\"%d errors while stopping watchtower containers\", stopErrors)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/actions/mocks/client.go",
    "content": "package mocks\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// MockClient is a mock that passes as a watchtower Client\ntype MockClient struct {\n\tTestData      *TestData\n\tpullImages    bool\n\tremoveVolumes bool\n}\n\n// TestData is the data used to perform the test\ntype TestData struct {\n\tTriedToRemoveImageCount int\n\tNameOfContainerToKeep   string\n\tContainers              []t.Container\n\tStaleness               map[string]bool\n}\n\n// TriedToRemoveImage is a test helper function to check whether RemoveImageByID has been called\nfunc (testdata *TestData) TriedToRemoveImage() bool {\n\treturn testdata.TriedToRemoveImageCount > 0\n}\n\n// CreateMockClient creates a mock watchtower Client for usage in tests\nfunc CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockClient {\n\treturn MockClient{\n\t\tdata,\n\t\tpullImages,\n\t\tremoveVolumes,\n\t}\n}\n\n// ListContainers is a mock method returning the provided container testdata\nfunc (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) {\n\treturn client.TestData.Containers, nil\n}\n\n// StopContainer is a mock method\nfunc (client MockClient) StopContainer(c t.Container, _ time.Duration) error {\n\tif c.Name() == client.TestData.NameOfContainerToKeep {\n\t\treturn errors.New(\"tried to stop the instance we want to keep\")\n\t}\n\treturn nil\n}\n\n// StartContainer is a mock method\nfunc (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) {\n\treturn \"\", nil\n}\n\n// RenameContainer is a mock method\nfunc (client MockClient) RenameContainer(_ t.Container, _ string) error {\n\treturn nil\n}\n\n// RemoveImageByID increments the TriedToRemoveImageCount on being called\nfunc (client MockClient) RemoveImageByID(_ t.ImageID) error {\n\tclient.TestData.TriedToRemoveImageCount++\n\treturn nil\n}\n\n// GetContainer is a mock method\nfunc (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) {\n\treturn client.TestData.Containers[0], nil\n}\n\n// ExecuteCommand is a mock method\nfunc (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int) (SkipUpdate bool, err error) {\n\tswitch command {\n\tcase \"/PreUpdateReturn0.sh\":\n\t\treturn false, nil\n\tcase \"/PreUpdateReturn1.sh\":\n\t\treturn false, fmt.Errorf(\"command exited with code 1\")\n\tcase \"/PreUpdateReturn75.sh\":\n\t\treturn true, nil\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n// IsContainerStale is true if not explicitly stated in TestData for the mock client\nfunc (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {\n\tstale, found := client.TestData.Staleness[cont.Name()]\n\tif !found {\n\t\tstale = true\n\t}\n\treturn stale, \"\", nil\n}\n\n// WarnOnHeadPullFailed is always true for the mock client\nfunc (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {\n\treturn true\n}\n"
  },
  {
    "path": "internal/actions/mocks/container.go",
    "content": "package mocks\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\twt \"github.com/containrrr/watchtower/pkg/types\"\n\t\"github.com/docker/docker/api/types\"\n\tdockerContainer \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/go-connections/nat\"\n)\n\n// CreateMockContainer creates a container substitute valid for testing\nfunc CreateMockContainer(id string, name string, image string, created time.Time) wt.Container {\n\tcontent := types.ContainerJSON{\n\t\tContainerJSONBase: &types.ContainerJSONBase{\n\t\t\tID:      id,\n\t\t\tImage:   image,\n\t\t\tName:    name,\n\t\t\tCreated: created.String(),\n\t\t\tHostConfig: &dockerContainer.HostConfig{\n\t\t\t\tPortBindings: map[nat.Port][]nat.PortBinding{},\n\t\t\t},\n\t\t},\n\t\tConfig: &dockerContainer.Config{\n\t\t\tImage:        image,\n\t\t\tLabels:       make(map[string]string),\n\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t},\n\t}\n\treturn container.NewContainer(\n\t\t&content,\n\t\tCreateMockImageInfo(image),\n\t)\n}\n\n// CreateMockImageInfo returns a mock image info struct based on the passed image\nfunc CreateMockImageInfo(image string) *types.ImageInspect {\n\treturn &types.ImageInspect{\n\t\tID: image,\n\t\tRepoDigests: []string{\n\t\t\timage,\n\t\t},\n\t}\n}\n\n// CreateMockContainerWithImageInfo should only be used for testing\nfunc CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container {\n\treturn CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)\n}\n\n// CreateMockContainerWithImageInfoP should only be used for testing\nfunc CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container {\n\tcontent := types.ContainerJSON{\n\t\tContainerJSONBase: &types.ContainerJSONBase{\n\t\t\tID:      id,\n\t\t\tImage:   image,\n\t\t\tName:    name,\n\t\t\tCreated: created.String(),\n\t\t},\n\t\tConfig: &dockerContainer.Config{\n\t\t\tImage:  image,\n\t\t\tLabels: make(map[string]string),\n\t\t},\n\t}\n\treturn container.NewContainer(\n\t\t&content,\n\t\timageInfo,\n\t)\n}\n\n// CreateMockContainerWithDigest should only be used for testing\nfunc CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container {\n\tc := CreateMockContainer(id, name, image, created)\n\tc.ImageInfo().RepoDigests = []string{digest}\n\treturn c\n}\n\n// CreateMockContainerWithConfig creates a container substitute valid for testing\nfunc CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container {\n\tcontent := types.ContainerJSON{\n\t\tContainerJSONBase: &types.ContainerJSONBase{\n\t\t\tID:    id,\n\t\t\tImage: image,\n\t\t\tName:  name,\n\t\t\tState: &types.ContainerState{\n\t\t\t\tRunning:    running,\n\t\t\t\tRestarting: restarting,\n\t\t\t},\n\t\t\tCreated: created.String(),\n\t\t\tHostConfig: &dockerContainer.HostConfig{\n\t\t\t\tPortBindings: map[nat.Port][]nat.PortBinding{},\n\t\t\t},\n\t\t},\n\t\tConfig: config,\n\t}\n\treturn container.NewContainer(\n\t\t&content,\n\t\tCreateMockImageInfo(image),\n\t)\n}\n\n// CreateContainerForProgress creates a container substitute for tracking session/update progress\nfunc CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) {\n\tindexStr := strconv.Itoa(idPrefix + index)\n\tmockID := indexStr + strings.Repeat(\"0\", 61-len(indexStr))\n\tcontID := \"c79\" + mockID\n\tcontName := fmt.Sprintf(nameFormat, index+1)\n\toldImgID := \"01d\" + mockID\n\tnewImgID := \"d0a\" + mockID\n\timageName := fmt.Sprintf(\"mock/%s:latest\", contName)\n\tconfig := &dockerContainer.Config{\n\t\tImage: imageName,\n\t}\n\tc := CreateMockContainerWithConfig(contID, contName, oldImgID, true, false, time.Now(), config)\n\treturn c, wt.ImageID(newImgID)\n}\n\n// CreateMockContainerWithLinks should only be used for testing\nfunc CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container {\n\tcontent := types.ContainerJSON{\n\t\tContainerJSONBase: &types.ContainerJSONBase{\n\t\t\tID:      id,\n\t\t\tImage:   image,\n\t\t\tName:    name,\n\t\t\tCreated: created.String(),\n\t\t\tHostConfig: &dockerContainer.HostConfig{\n\t\t\t\tLinks: links,\n\t\t\t},\n\t\t},\n\t\tConfig: &dockerContainer.Config{\n\t\t\tImage:  image,\n\t\t\tLabels: make(map[string]string),\n\t\t},\n\t}\n\treturn container.NewContainer(\n\t\t&content,\n\t\timageInfo,\n\t)\n}\n"
  },
  {
    "path": "internal/actions/mocks/progress.go",
    "content": "package mocks\n\nimport (\n\t\"errors\"\n\n\t\"github.com/containrrr/watchtower/pkg/session\"\n\twt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// CreateMockProgressReport creates a mock report from a given set of container states\n// All containers will be given a unique ID and name based on its state and index\nfunc CreateMockProgressReport(states ...session.State) wt.Report {\n\n\tstateNums := make(map[session.State]int)\n\tprogress := session.Progress{}\n\tfailed := make(map[wt.ContainerID]error)\n\n\tfor _, state := range states {\n\t\tindex := stateNums[state]\n\n\t\tswitch state {\n\t\tcase session.SkippedState:\n\t\t\tc, _ := CreateContainerForProgress(index, 41, \"skip%d\")\n\t\t\tprogress.AddSkipped(c, errors.New(\"unpossible\"))\n\t\tcase session.FreshState:\n\t\t\tc, _ := CreateContainerForProgress(index, 31, \"frsh%d\")\n\t\t\tprogress.AddScanned(c, c.ImageID())\n\t\tcase session.UpdatedState:\n\t\t\tc, newImage := CreateContainerForProgress(index, 11, \"updt%d\")\n\t\t\tprogress.AddScanned(c, newImage)\n\t\t\tprogress.MarkForUpdate(c.ID())\n\t\tcase session.FailedState:\n\t\t\tc, newImage := CreateContainerForProgress(index, 21, \"fail%d\")\n\t\t\tprogress.AddScanned(c, newImage)\n\t\t\tfailed[c.ID()] = errors.New(\"accidentally the whole container\")\n\t\t}\n\n\t\tstateNums[state] = index + 1\n\t}\n\tprogress.UpdateFailed(failed)\n\n\treturn progress.Report()\n\n}\n"
  },
  {
    "path": "internal/actions/update.go",
    "content": "package actions\n\nimport (\n\t\"errors\"\n\n\t\"github.com/containrrr/watchtower/internal/util\"\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\t\"github.com/containrrr/watchtower/pkg/lifecycle\"\n\t\"github.com/containrrr/watchtower/pkg/session\"\n\t\"github.com/containrrr/watchtower/pkg/sorter\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Update looks at the running Docker containers to see if any of the images\n// used to start those containers have been updated. If a change is detected in\n// any of the images, the associated containers are stopped and restarted with\n// the new image.\nfunc Update(client container.Client, params types.UpdateParams) (types.Report, error) {\n\tlog.Debug(\"Checking containers for updated images\")\n\tprogress := &session.Progress{}\n\tstaleCount := 0\n\n\tif params.LifecycleHooks {\n\t\tlifecycle.ExecutePreChecks(client, params)\n\t}\n\n\tcontainers, err := client.ListContainers(params.Filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstaleCheckFailed := 0\n\n\tfor i, targetContainer := range containers {\n\t\tstale, newestImage, err := client.IsContainerStale(targetContainer, params)\n\t\tshouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)\n\t\tif err == nil && shouldUpdate {\n\t\t\t// Check to make sure we have all the necessary information for recreating the container\n\t\t\terr = targetContainer.VerifyConfiguration()\n\t\t\t// If the image information is incomplete and trace logging is enabled, log it for further diagnosis\n\t\t\tif err != nil && log.IsLevelEnabled(log.TraceLevel) {\n\t\t\t\timageInfo := targetContainer.ImageInfo()\n\t\t\t\tlog.Tracef(\"Image info: %#v\", imageInfo)\n\t\t\t\tlog.Tracef(\"Container info: %#v\", targetContainer.ContainerInfo())\n\t\t\t\tif imageInfo != nil {\n\t\t\t\t\tlog.Tracef(\"Image config: %#v\", imageInfo.Config)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.Infof(\"Unable to update container %q: %v. Proceeding to next.\", targetContainer.Name(), err)\n\t\t\tstale = false\n\t\t\tstaleCheckFailed++\n\t\t\tprogress.AddSkipped(targetContainer, err)\n\t\t} else {\n\t\t\tprogress.AddScanned(targetContainer, newestImage)\n\t\t}\n\t\tcontainers[i].SetStale(stale)\n\n\t\tif stale {\n\t\t\tstaleCount++\n\t\t}\n\t}\n\n\tcontainers, err = sorter.SortByDependencies(containers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tUpdateImplicitRestart(containers)\n\n\tvar containersToUpdate []types.Container\n\tfor _, c := range containers {\n\t\tif !c.IsMonitorOnly(params) {\n\t\t\tcontainersToUpdate = append(containersToUpdate, c)\n\t\t\tprogress.MarkForUpdate(c.ID())\n\t\t}\n\t}\n\n\tif params.RollingRestart {\n\t\tprogress.UpdateFailed(performRollingRestart(containersToUpdate, client, params))\n\t} else {\n\t\tfailedStop, stoppedImages := stopContainersInReversedOrder(containersToUpdate, client, params)\n\t\tprogress.UpdateFailed(failedStop)\n\t\tfailedStart := restartContainersInSortedOrder(containersToUpdate, client, params, stoppedImages)\n\t\tprogress.UpdateFailed(failedStart)\n\t}\n\n\tif params.LifecycleHooks {\n\t\tlifecycle.ExecutePostChecks(client, params)\n\t}\n\treturn progress.Report(), nil\n}\n\nfunc performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {\n\tcleanupImageIDs := make(map[types.ImageID]bool, len(containers))\n\tfailed := make(map[types.ContainerID]error, len(containers))\n\n\tfor i := len(containers) - 1; i >= 0; i-- {\n\t\tif containers[i].ToRestart() {\n\t\t\terr := stopStaleContainer(containers[i], client, params)\n\t\t\tif err != nil {\n\t\t\t\tfailed[containers[i].ID()] = err\n\t\t\t} else {\n\t\t\t\tif err := restartStaleContainer(containers[i], client, params); err != nil {\n\t\t\t\t\tfailed[containers[i].ID()] = err\n\t\t\t\t} else if containers[i].IsStale() {\n\t\t\t\t\t// Only add (previously) stale containers' images to cleanup\n\t\t\t\t\tcleanupImageIDs[containers[i].ImageID()] = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif params.Cleanup {\n\t\tcleanupImages(client, cleanupImageIDs)\n\t}\n\treturn failed\n}\n\nfunc stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {\n\tfailed = make(map[types.ContainerID]error, len(containers))\n\tstopped = make(map[types.ImageID]bool, len(containers))\n\tfor i := len(containers) - 1; i >= 0; i-- {\n\t\tif err := stopStaleContainer(containers[i], client, params); err != nil {\n\t\t\tfailed[containers[i].ID()] = err\n\t\t} else {\n\t\t\t// NOTE: If a container is restarted due to a dependency this might be empty\n\t\t\tstopped[containers[i].SafeImageID()] = true\n\t\t}\n\n\t}\n\treturn\n}\n\nfunc stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {\n\tif container.IsWatchtower() {\n\t\tlog.Debugf(\"This is the watchtower container %s\", container.Name())\n\t\treturn nil\n\t}\n\n\tif !container.ToRestart() {\n\t\treturn nil\n\t}\n\n\t// Perform an additional check here to prevent us from stopping a linked container we cannot restart\n\tif container.IsLinkedToRestarting() {\n\t\tif err := container.VerifyConfiguration(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif params.LifecycleHooks {\n\t\tskipUpdate, err := lifecycle.ExecutePreUpdateCommand(client, container)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t\tlog.Info(\"Skipping container as the pre-update command failed\")\n\t\t\treturn err\n\t\t}\n\t\tif skipUpdate {\n\t\t\tlog.Debug(\"Skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)\")\n\t\t\treturn errors.New(\"skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)\")\n\t\t}\n\t}\n\n\tif err := client.StopContainer(container, params.Timeout); err != nil {\n\t\tlog.Error(err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {\n\tcleanupImageIDs := make(map[types.ImageID]bool, len(containers))\n\tfailed := make(map[types.ContainerID]error, len(containers))\n\n\tfor _, c := range containers {\n\t\tif !c.ToRestart() {\n\t\t\tcontinue\n\t\t}\n\t\tif stoppedImages[c.SafeImageID()] {\n\t\t\tif err := restartStaleContainer(c, client, params); err != nil {\n\t\t\t\tfailed[c.ID()] = err\n\t\t\t} else if c.IsStale() {\n\t\t\t\t// Only add (previously) stale containers' images to cleanup\n\t\t\t\tcleanupImageIDs[c.ImageID()] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif params.Cleanup {\n\t\tcleanupImages(client, cleanupImageIDs)\n\t}\n\n\treturn failed\n}\n\nfunc cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {\n\tfor imageID := range imageIDs {\n\t\tif imageID == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif err := client.RemoveImageByID(imageID); err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t}\n}\n\nfunc restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {\n\t// Since we can't shutdown a watchtower container immediately, we need to\n\t// start the new one while the old one is still running. This prevents us\n\t// from re-using the same container name so we first rename the current\n\t// instance so that the new one can adopt the old name.\n\tif container.IsWatchtower() {\n\t\tif err := client.RenameContainer(container, util.RandName()); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif !params.NoRestart {\n\t\tif newContainerID, err := client.StartContainer(container); err != nil {\n\t\t\tlog.Error(err)\n\t\t\treturn err\n\t\t} else if container.ToRestart() && params.LifecycleHooks {\n\t\t\tlifecycle.ExecutePostUpdateCommand(client, newContainerID)\n\t\t}\n\t}\n\treturn nil\n}\n\n// UpdateImplicitRestart iterates through the passed containers, setting the\n// `LinkedToRestarting` flag if any of it's linked containers are marked for restart\nfunc UpdateImplicitRestart(containers []types.Container) {\n\n\tfor ci, c := range containers {\n\t\tif c.ToRestart() {\n\t\t\t// The container is already marked for restart, no need to check\n\t\t\tcontinue\n\t\t}\n\n\t\tif link := linkedContainerMarkedForRestart(c.Links(), containers); link != \"\" {\n\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\"restarting\": link,\n\t\t\t\t\"linked\":     c.Name(),\n\t\t\t}).Debug(\"container is linked to restarting\")\n\t\t\t// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy\n\t\t\tcontainers[ci].SetLinkedToRestarting(true)\n\t\t}\n\n\t}\n}\n\n// linkedContainerMarkedForRestart returns the name of the first link that matches a\n// container marked for restart\nfunc linkedContainerMarkedForRestart(links []string, containers []types.Container) string {\n\tfor _, linkName := range links {\n\t\tfor _, candidate := range containers {\n\t\t\tif candidate.Name() == linkName && candidate.ToRestart() {\n\t\t\t\treturn linkName\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/actions/update_test.go",
    "content": "package actions_test\n\nimport (\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/actions\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tdockerTypes \"github.com/docker/docker/api/types\"\n\tdockerContainer \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/go-connections/nat\"\n\n\t. \"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc getCommonTestData(keepContainer string) *TestData {\n\treturn &TestData{\n\t\tNameOfContainerToKeep: keepContainer,\n\t\tContainers: []types.Container{\n\t\t\tCreateMockContainer(\n\t\t\t\t\"test-container-01\",\n\t\t\t\t\"test-container-01\",\n\t\t\t\t\"fake-image:latest\",\n\t\t\t\ttime.Now().AddDate(0, 0, -1)),\n\t\t\tCreateMockContainer(\n\t\t\t\t\"test-container-02\",\n\t\t\t\t\"test-container-02\",\n\t\t\t\t\"fake-image:latest\",\n\t\t\t\ttime.Now()),\n\t\t\tCreateMockContainer(\n\t\t\t\t\"test-container-02\",\n\t\t\t\t\"test-container-02\",\n\t\t\t\t\"fake-image:latest\",\n\t\t\t\ttime.Now()),\n\t\t},\n\t}\n}\n\nfunc getLinkedTestData(withImageInfo bool) *TestData {\n\tstaleContainer := CreateMockContainer(\n\t\t\"test-container-01\",\n\t\t\"/test-container-01\",\n\t\t\"fake-image1:latest\",\n\t\ttime.Now().AddDate(0, 0, -1))\n\n\tvar imageInfo *dockerTypes.ImageInspect\n\tif withImageInfo {\n\t\timageInfo = CreateMockImageInfo(\"test-container-02\")\n\t}\n\tlinkingContainer := CreateMockContainerWithLinks(\n\t\t\"test-container-02\",\n\t\t\"/test-container-02\",\n\t\t\"fake-image2:latest\",\n\t\ttime.Now(),\n\t\t[]string{staleContainer.Name()},\n\t\timageInfo)\n\n\treturn &TestData{\n\t\tStaleness: map[string]bool{linkingContainer.Name(): false},\n\t\tContainers: []types.Container{\n\t\t\tstaleContainer,\n\t\t\tlinkingContainer,\n\t\t},\n\t}\n}\n\nvar _ = Describe(\"the update action\", func() {\n\tWhen(\"watchtower has been instructed to clean up\", func() {\n\t\tWhen(\"there are multiple containers using the same image\", func() {\n\t\t\tIt(\"should only try to remove the image once\", func() {\n\t\t\t\tclient := CreateMockClient(getCommonTestData(\"\"), false, false)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\t\t})\n\t\tWhen(\"there are multiple containers using different images\", func() {\n\t\t\tIt(\"should try to remove each of them\", func() {\n\t\t\t\ttestData := getCommonTestData(\"\")\n\t\t\t\ttestData.Containers = append(\n\t\t\t\t\ttestData.Containers,\n\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\"unique-test-container\",\n\t\t\t\t\t\t\"unique-test-container\",\n\t\t\t\t\t\t\"unique-fake-image:latest\",\n\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tclient := CreateMockClient(testData, false, false)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(2))\n\t\t\t})\n\t\t})\n\t\tWhen(\"there are linked containers being updated\", func() {\n\t\t\tIt(\"should not try to remove their images\", func() {\n\t\t\t\tclient := CreateMockClient(getLinkedTestData(true), false, false)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\t\t})\n\t\tWhen(\"performing a rolling restart update\", func() {\n\t\t\tIt(\"should try to remove the image once\", func() {\n\t\t\t\tclient := CreateMockClient(getCommonTestData(\"\"), false, false)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\t\t})\n\t\tWhen(\"updating a linked container with missing image info\", func() {\n\t\t\tIt(\"should gracefully fail\", func() {\n\t\t\t\tclient := CreateMockClient(getLinkedTestData(false), false, false)\n\n\t\t\t\treport, err := actions.Update(client, types.UpdateParams{})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t// Note: Linked containers that were skipped for recreation is not counted in Failed\n\t\t\t\t// If this happens, an error is emitted to the logs, so a notification should still be sent.\n\t\t\t\tExpect(report.Updated()).To(HaveLen(1))\n\t\t\t\tExpect(report.Fresh()).To(HaveLen(1))\n\t\t\t})\n\t\t})\n\t})\n\n\tWhen(\"watchtower has been instructed to monitor only\", func() {\n\t\tWhen(\"certain containers are set to monitor only\", func() {\n\t\t\tIt(\"should not update those containers\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\tNameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"fake-image1:latest\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.monitor-only\": \"true\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"monitor only is set globally\", func() {\n\t\t\tIt(\"should not update any containers\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\"fake-image:latest\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image:latest\",\n\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(0))\n\t\t\t})\n\t\t\tWhen(\"watchtower has been instructed to have label take precedence\", func() {\n\t\t\t\tIt(\"it should update containers when monitor only is set to false\", func() {\n\t\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t\t&TestData{\n\t\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.monitor-only\": \"false\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)\n\t\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t\t})\n\t\t\t\tIt(\"it should update not containers when monitor only is set to true\", func() {\n\t\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t\t&TestData{\n\t\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.monitor-only\": \"true\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)\n\t\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(0))\n\t\t\t\t})\n\t\t\t\tIt(\"it should update not containers when monitor only is not set\", func() {\n\t\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t\t&TestData{\n\t\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\t\tCreateMockContainer(\n\t\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\t\"test-container-01\",\n\t\t\t\t\t\t\t\t\t\"fake-image:latest\",\n\t\t\t\t\t\t\t\t\ttime.Now()),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)\n\t\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(0))\n\t\t\t\t})\n\n\t\t\t})\n\t\t})\n\t})\n\n\tWhen(\"watchtower has been instructed to run lifecycle hooks\", func() {\n\n\t\tWhen(\"pre-update script returns 1\", func() {\n\t\t\tIt(\"should not update those containers\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\": \"190\",\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update\":         \"/PreUpdateReturn1.sh\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(0))\n\t\t\t})\n\n\t\t})\n\n\t\tWhen(\"prupddate script returns 75\", func() {\n\t\t\tIt(\"should not update those containers\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\": \"190\",\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update\":         \"/PreUpdateReturn75.sh\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(0))\n\t\t\t})\n\n\t\t})\n\n\t\tWhen(\"prupddate script returns 0\", func() {\n\t\t\tIt(\"should update those containers\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\": \"190\",\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update\":         \"/PreUpdateReturn0.sh\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"container is linked to restarting containers\", func() {\n\t\t\tIt(\"should be marked for restart\", func() {\n\n\t\t\t\tprovider := CreateMockContainerWithConfig(\n\t\t\t\t\t\"test-container-provider\",\n\t\t\t\t\t\"/test-container-provider\",\n\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\ttrue,\n\t\t\t\t\tfalse,\n\t\t\t\t\ttime.Now(),\n\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\tLabels:       map[string]string{},\n\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t})\n\n\t\t\t\tprovider.SetStale(true)\n\n\t\t\t\tconsumer := CreateMockContainerWithConfig(\n\t\t\t\t\t\"test-container-consumer\",\n\t\t\t\t\t\"/test-container-consumer\",\n\t\t\t\t\t\"fake-image3:latest\",\n\t\t\t\t\ttrue,\n\t\t\t\t\tfalse,\n\t\t\t\t\ttime.Now(),\n\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.depends-on\": \"test-container-provider\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t})\n\n\t\t\t\tcontainers := []types.Container{\n\t\t\t\t\tprovider,\n\t\t\t\t\tconsumer,\n\t\t\t\t}\n\n\t\t\t\tExpect(provider.ToRestart()).To(BeTrue())\n\t\t\t\tExpect(consumer.ToRestart()).To(BeFalse())\n\n\t\t\t\tactions.UpdateImplicitRestart(containers)\n\n\t\t\t\tExpect(containers[0].ToRestart()).To(BeTrue())\n\t\t\t\tExpect(containers[1].ToRestart()).To(BeTrue())\n\n\t\t\t})\n\n\t\t})\n\n\t\tWhen(\"container is not running\", func() {\n\t\t\tIt(\"skip running preupdate\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\": \"190\",\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update\":         \"/PreUpdateReturn1.sh\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\n\t\t})\n\n\t\tWhen(\"container is restarting\", func() {\n\t\t\tIt(\"skip running preupdate\", func() {\n\t\t\t\tclient := CreateMockClient(\n\t\t\t\t\t&TestData{\n\t\t\t\t\t\t//NameOfContainerToKeep: \"test-container-02\",\n\t\t\t\t\t\tContainers: []types.Container{\n\t\t\t\t\t\t\tCreateMockContainerWithConfig(\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"test-container-02\",\n\t\t\t\t\t\t\t\t\"fake-image2:latest\",\n\t\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\ttime.Now(),\n\t\t\t\t\t\t\t\t&dockerContainer.Config{\n\t\t\t\t\t\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\": \"190\",\n\t\t\t\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update\":         \"/PreUpdateReturn1.sh\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tExposedPorts: map[nat.Port]struct{}{},\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tfalse,\n\t\t\t\t\tfalse,\n\t\t\t\t)\n\t\t\t\t_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(client.TestData.TriedToRemoveImageCount).To(Equal(1))\n\t\t\t})\n\n\t\t})\n\n\t})\n})\n"
  },
  {
    "path": "internal/flags/flags.go",
    "content": "package flags\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n)\n\n// DockerAPIMinVersion is the minimum version of the docker api required to\n// use watchtower\nconst DockerAPIMinVersion string = \"1.25\"\n\nvar defaultInterval = int((time.Hour * 24).Seconds())\n\n// RegisterDockerFlags that are used directly by the docker api client\nfunc RegisterDockerFlags(rootCmd *cobra.Command) {\n\tflags := rootCmd.PersistentFlags()\n\tflags.StringP(\"host\", \"H\", envString(\"DOCKER_HOST\"), \"daemon socket to connect to\")\n\tflags.BoolP(\"tlsverify\", \"v\", envBool(\"DOCKER_TLS_VERIFY\"), \"use TLS and verify the remote\")\n\tflags.StringP(\"api-version\", \"a\", envString(\"DOCKER_API_VERSION\"), \"api version to use by docker client\")\n}\n\n// RegisterSystemFlags that are used by watchtower to modify the program flow\nfunc RegisterSystemFlags(rootCmd *cobra.Command) {\n\tflags := rootCmd.PersistentFlags()\n\tflags.IntP(\n\t\t\"interval\",\n\t\t\"i\",\n\t\tenvInt(\"WATCHTOWER_POLL_INTERVAL\"),\n\t\t\"Poll interval (in seconds)\")\n\n\tflags.StringP(\n\t\t\"schedule\",\n\t\t\"s\",\n\t\tenvString(\"WATCHTOWER_SCHEDULE\"),\n\t\t\"The cron expression which defines when to update\")\n\n\tflags.DurationP(\n\t\t\"stop-timeout\",\n\t\t\"t\",\n\t\tenvDuration(\"WATCHTOWER_TIMEOUT\"),\n\t\t\"Timeout before a container is forcefully stopped\")\n\n\tflags.BoolP(\n\t\t\"no-pull\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NO_PULL\"),\n\t\t\"Do not pull any new images\")\n\n\tflags.BoolP(\n\t\t\"no-restart\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NO_RESTART\"),\n\t\t\"Do not restart any containers\")\n\n\tflags.BoolP(\n\t\t\"no-startup-message\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NO_STARTUP_MESSAGE\"),\n\t\t\"Prevents watchtower from sending a startup message\")\n\n\tflags.BoolP(\n\t\t\"cleanup\",\n\t\t\"c\",\n\t\tenvBool(\"WATCHTOWER_CLEANUP\"),\n\t\t\"Remove previously used images after updating\")\n\n\tflags.BoolP(\n\t\t\"remove-volumes\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_REMOVE_VOLUMES\"),\n\t\t\"Remove attached volumes before updating\")\n\n\tflags.BoolP(\n\t\t\"label-enable\",\n\t\t\"e\",\n\t\tenvBool(\"WATCHTOWER_LABEL_ENABLE\"),\n\t\t\"Watch containers where the com.centurylinklabs.watchtower.enable label is true\")\n\n\tflags.StringSliceP(\n\t\t\"disable-containers\",\n\t\t\"x\",\n\t\t// Due to issue spf13/viper#380, can't use viper.GetStringSlice:\n\t\tregexp.MustCompile(\"[, ]+\").Split(envString(\"WATCHTOWER_DISABLE_CONTAINERS\"), -1),\n\t\t\"Comma-separated list of containers to explicitly exclude from watching.\")\n\n\tflags.StringP(\n\t\t\"log-format\",\n\t\t\"l\",\n\t\tviper.GetString(\"WATCHTOWER_LOG_FORMAT\"),\n\t\t\"Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON\")\n\n\tflags.BoolP(\n\t\t\"debug\",\n\t\t\"d\",\n\t\tenvBool(\"WATCHTOWER_DEBUG\"),\n\t\t\"Enable debug mode with verbose logging\")\n\n\tflags.BoolP(\n\t\t\"trace\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_TRACE\"),\n\t\t\"Enable trace mode with very verbose logging - caution, exposes credentials\")\n\n\tflags.BoolP(\n\t\t\"monitor-only\",\n\t\t\"m\",\n\t\tenvBool(\"WATCHTOWER_MONITOR_ONLY\"),\n\t\t\"Will only monitor for new images, not update the containers\")\n\n\tflags.BoolP(\n\t\t\"run-once\",\n\t\t\"R\",\n\t\tenvBool(\"WATCHTOWER_RUN_ONCE\"),\n\t\t\"Run once now and exit\")\n\n\tflags.BoolP(\n\t\t\"include-restarting\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_INCLUDE_RESTARTING\"),\n\t\t\"Will also include restarting containers\")\n\n\tflags.BoolP(\n\t\t\"include-stopped\",\n\t\t\"S\",\n\t\tenvBool(\"WATCHTOWER_INCLUDE_STOPPED\"),\n\t\t\"Will also include created and exited containers\")\n\n\tflags.BoolP(\n\t\t\"revive-stopped\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_REVIVE_STOPPED\"),\n\t\t\"Will also start stopped containers that were updated, if include-stopped is active\")\n\n\tflags.BoolP(\n\t\t\"enable-lifecycle-hooks\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_LIFECYCLE_HOOKS\"),\n\t\t\"Enable the execution of commands triggered by pre- and post-update lifecycle hooks\")\n\n\tflags.BoolP(\n\t\t\"rolling-restart\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_ROLLING_RESTART\"),\n\t\t\"Restart containers one at a time\")\n\n\tflags.BoolP(\n\t\t\"http-api-update\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_HTTP_API_UPDATE\"),\n\t\t\"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request\")\n\tflags.BoolP(\n\t\t\"http-api-metrics\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_HTTP_API_METRICS\"),\n\t\t\"Runs Watchtower with the Prometheus metrics API enabled\")\n\n\tflags.StringP(\n\t\t\"http-api-token\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_HTTP_API_TOKEN\"),\n\t\t\"Sets an authentication token to HTTP API requests.\")\n\n\tflags.BoolP(\n\t\t\"http-api-periodic-polls\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_HTTP_API_PERIODIC_POLLS\"),\n\t\t\"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled\")\n\n\t// https://no-color.org/\n\tflags.BoolP(\n\t\t\"no-color\",\n\t\t\"\",\n\t\tviper.IsSet(\"NO_COLOR\"),\n\t\t\"Disable ANSI color escape codes in log output\")\n\n\tflags.StringP(\n\t\t\"scope\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_SCOPE\"),\n\t\t\"Defines a monitoring scope for the Watchtower instance.\")\n\n\tflags.StringP(\n\t\t\"porcelain\",\n\t\t\"P\",\n\t\tenvString(\"WATCHTOWER_PORCELAIN\"),\n\t\t`Write session results to stdout using a stable versioned format. Supported values: \"v1\"`)\n\n\tflags.String(\n\t\t\"log-level\",\n\t\tenvString(\"WATCHTOWER_LOG_LEVEL\"),\n\t\t\"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace\")\n\n\tflags.BoolP(\n\t\t\"health-check\",\n\t\t\"\",\n\t\tfalse,\n\t\t\"Do health check and exit\")\n\n\tflags.BoolP(\n\t\t\"label-take-precedence\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_LABEL_TAKE_PRECEDENCE\"),\n\t\t\"Label applied to containers take precedence over arguments\")\n}\n\n// RegisterNotificationFlags that are used by watchtower to send notifications\nfunc RegisterNotificationFlags(rootCmd *cobra.Command) {\n\tflags := rootCmd.PersistentFlags()\n\n\tflags.StringSliceP(\n\t\t\"notifications\",\n\t\t\"n\",\n\t\tenvStringSlice(\"WATCHTOWER_NOTIFICATIONS\"),\n\t\t\" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)\")\n\n\tflags.String(\n\t\t\"notifications-level\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATIONS_LEVEL\"),\n\t\t\"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug\")\n\n\tflags.IntP(\n\t\t\"notifications-delay\",\n\t\t\"\",\n\t\tenvInt(\"WATCHTOWER_NOTIFICATIONS_DELAY\"),\n\t\t\"Delay before sending notifications, expressed in seconds\")\n\n\tflags.StringP(\n\t\t\"notifications-hostname\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATIONS_HOSTNAME\"),\n\t\t\"Custom hostname for notification titles\")\n\n\tflags.StringP(\n\t\t\"notification-email-from\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_FROM\"),\n\t\t\"Address to send notification emails from\")\n\n\tflags.StringP(\n\t\t\"notification-email-to\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_TO\"),\n\t\t\"Address to send notification emails to\")\n\n\tflags.IntP(\n\t\t\"notification-email-delay\",\n\t\t\"\",\n\t\tenvInt(\"WATCHTOWER_NOTIFICATION_EMAIL_DELAY\"),\n\t\t\"Delay before sending notifications, expressed in seconds\")\n\n\tflags.StringP(\n\t\t\"notification-email-server\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER\"),\n\t\t\"SMTP server to send notification emails through\")\n\n\tflags.IntP(\n\t\t\"notification-email-server-port\",\n\t\t\"\",\n\t\tenvInt(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT\"),\n\t\t\"SMTP server port to send notification emails through\")\n\n\tflags.BoolP(\n\t\t\"notification-email-server-tls-skip-verify\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY\"),\n\t\t`Controls whether watchtower verifies the SMTP server's certificate chain and host name.\nShould only be used for testing.`)\n\n\tflags.StringP(\n\t\t\"notification-email-server-user\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER\"),\n\t\t\"SMTP server user for sending notifications\")\n\n\tflags.StringP(\n\t\t\"notification-email-server-password\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD\"),\n\t\t\"SMTP server password for sending notifications\")\n\n\tflags.StringP(\n\t\t\"notification-email-subjecttag\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG\"),\n\t\t\"Subject prefix tag for notifications via mail\")\n\n\tflags.StringP(\n\t\t\"notification-slack-hook-url\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL\"),\n\t\t\"The Slack Hook URL to send notifications to\")\n\n\tflags.StringP(\n\t\t\"notification-slack-identifier\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER\"),\n\t\t\"A string which will be used to identify the messages coming from this watchtower instance\")\n\n\tflags.StringP(\n\t\t\"notification-slack-channel\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_SLACK_CHANNEL\"),\n\t\t\"A string which overrides the webhook's default channel. Example: #my-custom-channel\")\n\n\tflags.StringP(\n\t\t\"notification-slack-icon-emoji\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI\"),\n\t\t\"An emoji code string to use in place of the default icon\")\n\n\tflags.StringP(\n\t\t\"notification-slack-icon-url\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL\"),\n\t\t\"An icon image URL string to use in place of the default icon\")\n\n\tflags.StringP(\n\t\t\"notification-msteams-hook\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL\"),\n\t\t\"The MSTeams WebHook URL to send notifications to\")\n\n\tflags.BoolP(\n\t\t\"notification-msteams-data\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA\"),\n\t\t\"The MSTeams notifier will try to extract log entry fields as MSTeams message facts\")\n\n\tflags.StringP(\n\t\t\"notification-gotify-url\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_GOTIFY_URL\"),\n\t\t\"The Gotify URL to send notifications to\")\n\n\tflags.StringP(\n\t\t\"notification-gotify-token\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN\"),\n\t\t\"The Gotify Application required to query the Gotify API\")\n\n\tflags.BoolP(\n\t\t\"notification-gotify-tls-skip-verify\",\n\t\t\"\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY\"),\n\t\t`Controls whether watchtower verifies the Gotify server's certificate chain and host name.\nShould only be used for testing.`)\n\n\tflags.String(\n\t\t\"notification-template\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_TEMPLATE\"),\n\t\t\"The shoutrrr text/template for the messages\")\n\n\tflags.StringArray(\n\t\t\"notification-url\",\n\t\tenvStringSlice(\"WATCHTOWER_NOTIFICATION_URL\"),\n\t\t\"The shoutrrr URL to send notifications to\")\n\n\tflags.Bool(\"notification-report\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_REPORT\"),\n\t\t\"Use the session report as the notification template data\")\n\n\tflags.StringP(\n\t\t\"notification-title-tag\",\n\t\t\"\",\n\t\tenvString(\"WATCHTOWER_NOTIFICATION_TITLE_TAG\"),\n\t\t\"Title prefix tag for notifications\")\n\n\tflags.Bool(\"notification-skip-title\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_SKIP_TITLE\"),\n\t\t\"Do not pass the title param to notifications\")\n\n\tflags.String(\n\t\t\"warn-on-head-failure\",\n\t\tenvString(\"WATCHTOWER_WARN_ON_HEAD_FAILURE\"),\n\t\t\"When to warn about HEAD pull requests failing. Possible values: always, auto or never\")\n\n\tflags.Bool(\n\t\t\"notification-log-stdout\",\n\t\tenvBool(\"WATCHTOWER_NOTIFICATION_LOG_STDOUT\"),\n\t\t\"Write notification logs to stdout instead of logging (to stderr)\")\n}\n\nfunc envString(key string) string {\n\tviper.MustBindEnv(key)\n\treturn viper.GetString(key)\n}\n\nfunc envStringSlice(key string) []string {\n\tviper.MustBindEnv(key)\n\treturn viper.GetStringSlice(key)\n}\n\nfunc envInt(key string) int {\n\tviper.MustBindEnv(key)\n\treturn viper.GetInt(key)\n}\n\nfunc envBool(key string) bool {\n\tviper.MustBindEnv(key)\n\treturn viper.GetBool(key)\n}\n\nfunc envDuration(key string) time.Duration {\n\tviper.MustBindEnv(key)\n\treturn viper.GetDuration(key)\n}\n\n// SetDefaults provides default values for environment variables\nfunc SetDefaults() {\n\tviper.AutomaticEnv()\n\tviper.SetDefault(\"DOCKER_HOST\", \"unix:///var/run/docker.sock\")\n\tviper.SetDefault(\"DOCKER_API_VERSION\", DockerAPIMinVersion)\n\tviper.SetDefault(\"WATCHTOWER_POLL_INTERVAL\", defaultInterval)\n\tviper.SetDefault(\"WATCHTOWER_TIMEOUT\", time.Second*10)\n\tviper.SetDefault(\"WATCHTOWER_NOTIFICATIONS\", []string{})\n\tviper.SetDefault(\"WATCHTOWER_NOTIFICATIONS_LEVEL\", \"info\")\n\tviper.SetDefault(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT\", 25)\n\tviper.SetDefault(\"WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG\", \"\")\n\tviper.SetDefault(\"WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER\", \"watchtower\")\n\tviper.SetDefault(\"WATCHTOWER_LOG_LEVEL\", \"info\")\n\tviper.SetDefault(\"WATCHTOWER_LOG_FORMAT\", \"auto\")\n}\n\n// EnvConfig translates the command-line options into environment variables\n// that will initialize the api client\nfunc EnvConfig(cmd *cobra.Command) error {\n\tvar err error\n\tvar host string\n\tvar tls bool\n\tvar version string\n\n\tflags := cmd.PersistentFlags()\n\n\tif host, err = flags.GetString(\"host\"); err != nil {\n\t\treturn err\n\t}\n\tif tls, err = flags.GetBool(\"tlsverify\"); err != nil {\n\t\treturn err\n\t}\n\tif version, err = flags.GetString(\"api-version\"); err != nil {\n\t\treturn err\n\t}\n\tif err = setEnvOptStr(\"DOCKER_HOST\", host); err != nil {\n\t\treturn err\n\t}\n\tif err = setEnvOptBool(\"DOCKER_TLS_VERIFY\", tls); err != nil {\n\t\treturn err\n\t}\n\tif err = setEnvOptStr(\"DOCKER_API_VERSION\", version); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// ReadFlags reads common flags used in the main program flow of watchtower\nfunc ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) {\n\tflags := cmd.PersistentFlags()\n\n\tvar err error\n\tvar cleanup bool\n\tvar noRestart bool\n\tvar monitorOnly bool\n\tvar timeout time.Duration\n\n\tif cleanup, err = flags.GetBool(\"cleanup\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif noRestart, err = flags.GetBool(\"no-restart\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif monitorOnly, err = flags.GetBool(\"monitor-only\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif timeout, err = flags.GetDuration(\"stop-timeout\"); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\treturn cleanup, noRestart, monitorOnly, timeout\n}\n\nfunc setEnvOptStr(env string, opt string) error {\n\tif opt == \"\" || opt == os.Getenv(env) {\n\t\treturn nil\n\t}\n\terr := os.Setenv(env, opt)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc setEnvOptBool(env string, opt bool) error {\n\tif opt {\n\t\treturn setEnvOptStr(env, \"1\")\n\t}\n\treturn nil\n}\n\n// GetSecretsFromFiles checks if passwords/tokens/webhooks have been passed as a file instead of plaintext.\n// If so, the value of the flag will be replaced with the contents of the file.\nfunc GetSecretsFromFiles(rootCmd *cobra.Command) {\n\tflags := rootCmd.PersistentFlags()\n\n\tsecrets := []string{\n\t\t\"notification-email-server-password\",\n\t\t\"notification-slack-hook-url\",\n\t\t\"notification-msteams-hook\",\n\t\t\"notification-gotify-token\",\n\t\t\"notification-url\",\n\t\t\"http-api-token\",\n\t}\n\tfor _, secret := range secrets {\n\t\tif err := getSecretFromFile(flags, secret); err != nil {\n\t\t\tlog.Fatalf(\"failed to get secret from flag %v: %s\", secret, err)\n\t\t}\n\t}\n}\n\n// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.\nfunc getSecretFromFile(flags *pflag.FlagSet, secret string) error {\n\tflag := flags.Lookup(secret)\n\tif sliceValue, ok := flag.Value.(pflag.SliceValue); ok {\n\t\toldValues := sliceValue.GetSlice()\n\t\tvalues := make([]string, 0, len(oldValues))\n\t\tfor _, value := range oldValues {\n\t\t\tif value != \"\" && isFile(value) {\n\t\t\t\tfile, err := os.Open(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tscanner := bufio.NewScanner(file)\n\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\tline := scanner.Text()\n\t\t\t\t\tif line == \"\" {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tvalues = append(values, line)\n\t\t\t\t}\n\t\t\t\tif err := file.Close(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvalues = append(values, value)\n\t\t\t}\n\t\t}\n\t\treturn sliceValue.Replace(values)\n\t}\n\n\tvalue := flag.Value.String()\n\tif value != \"\" && isFile(value) {\n\t\tcontent, err := os.ReadFile(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn flags.Set(secret, strings.TrimSpace(string(content)))\n\t}\n\n\treturn nil\n}\n\nfunc isFile(s string) bool {\n\tfirstColon := strings.IndexRune(s, ':')\n\tif firstColon != 1 && firstColon != -1 {\n\t\t// If the string contains a ':', but it's not the second character, it's probably not a file\n\t\t// and will cause a fatal error on windows if stat'ed\n\t\t// This still allows for paths that start with 'c:\\' etc.\n\t\treturn false\n\t}\n\t_, err := os.Stat(s)\n\treturn !errors.Is(err, os.ErrNotExist)\n}\n\n// ProcessFlagAliases updates the value of flags that are being set by helper flags\nfunc ProcessFlagAliases(flags *pflag.FlagSet) {\n\n\tporcelain, err := flags.GetString(`porcelain`)\n\tif err != nil {\n\t\tlog.Fatalf(`Failed to get flag: %v`, err)\n\t}\n\tif porcelain != \"\" {\n\t\tif porcelain != \"v1\" {\n\t\t\tlog.Fatalf(`Unknown porcelain version %q. Supported values: \"v1\"`, porcelain)\n\t\t}\n\t\tif err = appendFlagValue(flags, `notification-url`, `logger://`); err != nil {\n\t\t\tlog.Errorf(`Failed to set flag: %v`, err)\n\t\t}\n\t\tsetFlagIfDefault(flags, `notification-log-stdout`, `true`)\n\t\tsetFlagIfDefault(flags, `notification-report`, `true`)\n\t\ttpl := fmt.Sprintf(`porcelain.%s.summary-no-log`, porcelain)\n\t\tsetFlagIfDefault(flags, `notification-template`, tpl)\n\t}\n\n\tscheduleChanged := flags.Changed(`schedule`)\n\tintervalChanged := flags.Changed(`interval`)\n\t// FIXME: snakeswap\n\t// due to how viper is integrated by swapping the defaults for the flags, we need this hack:\n\tif val, _ := flags.GetString(`schedule`); val != `` {\n\t\tscheduleChanged = true\n\t}\n\tif val, _ := flags.GetInt(`interval`); val != defaultInterval {\n\t\tintervalChanged = true\n\t}\n\n\tif intervalChanged && scheduleChanged {\n\t\tlog.Fatal(`Only schedule or interval can be defined, not both.`)\n\t}\n\n\t// update schedule flag to match interval if it's set, or to the default if none of them are\n\tif intervalChanged || !scheduleChanged {\n\t\tinterval, _ := flags.GetInt(`interval`)\n\t\t_ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))\n\t}\n\n\tif flagIsEnabled(flags, `debug`) {\n\t\t_ = flags.Set(`log-level`, `debug`)\n\t}\n\n\tif flagIsEnabled(flags, `trace`) {\n\t\t_ = flags.Set(`log-level`, `trace`)\n\t}\n\n}\n\n// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger\nfunc SetupLogging(f *pflag.FlagSet) error {\n\tlogFormat, _ := f.GetString(`log-format`)\n\tnoColor, _ := f.GetBool(\"no-color\")\n\n\tswitch strings.ToLower(logFormat) {\n\tcase \"auto\":\n\t\t// This will either use the \"pretty\" or \"logfmt\" format, based on whether the standard out is connected to a TTY\n\t\tlog.SetFormatter(&log.TextFormatter{\n\t\t\tDisableColors: noColor,\n\t\t\t// enable logrus built-in support for https://bixense.com/clicolors/\n\t\t\tEnvironmentOverrideColors: true,\n\t\t})\n\tcase \"json\":\n\t\tlog.SetFormatter(&log.JSONFormatter{})\n\tcase \"logfmt\":\n\t\tlog.SetFormatter(&log.TextFormatter{\n\t\t\tDisableColors: true,\n\t\t\tFullTimestamp: true,\n\t\t})\n\tcase \"pretty\":\n\t\tlog.SetFormatter(&log.TextFormatter{\n\t\t\t// \"Pretty\" format combined with `--no-color` will only change the timestamp to the time since start\n\t\t\tForceColors:   !noColor,\n\t\t\tFullTimestamp: false,\n\t\t})\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid log format: %s\", logFormat)\n\t}\n\n\trawLogLevel, _ := f.GetString(`log-level`)\n\tif logLevel, err := log.ParseLevel(rawLogLevel); err != nil {\n\t\treturn fmt.Errorf(\"invalid log level: %e\", err)\n\t} else {\n\t\tlog.SetLevel(logLevel)\n\t}\n\n\treturn nil\n}\n\nfunc flagIsEnabled(flags *pflag.FlagSet, name string) bool {\n\tvalue, err := flags.GetBool(name)\n\tif err != nil {\n\t\tlog.Fatalf(`The flag %q is not defined`, name)\n\t}\n\treturn value\n}\n\nfunc appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error {\n\tflag := flags.Lookup(name)\n\tif flag == nil {\n\t\treturn fmt.Errorf(`invalid flag name %q`, name)\n\t}\n\n\tif flagValues, ok := flag.Value.(pflag.SliceValue); ok {\n\t\tfor _, value := range values {\n\t\t\t_ = flagValues.Append(value)\n\t\t}\n\t} else {\n\t\treturn fmt.Errorf(`the value for flag %q is not a slice value`, name)\n\t}\n\n\treturn nil\n}\n\nfunc setFlagIfDefault(flags *pflag.FlagSet, name string, value string) {\n\tif flags.Changed(name) {\n\t\treturn\n\t}\n\tif err := flags.Set(name, value); err != nil {\n\t\tlog.Errorf(`Failed to set flag: %v`, err)\n\t}\n}\n"
  },
  {
    "path": "internal/flags/flags_test.go",
    "content": "package flags\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEnvConfig_Defaults(t *testing.T) {\n\t// Unset testing environments own variables, since those are not what is under test\n\t_ = os.Unsetenv(\"DOCKER_TLS_VERIFY\")\n\t_ = os.Unsetenv(\"DOCKER_HOST\")\n\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\n\terr := EnvConfig(cmd)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"unix:///var/run/docker.sock\", os.Getenv(\"DOCKER_HOST\"))\n\tassert.Equal(t, \"\", os.Getenv(\"DOCKER_TLS_VERIFY\"))\n\t// Re-enable this test when we've moved to github actions.\n\t// assert.Equal(t, DockerAPIMinVersion, os.Getenv(\"DOCKER_API_VERSION\"))\n}\n\nfunc TestEnvConfig_Custom(t *testing.T) {\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\n\terr := cmd.ParseFlags([]string{\"--host\", \"some-custom-docker-host\", \"--tlsverify\", \"--api-version\", \"1.99\"})\n\trequire.NoError(t, err)\n\n\terr = EnvConfig(cmd)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"some-custom-docker-host\", os.Getenv(\"DOCKER_HOST\"))\n\tassert.Equal(t, \"1\", os.Getenv(\"DOCKER_TLS_VERIFY\"))\n\t// Re-enable this test when we've moved to github actions.\n\t// assert.Equal(t, \"1.99\", os.Getenv(\"DOCKER_API_VERSION\"))\n}\n\nfunc TestGetSecretsFromFilesWithString(t *testing.T) {\n\tvalue := \"supersecretstring\"\n\tt.Setenv(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD\", value)\n\n\ttestGetSecretsFromFiles(t, \"notification-email-server-password\", value)\n}\n\nfunc TestGetSecretsFromFilesWithFile(t *testing.T) {\n\tvalue := \"megasecretstring\"\n\n\t// Create the temporary file which will contain a secret.\n\tfile, err := os.CreateTemp(t.TempDir(), \"watchtower-\")\n\trequire.NoError(t, err)\n\n\t// Write the secret to the temporary file.\n\t_, err = file.Write([]byte(value))\n\trequire.NoError(t, err)\n\trequire.NoError(t, file.Close())\n\n\tt.Setenv(\"WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD\", file.Name())\n\n\ttestGetSecretsFromFiles(t, \"notification-email-server-password\", value)\n}\n\nfunc TestGetSliceSecretsFromFiles(t *testing.T) {\n\tvalues := []string{\"entry2\", \"\", \"entry3\"}\n\n\t// Create the temporary file which will contain a secret.\n\tfile, err := os.CreateTemp(t.TempDir(), \"watchtower-\")\n\trequire.NoError(t, err)\n\n\t// Write the secret to the temporary file.\n\tfor _, value := range values {\n\t\t_, err = file.WriteString(\"\\n\" + value)\n\t\trequire.NoError(t, err)\n\t}\n\trequire.NoError(t, file.Close())\n\n\ttestGetSecretsFromFiles(t, \"notification-url\", `[entry1,entry2,entry3]`,\n\t\t`--notification-url`, \"entry1\",\n\t\t`--notification-url`, file.Name())\n}\n\nfunc testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\trequire.NoError(t, cmd.ParseFlags(args))\n\tGetSecretsFromFiles(cmd)\n\tflag := cmd.PersistentFlags().Lookup(flagName)\n\trequire.NotNil(t, flag)\n\tvalue := flag.Value.String()\n\n\tassert.Equal(t, expected, value)\n}\n\nfunc TestHTTPAPIPeriodicPollsFlag(t *testing.T) {\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\n\terr := cmd.ParseFlags([]string{\"--http-api-periodic-polls\"})\n\trequire.NoError(t, err)\n\n\tperiodicPolls, err := cmd.PersistentFlags().GetBool(\"http-api-periodic-polls\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, true, periodicPolls)\n}\n\nfunc TestIsFile(t *testing.T) {\n\tassert.False(t, isFile(\"https://google.com\"), \"an URL should never be considered a file\")\n\tassert.True(t, isFile(os.Args[0]), \"the currently running binary path should always be considered a file\")\n}\n\nfunc TestProcessFlagAliases(t *testing.T) {\n\tlogrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\trequire.NoError(t, cmd.ParseFlags([]string{\n\t\t`--porcelain`, `v1`,\n\t\t`--interval`, `10`,\n\t\t`--trace`,\n\t}))\n\tflags := cmd.Flags()\n\tProcessFlagAliases(flags)\n\n\turls, _ := flags.GetStringArray(`notification-url`)\n\tassert.Contains(t, urls, `logger://`)\n\n\tlogStdout, _ := flags.GetBool(`notification-log-stdout`)\n\tassert.True(t, logStdout)\n\n\treport, _ := flags.GetBool(`notification-report`)\n\tassert.True(t, report)\n\n\ttemplate, _ := flags.GetString(`notification-template`)\n\tassert.Equal(t, `porcelain.v1.summary-no-log`, template)\n\n\tsched, _ := flags.GetString(`schedule`)\n\tassert.Equal(t, `@every 10s`, sched)\n\n\tlogLevel, _ := flags.GetString(`log-level`)\n\tassert.Equal(t, `trace`, logLevel)\n}\n\nfunc TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {\n\tcmd := new(cobra.Command)\n\tt.Setenv(\"WATCHTOWER_DEBUG\", `true`)\n\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\trequire.NoError(t, cmd.ParseFlags([]string{}))\n\tflags := cmd.Flags()\n\tProcessFlagAliases(flags)\n\n\tlogLevel, _ := flags.GetString(`log-level`)\n\tassert.Equal(t, `debug`, logLevel)\n}\n\nfunc TestLogFormatFlag(t *testing.T) {\n\tcmd := new(cobra.Command)\n\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\n\t// Ensure the default value is Auto\n\trequire.NoError(t, cmd.ParseFlags([]string{}))\n\trequire.NoError(t, SetupLogging(cmd.Flags()))\n\tassert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)\n\n\t// Test JSON format\n\trequire.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))\n\trequire.NoError(t, SetupLogging(cmd.Flags()))\n\tassert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)\n\n\t// Test Pretty format\n\trequire.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))\n\trequire.NoError(t, SetupLogging(cmd.Flags()))\n\tassert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)\n\ttextFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)\n\tassert.True(t, ok)\n\tassert.True(t, textFormatter.ForceColors)\n\tassert.False(t, textFormatter.FullTimestamp)\n\n\t// Test LogFmt format\n\trequire.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))\n\trequire.NoError(t, SetupLogging(cmd.Flags()))\n\ttextFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)\n\tassert.True(t, ok)\n\tassert.True(t, textFormatter.DisableColors)\n\tassert.True(t, textFormatter.FullTimestamp)\n\n\t// Test invalid format\n\trequire.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))\n\trequire.Error(t, SetupLogging(cmd.Flags()))\n}\n\nfunc TestLogLevelFlag(t *testing.T) {\n\tcmd := new(cobra.Command)\n\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\n\t// Test invalid format\n\trequire.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))\n\trequire.Error(t, SetupLogging(cmd.Flags()))\n}\n\nfunc TestProcessFlagAliasesSchedAndInterval(t *testing.T) {\n\tlogrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\trequire.NoError(t, cmd.ParseFlags([]string{`--schedule`, `@hourly`, `--interval`, `10`}))\n\tflags := cmd.Flags()\n\n\tassert.PanicsWithValue(t, `FATAL`, func() {\n\t\tProcessFlagAliases(flags)\n\t})\n}\n\nfunc TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {\n\tcmd := new(cobra.Command)\n\n\tt.Setenv(\"WATCHTOWER_SCHEDULE\", `@hourly`)\n\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\trequire.NoError(t, cmd.ParseFlags([]string{}))\n\tflags := cmd.Flags()\n\tProcessFlagAliases(flags)\n\n\tsched, _ := flags.GetString(`schedule`)\n\tassert.Equal(t, `@hourly`, sched)\n}\n\nfunc TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {\n\tlogrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\trequire.NoError(t, cmd.ParseFlags([]string{`--porcelain`, `cowboy`}))\n\tflags := cmd.Flags()\n\n\tassert.PanicsWithValue(t, `FATAL`, func() {\n\t\tProcessFlagAliases(flags)\n\t})\n}\n\nfunc TestFlagsArePrecentInDocumentation(t *testing.T) {\n\n\t// Legacy notifcations are ignored, since they are (soft) deprecated\n\tignoredEnvs := map[string]string{\n\t\t\"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI\": \"legacy\",\n\t\t\"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL\":   \"legacy\",\n\t}\n\n\tignoredFlags := map[string]string{\n\t\t\"notification-gotify-url\":       \"legacy\",\n\t\t\"notification-slack-icon-emoji\": \"legacy\",\n\t\t\"notification-slack-icon-url\":   \"legacy\",\n\t}\n\n\tcmd := new(cobra.Command)\n\tSetDefaults()\n\tRegisterDockerFlags(cmd)\n\tRegisterSystemFlags(cmd)\n\tRegisterNotificationFlags(cmd)\n\n\tflags := cmd.PersistentFlags()\n\n\tdocFiles := []string{\n\t\t\"../../docs/arguments.md\",\n\t\t\"../../docs/lifecycle-hooks.md\",\n\t\t\"../../docs/notifications.md\",\n\t}\n\tallDocs := \"\"\n\tfor _, f := range docFiles {\n\t\tbytes, err := os.ReadFile(f)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Could not load docs file %q: %v\", f, err)\n\t\t}\n\t\tallDocs += string(bytes)\n\t}\n\n\tflags.VisitAll(func(f *pflag.Flag) {\n\t\tif !strings.Contains(allDocs, \"--\"+f.Name) {\n\t\t\tif _, found := ignoredFlags[f.Name]; !found {\n\t\t\t\tt.Logf(\"Docs does not mention flag long name %q\", f.Name)\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t}\n\t\tif !strings.Contains(allDocs, \"-\"+f.Shorthand) {\n\t\t\tt.Logf(\"Docs does not mention flag shorthand %q (%q)\", f.Shorthand, f.Name)\n\t\t\tt.Fail()\n\t\t}\n\t})\n\n\tfor _, key := range viper.AllKeys() {\n\t\tenvKey := strings.ToUpper(key)\n\t\tif !strings.Contains(allDocs, envKey) {\n\t\t\tif _, found := ignoredEnvs[envKey]; !found {\n\t\t\t\tt.Logf(\"Docs does not mention environment variable %q\", envKey)\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/meta/meta.go",
    "content": "package meta\n\nvar (\n\t// Version is the compile-time set version of Watchtower\n\tVersion = \"v0.0.0-unknown\"\n\n\t// UserAgent is the http client identifier derived from Version\n\tUserAgent string\n)\n\nfunc init() {\n\tUserAgent = \"Watchtower/\" + Version\n}\n"
  },
  {
    "path": "internal/util/rand_name.go",
    "content": "package util\n\nimport \"math/rand\"\n\nvar letters = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\n// RandName Generates a random, 32-character, Docker-compatible container name.\nfunc RandName() string {\n\tb := make([]rune, 32)\n\tfor i := range b {\n\t\tb[i] = letters[rand.Intn(len(letters))]\n\t}\n\n\treturn string(b)\n}\n"
  },
  {
    "path": "internal/util/rand_sha256.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n)\n\n// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string\nfunc GenerateRandomSHA256() string {\n\treturn GenerateRandomPrefixedSHA256()[7:]\n}\n\n// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:`\nfunc GenerateRandomPrefixedSHA256() string {\n\thash := make([]byte, 32)\n\t_, _ = rand.Read(hash)\n\tsb := bytes.NewBufferString(\"sha256:\")\n\tsb.Grow(64)\n\tfor _, h := range hash {\n\t\t_, _ = fmt.Fprintf(sb, \"%02x\", h)\n\t}\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "package util\n\n// SliceEqual compares two slices and checks whether they have equal content\nfunc SliceEqual(s1, s2 []string) bool {\n\tif len(s1) != len(s2) {\n\t\treturn false\n\t}\n\n\tfor i := range s1 {\n\t\tif s1[i] != s2[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// SliceSubtract subtracts the content of slice a2 from slice a1\nfunc SliceSubtract(a1, a2 []string) []string {\n\ta := []string{}\n\n\tfor _, e1 := range a1 {\n\t\tfound := false\n\n\t\tfor _, e2 := range a2 {\n\t\t\tif e1 == e2 {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\ta = append(a, e1)\n\t\t}\n\t}\n\n\treturn a\n}\n\n// StringMapSubtract subtracts the content of structmap m2 from structmap m1\nfunc StringMapSubtract(m1, m2 map[string]string) map[string]string {\n\tm := map[string]string{}\n\n\tfor k1, v1 := range m1 {\n\t\tif v2, ok := m2[k1]; ok {\n\t\t\tif v2 != v1 {\n\t\t\t\tm[k1] = v1\n\t\t\t}\n\t\t} else {\n\t\t\tm[k1] = v1\n\t\t}\n\t}\n\n\treturn m\n}\n\n// StructMapSubtract subtracts the content of structmap m2 from structmap m1\nfunc StructMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {\n\tm := map[string]struct{}{}\n\n\tfor k1, v1 := range m1 {\n\t\tif _, ok := m2[k1]; !ok {\n\t\t\tm[k1] = v1\n\t\t}\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/util/util_test.go",
    "content": "package util\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSliceEqual_True(t *testing.T) {\n\ts1 := []string{\"a\", \"b\", \"c\"}\n\ts2 := []string{\"a\", \"b\", \"c\"}\n\n\tresult := SliceEqual(s1, s2)\n\n\tassert.True(t, result)\n}\n\nfunc TestSliceEqual_DifferentLengths(t *testing.T) {\n\ts1 := []string{\"a\", \"b\", \"c\"}\n\ts2 := []string{\"a\", \"b\", \"c\", \"d\"}\n\n\tresult := SliceEqual(s1, s2)\n\n\tassert.False(t, result)\n}\n\nfunc TestSliceEqual_DifferentContents(t *testing.T) {\n\ts1 := []string{\"a\", \"b\", \"c\"}\n\ts2 := []string{\"a\", \"b\", \"d\"}\n\n\tresult := SliceEqual(s1, s2)\n\n\tassert.False(t, result)\n}\n\nfunc TestSliceSubtract(t *testing.T) {\n\ta1 := []string{\"a\", \"b\", \"c\"}\n\ta2 := []string{\"a\", \"c\"}\n\n\tresult := SliceSubtract(a1, a2)\n\tassert.Equal(t, []string{\"b\"}, result)\n\tassert.Equal(t, []string{\"a\", \"b\", \"c\"}, a1)\n\tassert.Equal(t, []string{\"a\", \"c\"}, a2)\n}\n\nfunc TestStringMapSubtract(t *testing.T) {\n\tm1 := map[string]string{\"a\": \"a\", \"b\": \"b\", \"c\": \"sea\"}\n\tm2 := map[string]string{\"a\": \"a\", \"c\": \"c\"}\n\n\tresult := StringMapSubtract(m1, m2)\n\tassert.Equal(t, map[string]string{\"b\": \"b\", \"c\": \"sea\"}, result)\n\tassert.Equal(t, map[string]string{\"a\": \"a\", \"b\": \"b\", \"c\": \"sea\"}, m1)\n\tassert.Equal(t, map[string]string{\"a\": \"a\", \"c\": \"c\"}, m2)\n}\n\nfunc TestStructMapSubtract(t *testing.T) {\n\tx := struct{}{}\n\tm1 := map[string]struct{}{\"a\": x, \"b\": x, \"c\": x}\n\tm2 := map[string]struct{}{\"a\": x, \"c\": x}\n\n\tresult := StructMapSubtract(m1, m2)\n\tassert.Equal(t, map[string]struct{}{\"b\": x}, result)\n\tassert.Equal(t, map[string]struct{}{\"a\": x, \"b\": x, \"c\": x}, m1)\n\tassert.Equal(t, map[string]struct{}{\"a\": x, \"c\": x}, m2)\n}\n\n// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string\nfunc TestGenerateRandomSHA256(t *testing.T) {\n\tres := GenerateRandomSHA256()\n\tassert.Len(t, res, 64)\n\tassert.NotContains(t, res, \"sha256:\")\n}\n\nfunc TestGenerateRandomPrefixedSHA256(t *testing.T) {\n\tres := GenerateRandomPrefixedSHA256()\n\tassert.Regexp(t, regexp.MustCompile(\"sha256:[0-9|a-f]{64}\"), res)\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"github.com/containrrr/watchtower/cmd\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc init() {\n\tlog.SetLevel(log.InfoLevel)\n}\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Watchtower\nsite_url: https://containrrr.dev/watchtower/\nrepo_url: https://github.com/containrrr/watchtower/\nedit_uri: edit/main/docs/\ntheme:\n  name: 'material'\n  palette:\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: containrrr\n      toggle:\n        icon: material/weather-night\n        name: Switch to dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: containrrr-dark\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to light mode\n  logo: images/logo-450px.png\n  favicon: images/favicon.ico\nextra_css:\n  - stylesheets/theme.css\nmarkdown_extensions:\n    - toc:\n        permalink: True\n        separator: \"_\"\n    - admonition\n    - pymdownx.highlight\n    - pymdownx.superfences\n    - pymdownx.magiclink:\n        repo_url_shortener: True\n        provider: github\n        user: containrrr\n        repo: watchtower\n    - pymdownx.saneheaders\n    - pymdownx.tabbed:\n        alternate_style: true\nnav:\n   - 'Home': 'index.md'\n   - 'Introduction': 'introduction.md'\n   - 'Usage overview': 'usage-overview.md'\n   - 'Arguments': 'arguments.md'\n   - 'Notifications': 'notifications.md'\n   - 'Container selection': 'container-selection.md'\n   - 'Private registries': 'private-registries.md'\n   - 'Linked containers': 'linked-containers.md'\n   - 'Remote hosts': 'remote-hosts.md'\n   - 'Secure connections': 'secure-connections.md'\n   - 'Stop signals': 'stop-signals.md'\n   - 'Lifecycle hooks': 'lifecycle-hooks.md'\n   - 'Running multiple instances': 'running-multiple-instances.md'\n   - 'HTTP API Mode': 'http-api-mode.md'\n   - 'Metrics': 'metrics.md'\nplugins:\n    - search\n"
  },
  {
    "path": "pkg/api/api.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst tokenMissingMsg = \"api token is empty or has not been set. exiting\"\n\n// API is the http server responsible for serving the HTTP API endpoints\ntype API struct {\n\tToken       string\n\thasHandlers bool\n}\n\n// New is a factory function creating a new API instance\nfunc New(token string) *API {\n\treturn &API{\n\t\tToken:       token,\n\t\thasHandlers: false,\n\t}\n}\n\n// RequireToken is wrapper around http.HandleFunc that checks token validity\nfunc (api *API) RequireToken(fn http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tauth := r.Header.Get(\"Authorization\")\n\t\twant := fmt.Sprintf(\"Bearer %s\", api.Token)\n\t\tif auth != want {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tlog.Debug(\"Valid token found.\")\n\t\tfn(w, r)\n\t}\n}\n\n// RegisterFunc is a wrapper around http.HandleFunc that also sets the flag used to determine whether to launch the API\nfunc (api *API) RegisterFunc(path string, fn http.HandlerFunc) {\n\tapi.hasHandlers = true\n\thttp.HandleFunc(path, api.RequireToken(fn))\n}\n\n// RegisterHandler is a wrapper around http.Handler that also sets the flag used to determine whether to launch the API\nfunc (api *API) RegisterHandler(path string, handler http.Handler) {\n\tapi.hasHandlers = true\n\thttp.Handle(path, api.RequireToken(handler.ServeHTTP))\n}\n\n// Start the API and serve over HTTP. Requires an API Token to be set.\nfunc (api *API) Start(block bool) error {\n\n\tif !api.hasHandlers {\n\t\tlog.Debug(\"Watchtower HTTP API skipped.\")\n\t\treturn nil\n\t}\n\n\tif api.Token == \"\" {\n\t\tlog.Fatal(tokenMissingMsg)\n\t}\n\n\tif block {\n\t\trunHTTPServer()\n\t} else {\n\t\tgo func() {\n\t\t\trunHTTPServer()\n\t\t}()\n\t}\n\treturn nil\n}\n\nfunc runHTTPServer() {\n\tlog.Fatal(http.ListenAndServe(\":8080\", nil))\n}\n"
  },
  {
    "path": "pkg/api/api_test.go",
    "content": "package api\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nconst (\n\ttoken  = \"123123123\"\n)\n\nfunc TestAPI(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"API Suite\")\n}\n\nvar _ = Describe(\"API\", func() {\n\tapi := New(token)\n\n\tDescribe(\"RequireToken middleware\", func() {\n\t\tIt(\"should return 401 Unauthorized when token is not provided\", func() {\n\t\t\thandlerFunc := api.RequireToken(testHandler)\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/hello\", nil)\n\n\t\t\thandlerFunc(rec, req)\n\n\t\t\tExpect(rec.Code).To(Equal(http.StatusUnauthorized))\n\t\t})\n\n\t\tIt(\"should return 401 Unauthorized when token is invalid\", func() {\n\t\t\thandlerFunc := api.RequireToken(testHandler)\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/hello\", nil)\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer 123\")\n\n\t\t\thandlerFunc(rec, req)\n\n\t\t\tExpect(rec.Code).To(Equal(http.StatusUnauthorized))\n\t\t})\n\n\t\tIt(\"should return 200 OK when token is valid\", func() {\n\t\t\thandlerFunc := api.RequireToken(testHandler)\n\n\t\t\trec := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/hello\", nil)\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \" + token)\n\n\t\t\thandlerFunc(rec, req)\n\n\t\t\tExpect(rec.Code).To(Equal(http.StatusOK))\n\t\t})\n\t})\n})\n\nfunc testHandler(w http.ResponseWriter, req *http.Request) {\n\t_, _ = io.WriteString(w, \"Hello!\")\n}\n"
  },
  {
    "path": "pkg/api/metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/metrics\"\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\n// Handler is an HTTP handle for serving metric data\ntype Handler struct {\n\tPath    string\n\tHandle  http.HandlerFunc\n\tMetrics *metrics.Metrics\n}\n\n// New is a factory function creating a new Metrics instance\nfunc New() *Handler {\n\tm := metrics.Default()\n\thandler := promhttp.Handler()\n\n\treturn &Handler{\n\t\tPath:    \"/v1/metrics\",\n\t\tHandle:  handler.ServeHTTP,\n\t\tMetrics: m,\n\t}\n}\n"
  },
  {
    "path": "pkg/api/metrics/metrics_test.go",
    "content": "package metrics_test\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"github.com/containrrr/watchtower/pkg/api\"\n\tmetricsAPI \"github.com/containrrr/watchtower/pkg/api/metrics\"\n\t\"github.com/containrrr/watchtower/pkg/metrics\"\n)\n\nconst (\n\ttoken  = \"123123123\"\n\tgetURL = \"http://localhost:8080/v1/metrics\"\n)\n\nfunc TestMetrics(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"Metrics Suite\")\n}\n\nfunc getWithToken(handler http.Handler) map[string]string {\n\tmetricMap := map[string]string{}\n\trespWriter := httptest.NewRecorder()\n\n\treq := httptest.NewRequest(\"GET\", getURL, nil)\n\treq.Header.Add(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\n\thandler.ServeHTTP(respWriter, req)\n\n\tres := respWriter.Result()\n\tbody, _ := io.ReadAll(res.Body)\n\n\tfor _, line := range strings.Split(string(body), \"\\n\") {\n\t\tif len(line) < 1 || line[0] == '#' {\n\t\t\tcontinue\n\t\t}\n\t\tparts := strings.Split(line, \" \")\n\t\tmetricMap[parts[0]] = parts[1]\n\t}\n\n\treturn metricMap\n}\n\nvar _ = Describe(\"the metrics API\", func() {\n\thttpAPI := api.New(token)\n\tm := metricsAPI.New()\n\n\thandleReq := httpAPI.RequireToken(m.Handle)\n\ttryGetMetrics := func() map[string]string { return getWithToken(handleReq) }\n\n\tIt(\"should serve metrics\", func() {\n\n\t\tExpect(tryGetMetrics()).To(HaveKeyWithValue(\"watchtower_containers_updated\", \"0\"))\n\n\t\tmetric := &metrics.Metric{\n\t\t\tScanned: 4,\n\t\t\tUpdated: 3,\n\t\t\tFailed:  1,\n\t\t}\n\n\t\tmetrics.RegisterScan(metric)\n\t\tEventually(metrics.Default().QueueIsEmpty).Should(BeTrue())\n\n\t\tEventually(tryGetMetrics).Should(SatisfyAll(\n\t\t\tHaveKeyWithValue(\"watchtower_containers_updated\", \"3\"),\n\t\t\tHaveKeyWithValue(\"watchtower_containers_failed\", \"1\"),\n\t\t\tHaveKeyWithValue(\"watchtower_containers_scanned\", \"4\"),\n\t\t\tHaveKeyWithValue(\"watchtower_scans_total\", \"1\"),\n\t\t\tHaveKeyWithValue(\"watchtower_scans_skipped\", \"0\"),\n\t\t))\n\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tmetrics.RegisterScan(nil)\n\t\t}\n\t\tEventually(metrics.Default().QueueIsEmpty).Should(BeTrue())\n\n\t\tEventually(tryGetMetrics).Should(SatisfyAll(\n\t\t\tHaveKeyWithValue(\"watchtower_scans_total\", \"4\"),\n\t\t\tHaveKeyWithValue(\"watchtower_scans_skipped\", \"3\"),\n\t\t))\n\t})\n})\n"
  },
  {
    "path": "pkg/api/update/update.go",
    "content": "package update\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\tlock chan bool\n)\n\n// New is a factory function creating a new  Handler instance\nfunc New(updateFn func(images []string), updateLock chan bool) *Handler {\n\tif updateLock != nil {\n\t\tlock = updateLock\n\t} else {\n\t\tlock = make(chan bool, 1)\n\t\tlock <- true\n\t}\n\n\treturn &Handler{\n\t\tfn:   updateFn,\n\t\tPath: \"/v1/update\",\n\t}\n}\n\n// Handler is an API handler used for triggering container update scans\ntype Handler struct {\n\tfn   func(images []string)\n\tPath string\n}\n\n// Handle is the actual http.Handle function doing all the heavy lifting\nfunc (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) {\n\tlog.Info(\"Updates triggered by HTTP API request.\")\n\n\t_, err := io.Copy(os.Stdout, r.Body)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\treturn\n\t}\n\n\tvar images []string\n\timageQueries, found := r.URL.Query()[\"image\"]\n\tif found {\n\t\tfor _, image := range imageQueries {\n\t\t\timages = append(images, strings.Split(image, \",\")...)\n\t\t}\n\n\t} else {\n\t\timages = nil\n\t}\n\n\tif len(images) > 0 {\n\t\tchanValue := <-lock\n\t\tdefer func() { lock <- chanValue }()\n\t\thandle.fn(images)\n\t} else {\n\t\tselect {\n\t\tcase chanValue := <-lock:\n\t\t\tdefer func() { lock <- chanValue }()\n\t\t\thandle.fn(images)\n\t\tdefault:\n\t\t\tlog.Debug(\"Skipped. Another update already running.\")\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "pkg/container/cgroup_id.go",
    "content": "package container\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\nvar dockerContainerPattern = regexp.MustCompile(`[0-9]+:.*:/docker/([a-f|0-9]{64})`)\n\n// GetRunningContainerID tries to resolve the current container ID from the current process cgroup information\nfunc GetRunningContainerID() (cid types.ContainerID, err error) {\n\tfile, err := os.ReadFile(fmt.Sprintf(\"/proc/%d/cgroup\", os.Getpid()))\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn getRunningContainerIDFromString(string(file)), nil\n}\n\nfunc getRunningContainerIDFromString(s string) types.ContainerID {\n\tmatches := dockerContainerPattern.FindStringSubmatch(s)\n\tif len(matches) < 2 {\n\t\treturn \"\"\n\t}\n\treturn types.ContainerID(matches[1])\n}\n"
  },
  {
    "path": "pkg/container/cgroup_id_test.go",
    "content": "package container\n\nimport (\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"GetRunningContainerID\", func() {\n\tWhen(\"a matching container ID is found\", func() {\n\t\tIt(\"should return that container ID\", func() {\n\t\t\tcid := getRunningContainerIDFromString(`\n15:name=systemd:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n14:misc:/\n13:rdma:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n12:pids:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n11:hugetlb:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n10:net_prio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n9:perf_event:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n8:net_cls:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n7:freezer:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n6:devices:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n5:blkio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n4:cpuacct:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n3:cpu:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n2:cpuset:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n1:memory:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n0::/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377\n\t\t\t`)\n\t\t\tExpect(cid).To(BeEquivalentTo(`991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377`))\n\t\t})\n\t})\n\tWhen(\"no matching container ID could be found\", func() {\n\t\tIt(\"should return that container ID\", func() {\n\t\t\tcid := getRunningContainerIDFromString(`14:misc:/`)\n\t\t\tExpect(cid).To(BeEmpty())\n\t\t})\n\t})\n})\n\n//\n"
  },
  {
    "path": "pkg/container/client.go",
    "content": "package container\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/docker/docker/api/types\"\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/docker/api/types/filters\"\n\t\"github.com/docker/docker/api/types/network\"\n\tsdkClient \"github.com/docker/docker/client\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"golang.org/x/net/context\"\n\n\t\"github.com/containrrr/watchtower/pkg/registry\"\n\t\"github.com/containrrr/watchtower/pkg/registry/digest\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\nconst defaultStopSignal = \"SIGTERM\"\n\n// A Client is the interface through which watchtower interacts with the\n// Docker API.\ntype Client interface {\n\tListContainers(t.Filter) ([]t.Container, error)\n\tGetContainer(containerID t.ContainerID) (t.Container, error)\n\tStopContainer(t.Container, time.Duration) error\n\tStartContainer(t.Container) (t.ContainerID, error)\n\tRenameContainer(t.Container, string) error\n\tIsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error)\n\tExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)\n\tRemoveImageByID(t.ImageID) error\n\tWarnOnHeadPullFailed(container t.Container) bool\n}\n\n// NewClient returns a new Client instance which can be used to interact with\n// the Docker API.\n// The client reads its configuration from the following environment variables:\n//   - DOCKER_HOST\t\t\tthe docker-engine host to send api requests to\n//   - DOCKER_TLS_VERIFY\t\twhether to verify tls certificates\n//   - DOCKER_API_VERSION\tthe minimum docker api version to work with\nfunc NewClient(opts ClientOptions) Client {\n\tcli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"Error instantiating Docker client: %s\", err)\n\t}\n\n\treturn dockerClient{\n\t\tapi:           cli,\n\t\tClientOptions: opts,\n\t}\n}\n\n// ClientOptions contains the options for how the docker client wrapper should behave\ntype ClientOptions struct {\n\tRemoveVolumes     bool\n\tIncludeStopped    bool\n\tReviveStopped     bool\n\tIncludeRestarting bool\n\tWarnOnHeadFailed  WarningStrategy\n}\n\n// WarningStrategy is a value determining when to show warnings\ntype WarningStrategy string\n\nconst (\n\t// WarnAlways warns whenever the problem occurs\n\tWarnAlways WarningStrategy = \"always\"\n\t// WarnNever never warns when the problem occurs\n\tWarnNever WarningStrategy = \"never\"\n\t// WarnAuto skips warning when the problem was expected\n\tWarnAuto WarningStrategy = \"auto\"\n)\n\ntype dockerClient struct {\n\tapi sdkClient.CommonAPIClient\n\tClientOptions\n}\n\nfunc (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {\n\tif client.WarnOnHeadFailed == WarnAlways {\n\t\treturn true\n\t}\n\tif client.WarnOnHeadFailed == WarnNever {\n\t\treturn false\n\t}\n\n\treturn registry.WarnOnAPIConsumption(container)\n}\n\nfunc (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {\n\tcs := []t.Container{}\n\tbg := context.Background()\n\n\tif client.IncludeStopped && client.IncludeRestarting {\n\t\tlog.Debug(\"Retrieving running, stopped, restarting and exited containers\")\n\t} else if client.IncludeStopped {\n\t\tlog.Debug(\"Retrieving running, stopped and exited containers\")\n\t} else if client.IncludeRestarting {\n\t\tlog.Debug(\"Retrieving running and restarting containers\")\n\t} else {\n\t\tlog.Debug(\"Retrieving running containers\")\n\t}\n\n\tfilter := client.createListFilter()\n\tcontainers, err := client.api.ContainerList(\n\t\tbg,\n\t\ttypes.ContainerListOptions{\n\t\t\tFilters: filter,\n\t\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, runningContainer := range containers {\n\n\t\tc, err := client.GetContainer(t.ContainerID(runningContainer.ID))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif fn(c) {\n\t\t\tcs = append(cs, c)\n\t\t}\n\t}\n\n\treturn cs, nil\n}\n\nfunc (client dockerClient) createListFilter() filters.Args {\n\tfilterArgs := filters.NewArgs()\n\tfilterArgs.Add(\"status\", \"running\")\n\n\tif client.IncludeStopped {\n\t\tfilterArgs.Add(\"status\", \"created\")\n\t\tfilterArgs.Add(\"status\", \"exited\")\n\t}\n\n\tif client.IncludeRestarting {\n\t\tfilterArgs.Add(\"status\", \"restarting\")\n\t}\n\n\treturn filterArgs\n}\n\nfunc (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {\n\tbg := context.Background()\n\n\tcontainerInfo, err := client.api.ContainerInspect(bg, string(containerID))\n\tif err != nil {\n\t\treturn &Container{}, err\n\t}\n\n\tnetType, netContainerId, found := strings.Cut(string(containerInfo.HostConfig.NetworkMode), \":\")\n\tif found && netType == \"container\" {\n\t\tparentContainer, err := client.api.ContainerInspect(bg, netContainerId)\n\t\tif err != nil {\n\t\t\tlog.WithFields(map[string]interface{}{\n\t\t\t\t\"container\":         containerInfo.Name,\n\t\t\t\t\"error\":             err,\n\t\t\t\t\"network-container\": netContainerId,\n\t\t\t}).Warnf(\"Unable to resolve network container: %v\", err)\n\n\t\t} else {\n\t\t\t// Replace the container ID with a container name to allow it to reference the re-created network container\n\t\t\tcontainerInfo.HostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf(\"container:%s\", parentContainer.Name))\n\t\t}\n\t}\n\n\timageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)\n\tif err != nil {\n\t\tlog.Warnf(\"Failed to retrieve container image info: %v\", err)\n\t\treturn &Container{containerInfo: &containerInfo, imageInfo: nil}, nil\n\t}\n\n\treturn &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil\n}\n\nfunc (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error {\n\tbg := context.Background()\n\tsignal := c.StopSignal()\n\tif signal == \"\" {\n\t\tsignal = defaultStopSignal\n\t}\n\n\tidStr := string(c.ID())\n\tshortID := c.ID().ShortID()\n\n\tif c.IsRunning() {\n\t\tlog.Infof(\"Stopping %s (%s) with %s\", c.Name(), shortID, signal)\n\t\tif err := client.api.ContainerKill(bg, idStr, signal); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// TODO: This should probably be checked.\n\t_ = client.waitForStopOrTimeout(c, timeout)\n\n\tif c.ContainerInfo().HostConfig.AutoRemove {\n\t\tlog.Debugf(\"AutoRemove container %s, skipping ContainerRemove call.\", shortID)\n\t} else {\n\t\tlog.Debugf(\"Removing container %s\", shortID)\n\n\t\tif err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {\n\t\t\tif sdkClient.IsErrNotFound(err) {\n\t\t\t\tlog.Debugf(\"Container %s not found, skipping removal.\", shortID)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Wait for container to be removed. In this case an error is a good thing\n\tif err := client.waitForStopOrTimeout(c, timeout); err == nil {\n\t\treturn fmt.Errorf(\"container %s (%s) could not be removed\", c.Name(), shortID)\n\t}\n\n\treturn nil\n}\n\nfunc (client dockerClient) GetNetworkConfig(c t.Container) *network.NetworkingConfig {\n\tconfig := &network.NetworkingConfig{\n\t\tEndpointsConfig: c.ContainerInfo().NetworkSettings.Networks,\n\t}\n\n\tfor _, ep := range config.EndpointsConfig {\n\t\taliases := make([]string, 0, len(ep.Aliases))\n\t\tcidAlias := c.ID().ShortID()\n\n\t\t// Remove the old container ID alias from the network aliases, as it would accumulate across updates otherwise\n\t\tfor _, alias := range ep.Aliases {\n\t\t\tif alias == cidAlias {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\taliases = append(aliases, alias)\n\t\t}\n\n\t\tep.Aliases = aliases\n\t}\n\treturn config\n}\n\nfunc (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) {\n\tbg := context.Background()\n\tconfig := c.GetCreateConfig()\n\thostConfig := c.GetCreateHostConfig()\n\tnetworkConfig := client.GetNetworkConfig(c)\n\n\t// simpleNetworkConfig is a networkConfig with only 1 network.\n\t// see: https://github.com/docker/docker/issues/29265\n\tsimpleNetworkConfig := func() *network.NetworkingConfig {\n\t\toneEndpoint := make(map[string]*network.EndpointSettings)\n\t\tfor k, v := range networkConfig.EndpointsConfig {\n\t\t\toneEndpoint[k] = v\n\t\t\t// we only need 1\n\t\t\tbreak\n\t\t}\n\t\treturn &network.NetworkingConfig{EndpointsConfig: oneEndpoint}\n\t}()\n\n\tname := c.Name()\n\n\tlog.Infof(\"Creating %s\", name)\n\n\tcreatedContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !(hostConfig.NetworkMode.IsHost()) {\n\n\t\tfor k := range simpleNetworkConfig.EndpointsConfig {\n\t\t\terr = client.api.NetworkDisconnect(bg, k, createdContainer.ID, true)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\n\t\tfor k, v := range networkConfig.EndpointsConfig {\n\t\t\terr = client.api.NetworkConnect(bg, k, createdContainer.ID, v)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tcreatedContainerID := t.ContainerID(createdContainer.ID)\n\tif !c.IsRunning() && !client.ReviveStopped {\n\t\treturn createdContainerID, nil\n\t}\n\n\treturn createdContainerID, client.doStartContainer(bg, c, createdContainer)\n\n}\n\nfunc (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error {\n\tname := c.Name()\n\n\tlog.Debugf(\"Starting container %s (%s)\", name, t.ContainerID(creation.ID).ShortID())\n\terr := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (client dockerClient) RenameContainer(c t.Container, newName string) error {\n\tbg := context.Background()\n\tlog.Debugf(\"Renaming container %s (%s) to %s\", c.Name(), c.ID().ShortID(), newName)\n\treturn client.api.ContainerRename(bg, string(c.ID()), newName)\n}\n\nfunc (client dockerClient) IsContainerStale(container t.Container, params t.UpdateParams) (stale bool, latestImage t.ImageID, err error) {\n\tctx := context.Background()\n\n\tif container.IsNoPull(params) {\n\t\tlog.Debugf(\"Skipping image pull.\")\n\t} else if err := client.PullImage(ctx, container); err != nil {\n\t\treturn false, container.SafeImageID(), err\n\t}\n\n\treturn client.HasNewImage(ctx, container)\n}\n\nfunc (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) {\n\tcurrentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image)\n\timageName := container.ImageName()\n\n\tnewImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)\n\tif err != nil {\n\t\treturn false, currentImageID, err\n\t}\n\n\tnewImageID := t.ImageID(newImageInfo.ID)\n\tif newImageID == currentImageID {\n\t\tlog.Debugf(\"No new images found for %s\", container.Name())\n\t\treturn false, currentImageID, nil\n\t}\n\n\tlog.Infof(\"Found new %s image (%s)\", imageName, newImageID.ShortID())\n\treturn true, newImageID, nil\n}\n\n// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed\n// to match the one that the registry reports via a HEAD request\nfunc (client dockerClient) PullImage(ctx context.Context, container t.Container) error {\n\tcontainerName := container.Name()\n\timageName := container.ImageName()\n\n\tfields := log.Fields{\n\t\t\"image\":     imageName,\n\t\t\"container\": containerName,\n\t}\n\n\tif strings.HasPrefix(imageName, \"sha256:\") {\n\t\treturn fmt.Errorf(\"container uses a pinned image, and cannot be updated by watchtower\")\n\t}\n\n\tlog.WithFields(fields).Debugf(\"Trying to load authentication credentials.\")\n\topts, err := registry.GetPullOptions(imageName)\n\tif err != nil {\n\t\tlog.Debugf(\"Error loading authentication credentials %s\", err)\n\t\treturn err\n\t}\n\tif opts.RegistryAuth != \"\" {\n\t\tlog.Debug(\"Credentials loaded\")\n\t}\n\n\tlog.WithFields(fields).Debugf(\"Checking if pull is needed\")\n\n\tif match, err := digest.CompareDigest(container, opts.RegistryAuth); err != nil {\n\t\theadLevel := log.DebugLevel\n\t\tif client.WarnOnHeadPullFailed(container) {\n\t\t\theadLevel = log.WarnLevel\n\t\t}\n\t\tlog.WithFields(fields).Logf(headLevel, \"Could not do a head request for %q, falling back to regular pull.\", imageName)\n\t\tlog.WithFields(fields).Log(headLevel, \"Reason: \", err)\n\t} else if match {\n\t\tlog.Debug(\"No pull needed. Skipping image.\")\n\t\treturn nil\n\t} else {\n\t\tlog.Debug(\"Digests did not match, doing a pull.\")\n\t}\n\n\tlog.WithFields(fields).Debugf(\"Pulling image\")\n\n\tresponse, err := client.api.ImagePull(ctx, imageName, opts)\n\tif err != nil {\n\t\tlog.Debugf(\"Error pulling image %s, %s\", imageName, err)\n\t\treturn err\n\t}\n\n\tdefer response.Close()\n\t// the pull request will be aborted prematurely unless the response is read\n\tif _, err = io.ReadAll(response); err != nil {\n\t\tlog.Error(err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (client dockerClient) RemoveImageByID(id t.ImageID) error {\n\tlog.Infof(\"Removing image %s\", id.ShortID())\n\n\titems, err := client.api.ImageRemove(\n\t\tcontext.Background(),\n\t\tstring(id),\n\t\ttypes.ImageRemoveOptions{\n\t\t\tForce: true,\n\t\t})\n\n\tif log.IsLevelEnabled(log.DebugLevel) {\n\t\tdeleted := strings.Builder{}\n\t\tuntagged := strings.Builder{}\n\t\tfor _, item := range items {\n\t\t\tif item.Deleted != \"\" {\n\t\t\t\tif deleted.Len() > 0 {\n\t\t\t\t\tdeleted.WriteString(`, `)\n\t\t\t\t}\n\t\t\t\tdeleted.WriteString(t.ImageID(item.Deleted).ShortID())\n\t\t\t}\n\t\t\tif item.Untagged != \"\" {\n\t\t\t\tif untagged.Len() > 0 {\n\t\t\t\t\tuntagged.WriteString(`, `)\n\t\t\t\t}\n\t\t\t\tuntagged.WriteString(t.ImageID(item.Untagged).ShortID())\n\t\t\t}\n\t\t}\n\t\tfields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()}\n\t\tlog.WithFields(fields).Debug(\"Image removal completed\")\n\t}\n\n\treturn err\n}\n\nfunc (client dockerClient) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) {\n\tbg := context.Background()\n\tclog := log.WithField(\"containerID\", containerID)\n\n\t// Create the exec\n\texecConfig := types.ExecConfig{\n\t\tTty:    true,\n\t\tDetach: false,\n\t\tCmd:    []string{\"sh\", \"-c\", command},\n\t}\n\n\texec, err := client.api.ContainerExecCreate(bg, string(containerID), execConfig)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tresponse, attachErr := client.api.ContainerExecAttach(bg, exec.ID, types.ExecStartCheck{\n\t\tTty:    true,\n\t\tDetach: false,\n\t})\n\tif attachErr != nil {\n\t\tclog.Errorf(\"Failed to extract command exec logs: %v\", attachErr)\n\t}\n\n\t// Run the exec\n\texecStartCheck := types.ExecStartCheck{Detach: false, Tty: true}\n\terr = client.api.ContainerExecStart(bg, exec.ID, execStartCheck)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar output string\n\tif attachErr == nil {\n\t\tdefer response.Close()\n\t\tvar writer bytes.Buffer\n\t\twritten, err := writer.ReadFrom(response.Reader)\n\t\tif err != nil {\n\t\t\tclog.Error(err)\n\t\t} else if written > 0 {\n\t\t\toutput = strings.TrimSpace(writer.String())\n\t\t}\n\t}\n\n\t// Inspect the exec to get the exit code and print a message if the\n\t// exit code is not success.\n\tskipUpdate, err := client.waitForExecOrTimeout(bg, exec.ID, output, timeout)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\treturn skipUpdate, nil\n}\n\nfunc (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, execOutput string, timeout int) (SkipUpdate bool, err error) {\n\tconst ExTempFail = 75\n\tvar ctx context.Context\n\tvar cancel context.CancelFunc\n\n\tif timeout > 0 {\n\t\tctx, cancel = context.WithTimeout(bg, time.Duration(timeout)*time.Minute)\n\t\tdefer cancel()\n\t} else {\n\t\tctx = bg\n\t}\n\n\tfor {\n\t\texecInspect, err := client.api.ContainerExecInspect(ctx, ID)\n\n\t\t//goland:noinspection GoNilness\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"exit-code\":    execInspect.ExitCode,\n\t\t\t\"exec-id\":      execInspect.ExecID,\n\t\t\t\"running\":      execInspect.Running,\n\t\t\t\"container-id\": execInspect.ContainerID,\n\t\t}).Debug(\"Awaiting timeout or completion\")\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif execInspect.Running {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tif len(execOutput) > 0 {\n\t\t\tlog.Infof(\"Command output:\\n%v\", execOutput)\n\t\t}\n\n\t\tif execInspect.ExitCode == ExTempFail {\n\t\t\treturn true, nil\n\t\t}\n\n\t\tif execInspect.ExitCode > 0 {\n\t\t\treturn false, fmt.Errorf(\"command exited with code %v  %s\", execInspect.ExitCode, execOutput)\n\t\t}\n\t\tbreak\n\t}\n\treturn false, nil\n}\n\nfunc (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error {\n\tbg := context.Background()\n\ttimeout := time.After(waitTime)\n\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\treturn nil\n\t\tdefault:\n\t\t\tif ci, err := client.api.ContainerInspect(bg, string(c.ID())); err != nil {\n\t\t\t\treturn err\n\t\t\t} else if !ci.State.Running {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(1 * time.Second)\n\t}\n}\n"
  },
  {
    "path": "pkg/container/client_test.go",
    "content": "package container\n\nimport (\n\t\"github.com/docker/docker/api/types/network\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/util\"\n\t\"github.com/containrrr/watchtower/pkg/container/mocks\"\n\t\"github.com/containrrr/watchtower/pkg/filters\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\n\t\"github.com/docker/docker/api/types\"\n\t\"github.com/docker/docker/api/types/backend\"\n\tcli \"github.com/docker/docker/client\"\n\t\"github.com/docker/docker/errdefs\"\n\t\"github.com/onsi/gomega/gbytes\"\n\t\"github.com/onsi/gomega/ghttp\"\n\t\"github.com/sirupsen/logrus\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\tgt \"github.com/onsi/gomega/types\"\n\n\t\"context\"\n\t\"net/http\"\n)\n\nvar _ = Describe(\"the client\", func() {\n\tvar docker *cli.Client\n\tvar mockServer *ghttp.Server\n\tBeforeEach(func() {\n\t\tmockServer = ghttp.NewServer()\n\t\tdocker, _ = cli.NewClientWithOpts(\n\t\t\tcli.WithHost(mockServer.URL()),\n\t\t\tcli.WithHTTPClient(mockServer.HTTPTestServer.Client()))\n\t})\n\tAfterEach(func() {\n\t\tmockServer.Close()\n\t})\n\tDescribe(\"WarnOnHeadPullFailed\", func() {\n\t\tcontainerUnknown := MockContainer(WithImageName(\"unknown.repo/prefix/imagename:latest\"))\n\t\tcontainerKnown := MockContainer(WithImageName(\"docker.io/prefix/imagename:latest\"))\n\n\t\tWhen(`warn on head failure is set to \"always\"`, func() {\n\t\t\tc := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}\n\t\t\tIt(\"should always return true\", func() {\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())\n\t\t\t})\n\t\t})\n\t\tWhen(`warn on head failure is set to \"auto\"`, func() {\n\t\t\tc := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAuto}}\n\t\t\tIt(\"should return false for unknown repos\", func() {\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())\n\t\t\t})\n\t\t\tIt(\"should return true for known repos\", func() {\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())\n\t\t\t})\n\t\t})\n\t\tWhen(`warn on head failure is set to \"never\"`, func() {\n\t\t\tc := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnNever}}\n\t\t\tIt(\"should never return true\", func() {\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())\n\t\t\t\tExpect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())\n\t\t\t})\n\t\t})\n\t})\n\tWhen(\"pulling the latest image\", func() {\n\t\tWhen(\"the image consist of a pinned hash\", func() {\n\t\t\tIt(\"should gracefully fail with a useful message\", func() {\n\t\t\t\tc := dockerClient{}\n\t\t\t\tpinnedContainer := MockContainer(WithImageName(\"sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b\"))\n\t\t\t\terr := c.PullImage(context.Background(), pinnedContainer)\n\t\t\t\tExpect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`))\n\t\t\t})\n\t\t})\n\t})\n\tWhen(\"removing a running container\", func() {\n\t\tWhen(\"the container still exist after stopping\", func() {\n\t\t\tIt(\"should attempt to remove the container\", func() {\n\t\t\t\tcontainer := MockContainer(WithContainerState(types.ContainerState{Running: true}))\n\t\t\t\tcontainerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))\n\n\t\t\t\tcid := container.ContainerInfo().ID\n\t\t\t\tmockServer.AppendHandlers(\n\t\t\t\t\tmocks.KillContainerHandler(cid, mocks.Found),\n\t\t\t\t\tmocks.GetContainerHandler(cid, containerStopped.ContainerInfo()),\n\t\t\t\t\tmocks.RemoveContainerHandler(cid, mocks.Found),\n\t\t\t\t\tmocks.GetContainerHandler(cid, nil),\n\t\t\t\t)\n\n\t\t\t\tExpect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())\n\t\t\t})\n\t\t})\n\t\tWhen(\"the container does not exist after stopping\", func() {\n\t\t\tIt(\"should not cause an error\", func() {\n\t\t\t\tcontainer := MockContainer(WithContainerState(types.ContainerState{Running: true}))\n\n\t\t\t\tcid := container.ContainerInfo().ID\n\t\t\t\tmockServer.AppendHandlers(\n\t\t\t\t\tmocks.KillContainerHandler(cid, mocks.Found),\n\t\t\t\t\tmocks.GetContainerHandler(cid, nil),\n\t\t\t\t\tmocks.RemoveContainerHandler(cid, mocks.Missing),\n\t\t\t\t)\n\n\t\t\t\tExpect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())\n\t\t\t})\n\t\t})\n\t})\n\tWhen(\"removing a image\", func() {\n\t\tWhen(\"debug logging is enabled\", func() {\n\t\t\tIt(\"should log removed and untagged images\", func() {\n\t\t\t\timageA := util.GenerateRandomSHA256()\n\t\t\t\timageAParent := util.GenerateRandomSHA256()\n\t\t\t\timages := map[string][]string{imageA: {imageAParent}}\n\t\t\t\tmockServer.AppendHandlers(mocks.RemoveImageHandler(images))\n\t\t\t\tc := dockerClient{api: docker}\n\n\t\t\t\tresetLogrus, logbuf := captureLogrus(logrus.DebugLevel)\n\t\t\t\tdefer resetLogrus()\n\n\t\t\t\tExpect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())\n\n\t\t\t\tshortA := t.ImageID(imageA).ShortID()\n\t\t\t\tshortAParent := t.ImageID(imageAParent).ShortID()\n\n\t\t\t\tEventually(logbuf).Should(gbytes.Say(`deleted=\"%v, %v\" untagged=\"?%v\"?`, shortA, shortAParent, shortA))\n\t\t\t})\n\t\t})\n\t\tWhen(\"image is not found\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\timage := util.GenerateRandomSHA256()\n\t\t\t\tmockServer.AppendHandlers(mocks.RemoveImageHandler(nil))\n\t\t\t\tc := dockerClient{api: docker}\n\n\t\t\t\terr := c.RemoveImageByID(t.ImageID(image))\n\t\t\t\tExpect(errdefs.IsNotFound(err)).To(BeTrue())\n\t\t\t})\n\t\t})\n\t})\n\tWhen(\"listing containers\", func() {\n\t\tWhen(\"no filter is provided\", func() {\n\t\t\tIt(\"should return all available containers\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filters.NoFilter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).To(HaveLen(2))\n\t\t\t})\n\t\t})\n\t\tWhen(\"a filter matching nothing\", func() {\n\t\t\tIt(\"should return an empty array\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)\n\t\t\t\tfilter := filters.FilterByNames([]string{\"lollercoaster\"}, filters.NoFilter)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).To(BeEmpty())\n\t\t\t})\n\t\t})\n\t\tWhen(\"a watchtower filter is provided\", func() {\n\t\t\tIt(\"should return only the watchtower container\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filters.WatchtowerContainersFilter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).To(ConsistOf(withContainerImageName(Equal(\"containrrr/watchtower:latest\"))))\n\t\t\t})\n\t\t})\n\t\tWhen(`include stopped is enabled`, func() {\n\t\t\tIt(\"should return both stopped and running containers\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\", \"exited\", \"created\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{IncludeStopped: true},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filters.NoFilter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).To(ContainElement(havingRunningState(false)))\n\t\t\t})\n\t\t})\n\t\tWhen(`include restarting is enabled`, func() {\n\t\t\tIt(\"should return both restarting and running containers\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\", \"restarting\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{IncludeRestarting: true},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filters.NoFilter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).To(ContainElement(havingRestartingState(true)))\n\t\t\t})\n\t\t})\n\t\tWhen(`include restarting is disabled`, func() {\n\t\t\tIt(\"should not return restarting containers\", func() {\n\t\t\t\tmockServer.AppendHandlers(mocks.ListContainersHandler(\"running\"))\n\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{IncludeRestarting: false},\n\t\t\t\t}\n\t\t\t\tcontainers, err := client.ListContainers(filters.NoFilter)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(containers).NotTo(ContainElement(havingRestartingState(true)))\n\t\t\t})\n\t\t})\n\t\tWhen(`a container uses container network mode`, func() {\n\t\t\tWhen(`the network container can be resolved`, func() {\n\t\t\t\tIt(\"should return the container name instead of the ID\", func() {\n\t\t\t\t\tconsumerContainerRef := mocks.NetConsumerOK\n\t\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)\n\t\t\t\t\tclient := dockerClient{\n\t\t\t\t\t\tapi:           docker,\n\t\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t\t}\n\t\t\t\t\tcontainer, err := client.GetContainer(consumerContainerRef.ContainerID())\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tnetworkMode := container.ContainerInfo().HostConfig.NetworkMode\n\t\t\t\t\tExpect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(`the network container cannot be resolved`, func() {\n\t\t\t\tIt(\"should still return the container ID\", func() {\n\t\t\t\t\tconsumerContainerRef := mocks.NetConsumerInvalidSupplier\n\t\t\t\t\tmockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)\n\t\t\t\t\tclient := dockerClient{\n\t\t\t\t\t\tapi:           docker,\n\t\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t\t}\n\t\t\t\t\tcontainer, err := client.GetContainer(consumerContainerRef.ContainerID())\n\t\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t\tnetworkMode := container.ContainerInfo().HostConfig.NetworkMode\n\t\t\t\t\tExpect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n\tDescribe(`ExecuteCommand`, func() {\n\t\tWhen(`logging`, func() {\n\t\t\tIt(\"should include container id field\", func() {\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{},\n\t\t\t\t}\n\n\t\t\t\t// Capture logrus output in buffer\n\t\t\t\tresetLogrus, logbuf := captureLogrus(logrus.DebugLevel)\n\t\t\t\tdefer resetLogrus()\n\n\t\t\t\tuser := \"\"\n\t\t\t\tcontainerID := t.ContainerID(\"ex-cont-id\")\n\t\t\t\texecID := \"ex-exec-id\"\n\t\t\t\tcmd := \"exec-cmd\"\n\n\t\t\t\tmockServer.AppendHandlers(\n\t\t\t\t\t// API.ContainerExecCreate\n\t\t\t\t\tghttp.CombineHandlers(\n\t\t\t\t\t\tghttp.VerifyRequest(\"POST\", HaveSuffix(\"containers/%v/exec\", containerID)),\n\t\t\t\t\t\tghttp.VerifyJSONRepresenting(types.ExecConfig{\n\t\t\t\t\t\t\tUser:   user,\n\t\t\t\t\t\t\tDetach: false,\n\t\t\t\t\t\t\tTty:    true,\n\t\t\t\t\t\t\tCmd: []string{\n\t\t\t\t\t\t\t\t\"sh\",\n\t\t\t\t\t\t\t\t\"-c\",\n\t\t\t\t\t\t\t\tcmd,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tghttp.RespondWithJSONEncoded(http.StatusOK, types.IDResponse{ID: execID}),\n\t\t\t\t\t),\n\t\t\t\t\t// API.ContainerExecStart\n\t\t\t\t\tghttp.CombineHandlers(\n\t\t\t\t\t\tghttp.VerifyRequest(\"POST\", HaveSuffix(\"exec/%v/start\", execID)),\n\t\t\t\t\t\tghttp.VerifyJSONRepresenting(types.ExecStartCheck{\n\t\t\t\t\t\t\tDetach: false,\n\t\t\t\t\t\t\tTty:    true,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tghttp.RespondWith(http.StatusOK, nil),\n\t\t\t\t\t),\n\t\t\t\t\t// API.ContainerExecInspect\n\t\t\t\t\tghttp.CombineHandlers(\n\t\t\t\t\t\tghttp.VerifyRequest(\"GET\", HaveSuffix(\"exec/ex-exec-id/json\")),\n\t\t\t\t\t\tghttp.RespondWithJSONEncoded(http.StatusOK, backend.ExecInspect{\n\t\t\t\t\t\t\tID:       execID,\n\t\t\t\t\t\t\tRunning:  false,\n\t\t\t\t\t\t\tExitCode: nil,\n\t\t\t\t\t\t\tProcessConfig: &backend.ExecProcessConfig{\n\t\t\t\t\t\t\t\tEntrypoint: \"sh\",\n\t\t\t\t\t\t\t\tArguments:  []string{\"-c\", cmd},\n\t\t\t\t\t\t\t\tUser:       user,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tContainerID: string(containerID),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t)\n\n\t\t\t\t_, err := client.ExecuteCommand(containerID, cmd, 1)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\t// Note: Since Execute requires opening up a raw TCP stream to the daemon for the output, this will fail\n\t\t\t\t// when using the mock API server. Regardless of the outcome, the log should include the container ID\n\t\t\t\tEventually(logbuf).Should(gbytes.Say(`containerID=\"?ex-cont-id\"?`))\n\t\t\t})\n\t\t})\n\t})\n\tDescribe(`GetNetworkConfig`, func() {\n\t\tWhen(`providing a container with network aliases`, func() {\n\t\t\tIt(`should omit the container ID alias`, func() {\n\t\t\t\tclient := dockerClient{\n\t\t\t\t\tapi:           docker,\n\t\t\t\t\tClientOptions: ClientOptions{IncludeRestarting: false},\n\t\t\t\t}\n\t\t\t\tcontainer := MockContainer(WithImageName(\"docker.io/prefix/imagename:latest\"))\n\n\t\t\t\taliases := []string{\"One\", \"Two\", container.ID().ShortID(), \"Four\"}\n\t\t\t\tendpoints := map[string]*network.EndpointSettings{\n\t\t\t\t\t`test`: {Aliases: aliases},\n\t\t\t\t}\n\t\t\t\tcontainer.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints}\n\t\t\t\tExpect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases))\n\t\t\t\tExpect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(Equal([]string{\"One\", \"Two\", \"Four\"}))\n\t\t\t})\n\t\t})\n\t})\n})\n\n// Capture logrus output in buffer\nfunc captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) {\n\n\tlogbuf := gbytes.NewBuffer()\n\n\torigOut := logrus.StandardLogger().Out\n\tlogrus.SetOutput(logbuf)\n\n\torigLev := logrus.StandardLogger().Level\n\tlogrus.SetLevel(level)\n\n\treturn func() {\n\t\tlogrus.SetOutput(origOut)\n\t\tlogrus.SetLevel(origLev)\n\t}, logbuf\n}\n\n// Gomega matcher helpers\n\nfunc withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {\n\treturn WithTransform(containerImageName, matcher)\n}\n\nfunc containerImageName(container t.Container) string {\n\treturn container.ImageName()\n}\n\nfunc havingRestartingState(expected bool) gt.GomegaMatcher {\n\treturn WithTransform(func(container t.Container) bool {\n\t\treturn container.ContainerInfo().State.Restarting\n\t}, Equal(expected))\n}\n\nfunc havingRunningState(expected bool) gt.GomegaMatcher {\n\treturn WithTransform(func(container t.Container) bool {\n\t\treturn container.ContainerInfo().State.Running\n\t}, Equal(expected))\n}\n"
  },
  {
    "path": "pkg/container/container.go",
    "content": "// Package container contains code related to dealing with docker containers\npackage container\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/containrrr/watchtower/internal/util\"\n\twt \"github.com/containrrr/watchtower/pkg/types\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/docker/docker/api/types\"\n\tdockercontainer \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/go-connections/nat\"\n)\n\n// NewContainer returns a new Container instance instantiated with the\n// specified ContainerInfo and ImageInfo structs.\nfunc NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInspect) *Container {\n\treturn &Container{\n\t\tcontainerInfo: containerInfo,\n\t\timageInfo:     imageInfo,\n\t}\n}\n\n// Container represents a running Docker container.\ntype Container struct {\n\tLinkedToRestarting bool\n\tStale              bool\n\n\tcontainerInfo *types.ContainerJSON\n\timageInfo     *types.ImageInspect\n}\n\n// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container\nfunc (c *Container) IsLinkedToRestarting() bool {\n\treturn c.LinkedToRestarting\n}\n\n// IsStale returns the current value of the Stale field for the container\nfunc (c *Container) IsStale() bool {\n\treturn c.Stale\n}\n\n// SetLinkedToRestarting sets the LinkedToRestarting field for the container\nfunc (c *Container) SetLinkedToRestarting(value bool) {\n\tc.LinkedToRestarting = value\n}\n\n// SetStale implements sets the Stale field for the container\nfunc (c *Container) SetStale(value bool) {\n\tc.Stale = value\n}\n\n// ContainerInfo fetches JSON info for the container\nfunc (c Container) ContainerInfo() *types.ContainerJSON {\n\treturn c.containerInfo\n}\n\n// ID returns the Docker container ID.\nfunc (c Container) ID() wt.ContainerID {\n\treturn wt.ContainerID(c.containerInfo.ID)\n}\n\n// IsRunning returns a boolean flag indicating whether or not the current\n// container is running. The status is determined by the value of the\n// container's \"State.Running\" property.\nfunc (c Container) IsRunning() bool {\n\treturn c.containerInfo.State.Running\n}\n\n// IsRestarting returns a boolean flag indicating whether or not the current\n// container is restarting. The status is determined by the value of the\n// container's \"State.Restarting\" property.\nfunc (c Container) IsRestarting() bool {\n\treturn c.containerInfo.State.Restarting\n}\n\n// Name returns the Docker container name.\nfunc (c Container) Name() string {\n\treturn c.containerInfo.Name\n}\n\n// ImageID returns the ID of the Docker image that was used to start the\n// container. May cause nil dereference if imageInfo is not set!\nfunc (c Container) ImageID() wt.ImageID {\n\treturn wt.ImageID(c.imageInfo.ID)\n}\n\n// SafeImageID returns the ID of the Docker image that was used to start the container if available,\n// otherwise returns an empty string\nfunc (c Container) SafeImageID() wt.ImageID {\n\tif c.imageInfo == nil {\n\t\treturn \"\"\n\t}\n\treturn wt.ImageID(c.imageInfo.ID)\n}\n\n// ImageName returns the name of the Docker image that was used to start the\n// container. If the original image was specified without a particular tag, the\n// \"latest\" tag is assumed.\nfunc (c Container) ImageName() string {\n\t// Compatibility w/ Zodiac deployments\n\timageName, ok := c.getLabelValue(zodiacLabel)\n\tif !ok {\n\t\timageName = c.containerInfo.Config.Image\n\t}\n\n\tif !strings.Contains(imageName, \":\") {\n\t\timageName = fmt.Sprintf(\"%s:latest\", imageName)\n\t}\n\n\treturn imageName\n}\n\n// Enabled returns the value of the container enabled label and if the label\n// was set.\nfunc (c Container) Enabled() (bool, bool) {\n\trawBool, ok := c.getLabelValue(enableLabel)\n\tif !ok {\n\t\treturn false, false\n\t}\n\n\tparsedBool, err := strconv.ParseBool(rawBool)\n\tif err != nil {\n\t\treturn false, false\n\t}\n\n\treturn parsedBool, true\n}\n\n// IsMonitorOnly returns whether the container should only be monitored based on values of\n// the monitor-only label, the monitor-only argument and the label-take-precedence argument.\nfunc (c Container) IsMonitorOnly(params wt.UpdateParams) bool {\n\treturn c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)\n}\n\n// IsNoPull returns whether the image should be pulled based on values of\n// the no-pull label, the no-pull argument and the label-take-precedence argument.\nfunc (c Container) IsNoPull(params wt.UpdateParams) bool {\n\treturn c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence)\n}\n\nfunc (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool {\n\tif contVal, err := c.getBoolLabelValue(label); err != nil {\n\t\tif !errors.Is(err, errorLabelNotFound) {\n\t\t\tlogrus.WithField(\"error\", err).WithField(\"label\", label).Warn(\"Failed to parse label value\")\n\t\t}\n\t\treturn globalVal\n\t} else {\n\t\tif contPrecedence {\n\t\t\treturn contVal\n\t\t} else {\n\t\t\treturn contVal || globalVal\n\t\t}\n\t}\n}\n\n// Scope returns the value of the scope UID label and if the label\n// was set.\nfunc (c Container) Scope() (string, bool) {\n\trawString, ok := c.getLabelValue(scope)\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\n\treturn rawString, true\n}\n\n// Links returns a list containing the names of all the containers to which\n// this container is linked.\nfunc (c Container) Links() []string {\n\tvar links []string\n\n\tdependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)\n\n\tif dependsOnLabelValue != \"\" {\n\t\tfor _, link := range strings.Split(dependsOnLabelValue, \",\") {\n\t\t\t// Since the container names need to start with '/', let's prepend it if it's missing\n\t\t\tif !strings.HasPrefix(link, \"/\") {\n\t\t\t\tlink = \"/\" + link\n\t\t\t}\n\t\t\tlinks = append(links, link)\n\t\t}\n\n\t\treturn links\n\t}\n\n\tif (c.containerInfo != nil) && (c.containerInfo.HostConfig != nil) {\n\t\tfor _, link := range c.containerInfo.HostConfig.Links {\n\t\t\tname := strings.Split(link, \":\")[0]\n\t\t\tlinks = append(links, name)\n\t\t}\n\n\t\t// If the container uses another container for networking, it can be considered an implicit link\n\t\t// since the container would stop working if the network supplier were to be recreated\n\t\tnetworkMode := c.containerInfo.HostConfig.NetworkMode\n\t\tif networkMode.IsContainer() {\n\t\t\tlinks = append(links, networkMode.ConnectedContainer())\n\t\t}\n\t}\n\n\treturn links\n}\n\n// ToRestart return whether the container should be restarted, either because\n// is stale or linked to another stale container.\nfunc (c Container) ToRestart() bool {\n\treturn c.Stale || c.LinkedToRestarting\n}\n\n// IsWatchtower returns a boolean flag indicating whether or not the current\n// container is the watchtower container itself. The watchtower container is\n// identified by the presence of the \"com.centurylinklabs.watchtower\" label in\n// the container metadata.\nfunc (c Container) IsWatchtower() bool {\n\treturn ContainsWatchtowerLabel(c.containerInfo.Config.Labels)\n}\n\n// PreUpdateTimeout checks whether a container has a specific timeout set\n// for how long the pre-update command is allowed to run. This value is expressed\n// either as an integer, in minutes, or as 0 which will allow the command/script\n// to run indefinitely. Users should be cautious with the 0 option, as that\n// could result in watchtower waiting forever.\nfunc (c Container) PreUpdateTimeout() int {\n\tvar minutes int\n\tvar err error\n\n\tval := c.getLabelValueOrEmpty(preUpdateTimeoutLabel)\n\n\tminutes, err = strconv.Atoi(val)\n\tif err != nil || val == \"\" {\n\t\treturn 1\n\t}\n\n\treturn minutes\n}\n\n// PostUpdateTimeout checks whether a container has a specific timeout set\n// for how long the post-update command is allowed to run. This value is expressed\n// either as an integer, in minutes, or as 0 which will allow the command/script\n// to run indefinitely. Users should be cautious with the 0 option, as that\n// could result in watchtower waiting forever.\nfunc (c Container) PostUpdateTimeout() int {\n\tvar minutes int\n\tvar err error\n\n\tval := c.getLabelValueOrEmpty(postUpdateTimeoutLabel)\n\n\tminutes, err = strconv.Atoi(val)\n\tif err != nil || val == \"\" {\n\t\treturn 1\n\t}\n\n\treturn minutes\n}\n\n// StopSignal returns the custom stop signal (if any) that is encoded in the\n// container's metadata. If the container has not specified a custom stop\n// signal, the empty string \"\" is returned.\nfunc (c Container) StopSignal() string {\n\treturn c.getLabelValueOrEmpty(signalLabel)\n}\n\n// GetCreateConfig returns the container's current Config converted into a format\n// that can be re-submitted to the Docker create API.\n//\n// Ideally, we'd just be able to take the ContainerConfig from the old container\n// and use it as the starting point for creating the new container; however,\n// the ContainerConfig that comes back from the Inspect call merges the default\n// configuration (the stuff specified in the metadata for the image itself)\n// with the overridden configuration (the stuff that you might specify as part\n// of the \"docker run\").\n//\n// In order to avoid unintentionally overriding the\n// defaults in the new image we need to separate the override options from the\n// default options. To do this we have to compare the ContainerConfig for the\n// running container with the ContainerConfig from the image that container was\n// started from. This function returns a ContainerConfig which contains just\n// the options overridden at runtime.\nfunc (c Container) GetCreateConfig() *dockercontainer.Config {\n\tconfig := c.containerInfo.Config\n\thostConfig := c.containerInfo.HostConfig\n\timageConfig := c.imageInfo.Config\n\n\tif config.WorkingDir == imageConfig.WorkingDir {\n\t\tconfig.WorkingDir = \"\"\n\t}\n\n\tif config.User == imageConfig.User {\n\t\tconfig.User = \"\"\n\t}\n\n\tif hostConfig.NetworkMode.IsContainer() {\n\t\tconfig.Hostname = \"\"\n\t}\n\n\tif util.SliceEqual(config.Entrypoint, imageConfig.Entrypoint) {\n\t\tconfig.Entrypoint = nil\n\t\tif util.SliceEqual(config.Cmd, imageConfig.Cmd) {\n\t\t\tconfig.Cmd = nil\n\t\t}\n\t}\n\n\t// Clear HEALTHCHECK configuration (if default)\n\tif config.Healthcheck != nil && imageConfig.Healthcheck != nil {\n\t\tif util.SliceEqual(config.Healthcheck.Test, imageConfig.Healthcheck.Test) {\n\t\t\tconfig.Healthcheck.Test = nil\n\t\t}\n\n\t\tif config.Healthcheck.Retries == imageConfig.Healthcheck.Retries {\n\t\t\tconfig.Healthcheck.Retries = 0\n\t\t}\n\n\t\tif config.Healthcheck.Interval == imageConfig.Healthcheck.Interval {\n\t\t\tconfig.Healthcheck.Interval = 0\n\t\t}\n\n\t\tif config.Healthcheck.Timeout == imageConfig.Healthcheck.Timeout {\n\t\t\tconfig.Healthcheck.Timeout = 0\n\t\t}\n\n\t\tif config.Healthcheck.StartPeriod == imageConfig.Healthcheck.StartPeriod {\n\t\t\tconfig.Healthcheck.StartPeriod = 0\n\t\t}\n\t}\n\n\tconfig.Env = util.SliceSubtract(config.Env, imageConfig.Env)\n\n\tconfig.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)\n\n\tconfig.Volumes = util.StructMapSubtract(config.Volumes, imageConfig.Volumes)\n\n\t// subtract ports exposed in image from container\n\tfor k := range config.ExposedPorts {\n\t\tif _, ok := imageConfig.ExposedPorts[k]; ok {\n\t\t\tdelete(config.ExposedPorts, k)\n\t\t}\n\t}\n\tfor p := range c.containerInfo.HostConfig.PortBindings {\n\t\tconfig.ExposedPorts[p] = struct{}{}\n\t}\n\n\tconfig.Image = c.ImageName()\n\treturn config\n}\n\n// GetCreateHostConfig returns the container's current HostConfig with any links\n// re-written so that they can be re-submitted to the Docker create API.\nfunc (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {\n\thostConfig := c.containerInfo.HostConfig\n\n\tfor i, link := range hostConfig.Links {\n\t\tname := link[0:strings.Index(link, \":\")]\n\t\talias := link[strings.LastIndex(link, \"/\"):]\n\n\t\thostConfig.Links[i] = fmt.Sprintf(\"%s:%s\", name, alias)\n\t}\n\n\treturn hostConfig\n}\n\n// HasImageInfo returns whether image information could be retrieved for the container\nfunc (c Container) HasImageInfo() bool {\n\treturn c.imageInfo != nil\n}\n\n// ImageInfo fetches the ImageInspect data of the current container\nfunc (c Container) ImageInfo() *types.ImageInspect {\n\treturn c.imageInfo\n}\n\n// VerifyConfiguration checks the container and image configurations for nil references to make sure\n// that the container can be recreated once deleted\nfunc (c Container) VerifyConfiguration() error {\n\tif c.imageInfo == nil {\n\t\treturn errorNoImageInfo\n\t}\n\n\tcontainerInfo := c.ContainerInfo()\n\tif containerInfo == nil {\n\t\treturn errorNoContainerInfo\n\t}\n\n\tcontainerConfig := containerInfo.Config\n\tif containerConfig == nil {\n\t\treturn errorInvalidConfig\n\t}\n\n\thostConfig := containerInfo.HostConfig\n\tif hostConfig == nil {\n\t\treturn errorInvalidConfig\n\t}\n\n\t// Instead of returning an error here, we just create an empty map\n\t// This should allow for updating containers where the exposed ports are missing\n\tif len(hostConfig.PortBindings) > 0 && containerConfig.ExposedPorts == nil {\n\t\tcontainerConfig.ExposedPorts = make(map[nat.Port]struct{})\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/container/container_mock_test.go",
    "content": "package container\n\nimport (\n\t\"github.com/docker/docker/api/types\"\n\tdockerContainer \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/go-connections/nat\"\n)\n\ntype MockContainerUpdate func(*types.ContainerJSON, *types.ImageInspect)\n\nfunc MockContainer(updates ...MockContainerUpdate) *Container {\n\tcontainerInfo := types.ContainerJSON{\n\t\tContainerJSONBase: &types.ContainerJSONBase{\n\t\t\tID:         \"container_id\",\n\t\t\tImage:      \"image\",\n\t\t\tName:       \"test-containrrr\",\n\t\t\tHostConfig: &dockerContainer.HostConfig{},\n\t\t},\n\t\tConfig: &dockerContainer.Config{\n\t\t\tLabels: map[string]string{},\n\t\t},\n\t}\n\timage := types.ImageInspect{\n\t\tID:     \"image_id\",\n\t\tConfig: &dockerContainer.Config{},\n\t}\n\n\tfor _, update := range updates {\n\t\tupdate(&containerInfo, &image)\n\t}\n\treturn NewContainer(&containerInfo, &image)\n}\n\nfunc WithPortBindings(portBindingSources ...string) MockContainerUpdate {\n\treturn func(c *types.ContainerJSON, i *types.ImageInspect) {\n\t\tportBindings := nat.PortMap{}\n\t\tfor _, pbs := range portBindingSources {\n\t\t\tportBindings[nat.Port(pbs)] = []nat.PortBinding{}\n\t\t}\n\t\tc.HostConfig.PortBindings = portBindings\n\t}\n}\n\nfunc WithImageName(name string) MockContainerUpdate {\n\treturn func(c *types.ContainerJSON, i *types.ImageInspect) {\n\t\tc.Config.Image = name\n\t\ti.RepoTags = append(i.RepoTags, name)\n\t}\n}\n\nfunc WithLinks(links []string) MockContainerUpdate {\n\treturn func(c *types.ContainerJSON, i *types.ImageInspect) {\n\t\tc.HostConfig.Links = links\n\t}\n}\n\nfunc WithLabels(labels map[string]string) MockContainerUpdate {\n\treturn func(c *types.ContainerJSON, i *types.ImageInspect) {\n\t\tc.Config.Labels = labels\n\t}\n}\n\nfunc WithContainerState(state types.ContainerState) MockContainerUpdate {\n\treturn func(cnt *types.ContainerJSON, img *types.ImageInspect) {\n\t\tcnt.State = &state\n\t}\n}\n\nfunc WithHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate {\n\treturn func(cnt *types.ContainerJSON, img *types.ImageInspect) {\n\t\tcnt.Config.Healthcheck = &healthConfig\n\t}\n}\n\nfunc WithImageHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate {\n\treturn func(cnt *types.ContainerJSON, img *types.ImageInspect) {\n\t\timg.Config.Healthcheck = &healthConfig\n\t}\n}\n"
  },
  {
    "path": "pkg/container/container_suite_test.go",
    "content": "package container_test\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestContainer(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"Container Suite\")\n}\n"
  },
  {
    "path": "pkg/container/container_test.go",
    "content": "package container\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tdc \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/go-connections/nat\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"the container\", func() {\n\tDescribe(\"VerifyConfiguration\", func() {\n\t\tWhen(\"verifying a container with no image info\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings())\n\t\t\t\tc.imageInfo = nil\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).To(Equal(errorNoImageInfo))\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with no container info\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings())\n\t\t\t\tc.containerInfo = nil\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).To(Equal(errorNoContainerInfo))\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with no config\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings())\n\t\t\t\tc.containerInfo.Config = nil\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).To(Equal(errorInvalidConfig))\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with no host config\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings())\n\t\t\t\tc.containerInfo.HostConfig = nil\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).To(Equal(errorInvalidConfig))\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with no port bindings\", func() {\n\t\t\tIt(\"should not return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings())\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with port bindings, but no exposed ports\", func() {\n\t\t\tIt(\"should make the config compatible with updating\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings(\"80/tcp\"))\n\t\t\t\tc.containerInfo.Config.ExposedPorts = nil\n\t\t\t\tExpect(c.VerifyConfiguration()).To(Succeed())\n\n\t\t\t\tExpect(c.containerInfo.Config.ExposedPorts).ToNot(BeNil())\n\t\t\t\tExpect(c.containerInfo.Config.ExposedPorts).To(BeEmpty())\n\t\t\t})\n\t\t})\n\t\tWhen(\"verifying a container with port bindings and exposed ports is non-nil\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tc := MockContainer(WithPortBindings(\"80/tcp\"))\n\t\t\t\tc.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{\"80/tcp\": {}}\n\t\t\t\terr := c.VerifyConfiguration()\n\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\t})\n\t\t})\n\t})\n\tDescribe(\"GetCreateConfig\", func() {\n\t\tWhen(\"container healthcheck config is equal to image config\", func() {\n\t\t\tIt(\"should return empty healthcheck values\", func() {\n\t\t\t\tc := MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest: []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t}), WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest: []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))\n\n\t\t\t\tc = MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTimeout: 30,\n\t\t\t\t}), WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTimeout: 30,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))\n\n\t\t\t\tc = MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tStartPeriod: 30,\n\t\t\t\t}), WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tStartPeriod: 30,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))\n\n\t\t\t\tc = MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tRetries: 30,\n\t\t\t\t}), WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tRetries: 30,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))\n\t\t\t})\n\t\t})\n\t\tWhen(\"container healthcheck config is different to image config\", func() {\n\t\t\tIt(\"should return the container healthcheck values\", func() {\n\t\t\t\tc := MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t\tInterval:    30,\n\t\t\t\t\tTimeout:     30,\n\t\t\t\t\tStartPeriod: 10,\n\t\t\t\t\tRetries:     2,\n\t\t\t\t}), WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"10s\"},\n\t\t\t\t\tInterval:    10,\n\t\t\t\t\tTimeout:     60,\n\t\t\t\t\tStartPeriod: 30,\n\t\t\t\t\tRetries:     10,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t\tInterval:    30,\n\t\t\t\t\tTimeout:     30,\n\t\t\t\t\tStartPeriod: 10,\n\t\t\t\t\tRetries:     2,\n\t\t\t\t}))\n\t\t\t})\n\t\t})\n\t\tWhen(\"container healthcheck config is empty\", func() {\n\t\t\tIt(\"should not panic\", func() {\n\t\t\t\tc := MockContainer(WithImageHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"10s\"},\n\t\t\t\t\tInterval:    10,\n\t\t\t\t\tTimeout:     60,\n\t\t\t\t\tStartPeriod: 30,\n\t\t\t\t\tRetries:     10,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(BeNil())\n\t\t\t})\n\t\t})\n\t\tWhen(\"container image healthcheck config is empty\", func() {\n\t\t\tIt(\"should not panic\", func() {\n\t\t\t\tc := MockContainer(WithHealthcheck(dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t\tInterval:    30,\n\t\t\t\t\tTimeout:     30,\n\t\t\t\t\tStartPeriod: 10,\n\t\t\t\t\tRetries:     2,\n\t\t\t\t}))\n\t\t\t\tExpect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{\n\t\t\t\t\tTest:        []string{\"/usr/bin/sleep\", \"1s\"},\n\t\t\t\t\tInterval:    30,\n\t\t\t\t\tTimeout:     30,\n\t\t\t\t\tStartPeriod: 10,\n\t\t\t\t\tRetries:     2,\n\t\t\t\t}))\n\t\t\t})\n\t\t})\n\t})\n\tWhen(\"asked for metadata\", func() {\n\t\tvar c *Container\n\t\tBeforeEach(func() {\n\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\"com.centurylinklabs.watchtower.enable\": \"true\",\n\t\t\t\t\"com.centurylinklabs.watchtower\":        \"true\",\n\t\t\t}))\n\t\t})\n\t\tIt(\"should return its name on calls to .Name()\", func() {\n\t\t\tname := c.Name()\n\t\t\tExpect(name).To(Equal(\"test-containrrr\"))\n\t\t\tExpect(name).NotTo(Equal(\"wrong-name\"))\n\t\t})\n\t\tIt(\"should return its ID on calls to .ID()\", func() {\n\t\t\tid := c.ID()\n\n\t\t\tExpect(id).To(BeEquivalentTo(\"container_id\"))\n\t\t\tExpect(id).NotTo(BeEquivalentTo(\"wrong-id\"))\n\t\t})\n\t\tIt(\"should return true, true if enabled on calls to .Enabled()\", func() {\n\t\t\tenabled, exists := c.Enabled()\n\n\t\t\tExpect(enabled).To(BeTrue())\n\t\t\tExpect(exists).To(BeTrue())\n\t\t})\n\t\tIt(\"should return false, true if present but not true on calls to .Enabled()\", func() {\n\t\t\tc = MockContainer(WithLabels(map[string]string{\"com.centurylinklabs.watchtower.enable\": \"false\"}))\n\t\t\tenabled, exists := c.Enabled()\n\n\t\t\tExpect(enabled).To(BeFalse())\n\t\t\tExpect(exists).To(BeTrue())\n\t\t})\n\t\tIt(\"should return false, false if not present on calls to .Enabled()\", func() {\n\t\t\tc = MockContainer(WithLabels(map[string]string{\"lol\": \"false\"}))\n\t\t\tenabled, exists := c.Enabled()\n\n\t\t\tExpect(enabled).To(BeFalse())\n\t\t\tExpect(exists).To(BeFalse())\n\t\t})\n\t\tIt(\"should return false, false if present but not parsable .Enabled()\", func() {\n\t\t\tc = MockContainer(WithLabels(map[string]string{\"com.centurylinklabs.watchtower.enable\": \"falsy\"}))\n\t\t\tenabled, exists := c.Enabled()\n\n\t\t\tExpect(enabled).To(BeFalse())\n\t\t\tExpect(exists).To(BeFalse())\n\t\t})\n\t\tWhen(\"checking if its a watchtower instance\", func() {\n\t\t\tIt(\"should return true if the label is set to true\", func() {\n\t\t\t\tisWatchtower := c.IsWatchtower()\n\t\t\t\tExpect(isWatchtower).To(BeTrue())\n\t\t\t})\n\t\t\tIt(\"should return false if the label is present but set to false\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{\"com.centurylinklabs.watchtower\": \"false\"}))\n\t\t\t\tisWatchtower := c.IsWatchtower()\n\t\t\t\tExpect(isWatchtower).To(BeFalse())\n\t\t\t})\n\t\t\tIt(\"should return false if the label is not present\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{\"funny.label\": \"false\"}))\n\t\t\t\tisWatchtower := c.IsWatchtower()\n\t\t\t\tExpect(isWatchtower).To(BeFalse())\n\t\t\t})\n\t\t\tIt(\"should return false if there are no labels\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{}))\n\t\t\t\tisWatchtower := c.IsWatchtower()\n\t\t\t\tExpect(isWatchtower).To(BeFalse())\n\t\t\t})\n\t\t})\n\t\tWhen(\"fetching the custom stop signal\", func() {\n\t\t\tIt(\"should return the signal if its set\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\"com.centurylinklabs.watchtower.stop-signal\": \"SIGKILL\",\n\t\t\t\t}))\n\t\t\t\tstopSignal := c.StopSignal()\n\t\t\t\tExpect(stopSignal).To(Equal(\"SIGKILL\"))\n\t\t\t})\n\t\t\tIt(\"should return an empty string if its not set\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{}))\n\t\t\t\tstopSignal := c.StopSignal()\n\t\t\t\tExpect(stopSignal).To(Equal(\"\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"fetching the image name\", func() {\n\t\t\tWhen(\"the zodiac label is present\", func() {\n\t\t\t\tIt(\"should fetch the image name from it\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.zodiac.original-image\": \"the-original-image\",\n\t\t\t\t\t}))\n\t\t\t\t\timageName := c.ImageName()\n\t\t\t\t\tExpect(imageName).To(Equal(imageName))\n\t\t\t\t})\n\t\t\t})\n\t\t\tIt(\"should return the image name\", func() {\n\t\t\t\tname := \"image-name:3\"\n\t\t\t\tc = MockContainer(WithImageName(name))\n\t\t\t\timageName := c.ImageName()\n\t\t\t\tExpect(imageName).To(Equal(name))\n\t\t\t})\n\t\t\tIt(\"should assume latest if no tag is supplied\", func() {\n\t\t\t\tname := \"image-name\"\n\t\t\t\tc = MockContainer(WithImageName(name))\n\t\t\t\timageName := c.ImageName()\n\t\t\t\tExpect(imageName).To(Equal(name + \":latest\"))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"fetching container links\", func() {\n\t\t\tWhen(\"the depends on label is present\", func() {\n\t\t\t\tIt(\"should fetch depending containers from it\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.depends-on\": \"postgres\",\n\t\t\t\t\t}))\n\t\t\t\t\tlinks := c.Links()\n\t\t\t\t\tExpect(links).To(SatisfyAll(ContainElement(\"/postgres\"), HaveLen(1)))\n\t\t\t\t})\n\t\t\t\tIt(\"should fetch depending containers if there are many\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.depends-on\": \"postgres,redis\",\n\t\t\t\t\t}))\n\t\t\t\t\tlinks := c.Links()\n\t\t\t\t\tExpect(links).To(SatisfyAll(ContainElement(\"/postgres\"), ContainElement(\"/redis\"), HaveLen(2)))\n\t\t\t\t})\n\t\t\t\tIt(\"should only add slashes to names when they are missing\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.depends-on\": \"/postgres,redis\",\n\t\t\t\t\t}))\n\t\t\t\t\tlinks := c.Links()\n\t\t\t\t\tExpect(links).To(SatisfyAll(ContainElement(\"/postgres\"), ContainElement(\"/redis\")))\n\t\t\t\t})\n\t\t\t\tIt(\"should fetch depending containers if label is blank\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.depends-on\": \"\",\n\t\t\t\t\t}))\n\t\t\t\t\tlinks := c.Links()\n\t\t\t\t\tExpect(links).To(HaveLen(0))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"the depends on label is not present\", func() {\n\t\t\t\tIt(\"should fetch depending containers from host config links\", func() {\n\t\t\t\t\tc = MockContainer(WithLinks([]string{\n\t\t\t\t\t\t\"redis:test-containrrr\",\n\t\t\t\t\t\t\"postgres:test-containrrr\",\n\t\t\t\t\t}))\n\t\t\t\t\tlinks := c.Links()\n\t\t\t\t\tExpect(links).To(SatisfyAll(ContainElement(\"redis\"), ContainElement(\"postgres\"), HaveLen(2)))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"checking no-pull label\", func() {\n\t\t\tWhen(\"no-pull argument is not set\", func() {\n\t\t\t\tWhen(\"no-pull label is true\", func() {\n\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"true\",\n\t\t\t\t\t}))\n\t\t\t\t\tIt(\"should return true\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{})).To(Equal(true))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tWhen(\"no-pull label is false\", func() {\n\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"false\",\n\t\t\t\t\t}))\n\t\t\t\t\tIt(\"should return false\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tWhen(\"no-pull label is set to an invalid value\", func() {\n\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"maybe\",\n\t\t\t\t\t}))\n\t\t\t\t\tIt(\"should return false\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tWhen(\"no-pull label is unset\", func() {\n\t\t\t\t\tc = MockContainer(WithLabels(map[string]string{}))\n\t\t\t\t\tIt(\"should return false\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"no-pull argument is set to true\", func() {\n\t\t\t\tWhen(\"no-pull label is true\", func() {\n\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"true\",\n\t\t\t\t\t}))\n\t\t\t\t\tIt(\"should return true\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tWhen(\"no-pull label is false\", func() {\n\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"false\",\n\t\t\t\t\t}))\n\t\t\t\t\tIt(\"should return true\", func() {\n\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t\tWhen(\"label-take-precedence argument is set to true\", func() {\n\t\t\t\t\tWhen(\"no-pull label is true\", func() {\n\t\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"true\",\n\t\t\t\t\t\t}))\n\t\t\t\t\t\tIt(\"should return true\", func() {\n\t\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(true))\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t\tWhen(\"no-pull label is false\", func() {\n\t\t\t\t\t\tc := MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\t\t\"com.centurylinklabs.watchtower.no-pull\": \"false\",\n\t\t\t\t\t\t}))\n\t\t\t\t\t\tIt(\"should return false\", func() {\n\t\t\t\t\t\t\tExpect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(false))\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"there is a pre or post update timeout\", func() {\n\t\t\tIt(\"should return minute values\", func() {\n\t\t\t\tc = MockContainer(WithLabels(map[string]string{\n\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\":  \"3\",\n\t\t\t\t\t\"com.centurylinklabs.watchtower.lifecycle.post-update-timeout\": \"5\",\n\t\t\t\t}))\n\t\t\t\tpreTimeout := c.PreUpdateTimeout()\n\t\t\t\tExpect(preTimeout).To(Equal(3))\n\t\t\t\tpostTimeout := c.PostUpdateTimeout()\n\t\t\t\tExpect(postTimeout).To(Equal(5))\n\t\t\t})\n\t\t})\n\n\t})\n})\n"
  },
  {
    "path": "pkg/container/errors.go",
    "content": "package container\n\nimport \"errors\"\n\nvar errorNoImageInfo = errors.New(\"no available image info\")\nvar errorNoContainerInfo = errors.New(\"no available container info\")\nvar errorInvalidConfig = errors.New(\"container configuration missing or invalid\")\nvar errorLabelNotFound = errors.New(\"label was not found in container\")\n"
  },
  {
    "path": "pkg/container/metadata.go",
    "content": "package container\n\nimport \"strconv\"\n\nconst (\n\twatchtowerLabel        = \"com.centurylinklabs.watchtower\"\n\tsignalLabel            = \"com.centurylinklabs.watchtower.stop-signal\"\n\tenableLabel            = \"com.centurylinklabs.watchtower.enable\"\n\tmonitorOnlyLabel       = \"com.centurylinklabs.watchtower.monitor-only\"\n\tnoPullLabel            = \"com.centurylinklabs.watchtower.no-pull\"\n\tdependsOnLabel         = \"com.centurylinklabs.watchtower.depends-on\"\n\tzodiacLabel            = \"com.centurylinklabs.zodiac.original-image\"\n\tscope                  = \"com.centurylinklabs.watchtower.scope\"\n\tpreCheckLabel          = \"com.centurylinklabs.watchtower.lifecycle.pre-check\"\n\tpostCheckLabel         = \"com.centurylinklabs.watchtower.lifecycle.post-check\"\n\tpreUpdateLabel         = \"com.centurylinklabs.watchtower.lifecycle.pre-update\"\n\tpostUpdateLabel        = \"com.centurylinklabs.watchtower.lifecycle.post-update\"\n\tpreUpdateTimeoutLabel  = \"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout\"\n\tpostUpdateTimeoutLabel = \"com.centurylinklabs.watchtower.lifecycle.post-update-timeout\"\n)\n\n// GetLifecyclePreCheckCommand returns the pre-check command set in the container metadata or an empty string\nfunc (c Container) GetLifecyclePreCheckCommand() string {\n\treturn c.getLabelValueOrEmpty(preCheckLabel)\n}\n\n// GetLifecyclePostCheckCommand returns the post-check command set in the container metadata or an empty string\nfunc (c Container) GetLifecyclePostCheckCommand() string {\n\treturn c.getLabelValueOrEmpty(postCheckLabel)\n}\n\n// GetLifecyclePreUpdateCommand returns the pre-update command set in the container metadata or an empty string\nfunc (c Container) GetLifecyclePreUpdateCommand() string {\n\treturn c.getLabelValueOrEmpty(preUpdateLabel)\n}\n\n// GetLifecyclePostUpdateCommand returns the post-update command set in the container metadata or an empty string\nfunc (c Container) GetLifecyclePostUpdateCommand() string {\n\treturn c.getLabelValueOrEmpty(postUpdateLabel)\n}\n\n// ContainsWatchtowerLabel takes a map of labels and values and tells\n// the consumer whether it contains a valid watchtower instance label\nfunc ContainsWatchtowerLabel(labels map[string]string) bool {\n\tval, ok := labels[watchtowerLabel]\n\treturn ok && val == \"true\"\n}\n\nfunc (c Container) getLabelValueOrEmpty(label string) string {\n\tif val, ok := c.containerInfo.Config.Labels[label]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n\nfunc (c Container) getLabelValue(label string) (string, bool) {\n\tval, ok := c.containerInfo.Config.Labels[label]\n\treturn val, ok\n}\n\nfunc (c Container) getBoolLabelValue(label string) (bool, error) {\n\tif strVal, ok := c.containerInfo.Config.Labels[label]; ok {\n\t\tvalue, err := strconv.ParseBool(strVal)\n\t\treturn value, err\n\t}\n\treturn false, errorLabelNotFound\n}\n"
  },
  {
    "path": "pkg/container/mocks/ApiServer.go",
    "content": "package mocks\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/onsi/ginkgo\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\n\t\"github.com/docker/docker/api/types\"\n\t\"github.com/docker/docker/api/types/filters\"\n\tO \"github.com/onsi/gomega\"\n\t\"github.com/onsi/gomega/ghttp\"\n)\n\nfunc getMockJSONFile(relPath string) ([]byte, error) {\n\tabsPath, _ := filepath.Abs(relPath)\n\tbuf, err := os.ReadFile(absPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"mock JSON file %q not found: %e\", absPath, err)\n\t}\n\treturn buf, nil\n}\n\n// RespondWithJSONFile handles a request by returning the contents of the supplied file\nfunc RespondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.Header) http.HandlerFunc {\n\thandler, err := respondWithJSONFile(relPath, statusCode, optionalHeader...)\n\tO.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())\n\treturn handler\n}\n\nfunc respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.Header) (http.HandlerFunc, error) {\n\tbuf, err := getMockJSONFile(relPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ghttp.RespondWith(statusCode, buf, optionalHeader...), nil\n}\n\n// GetContainerHandlers returns the handlers serving lookups for the supplied container mock files\nfunc GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc {\n\thandlers := make([]http.HandlerFunc, 0, len(containerRefs)*3)\n\tfor _, containerRef := range containerRefs {\n\t\thandlers = append(handlers, getContainerFileHandler(containerRef))\n\n\t\t// Also append any containers that the container references, if any\n\t\tfor _, ref := range containerRef.references {\n\t\t\thandlers = append(handlers, getContainerFileHandler(ref))\n\t\t}\n\n\t\t// Also append the image request since that will be called for every container\n\t\thandlers = append(handlers, getImageHandler(containerRef.image.id,\n\t\t\tRespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK),\n\t\t))\n\t}\n\n\treturn handlers\n}\n\nfunc createFilterArgs(statuses []string) filters.Args {\n\targs := filters.NewArgs()\n\tfor _, status := range statuses {\n\t\targs.Add(\"status\", status)\n\t}\n\treturn args\n}\n\nvar defaultImage = imageRef{\n\t// watchtower\n\tid:   t.ImageID(\"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\"),\n\tfile: \"default\",\n}\n\nvar Watchtower = ContainerRef{\n\tname:  \"watchtower\",\n\tid:    \"3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134\",\n\timage: &defaultImage,\n}\nvar Stopped = ContainerRef{\n\tname:  \"stopped\",\n\tid:    \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65\",\n\timage: &defaultImage,\n}\nvar Running = ContainerRef{\n\tname: \"running\",\n\tid:   \"b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008\",\n\timage: &imageRef{\n\t\t// portainer\n\t\tid:   t.ImageID(\"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd\"),\n\t\tfile: \"running\",\n\t},\n}\nvar Restarting = ContainerRef{\n\tname:  \"restarting\",\n\tid:    \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67\",\n\timage: &defaultImage,\n}\n\nvar netSupplierOK = ContainerRef{\n\tid:   \"25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2\",\n\tname: \"net_supplier\",\n\timage: &imageRef{\n\t\t// gluetun\n\t\tid:   t.ImageID(\"sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51\"),\n\t\tfile: \"net_producer\",\n\t},\n}\nvar netSupplierNotFound = ContainerRef{\n\tid:        NetSupplierNotFoundID,\n\tname:      netSupplierOK.name,\n\tisMissing: true,\n}\n\n// NetConsumerOK is used for testing `container` networking mode\n// returns a container that consumes an existing supplier container\nvar NetConsumerOK = ContainerRef{\n\tid:   \"1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6\",\n\tname: \"net_consumer\",\n\timage: &imageRef{\n\t\tid:   t.ImageID(\"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\"), // nginx\n\t\tfile: \"net_consumer\",\n\t},\n\treferences: []*ContainerRef{&netSupplierOK},\n}\n\n// NetConsumerInvalidSupplier is used for testing `container` networking mode\n// returns a container that references a supplying container that does not exist\nvar NetConsumerInvalidSupplier = ContainerRef{\n\tid:         NetConsumerOK.id,\n\tname:       \"net_consumer-missing_supplier\",\n\timage:      NetConsumerOK.image,\n\treferences: []*ContainerRef{&netSupplierNotFound},\n}\n\nconst NetSupplierNotFoundID = \"badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc\"\nconst NetSupplierContainerName = \"/wt-contnet-producer-1\"\n\nfunc getContainerFileHandler(cr *ContainerRef) http.HandlerFunc {\n\n\tif cr.isMissing {\n\t\treturn containerNotFoundResponse(string(cr.id))\n\t}\n\n\tcontainerFile, err := cr.getContainerFile()\n\tif err != nil {\n\t\tginkgo.Fail(fmt.Sprintf(\"Failed to get container mock file: %v\", err))\n\t}\n\n\treturn getContainerHandler(\n\t\tstring(cr.id),\n\t\tRespondWithJSONFile(containerFile, http.StatusOK),\n\t)\n}\n\nfunc getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"GET\", O.HaveSuffix(\"/containers/%v/json\", containerId)),\n\t\tresponseHandler,\n\t)\n}\n\n// GetContainerHandler mocks the GET containers/{id}/json endpoint\nfunc GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {\n\tresponseHandler := containerNotFoundResponse(containerID)\n\tif containerInfo != nil {\n\t\tresponseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)\n\t}\n\treturn getContainerHandler(containerID, responseHandler)\n}\n\n// GetImageHandler mocks the GET images/{id}/json endpoint\nfunc GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {\n\treturn getImageHandler(t.ImageID(imageInfo.ID), ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))\n}\n\n// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses\nfunc ListContainersHandler(statuses ...string) http.HandlerFunc {\n\tfilterArgs := createFilterArgs(statuses)\n\tbytes, err := filterArgs.MarshalJSON()\n\tO.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())\n\tquery := url.Values{\n\t\t\"filters\": []string{string(bytes)},\n\t}\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"GET\", O.HaveSuffix(\"containers/json\"), query.Encode()),\n\t\trespondWithFilteredContainers(filterArgs),\n\t)\n}\n\nfunc respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {\n\tcontainersJSON, err := getMockJSONFile(\"./mocks/data/containers.json\")\n\tO.ExpectWithOffset(2, err).ShouldNot(O.HaveOccurred())\n\tvar filteredContainers []types.Container\n\tvar containers []types.Container\n\tO.ExpectWithOffset(2, json.Unmarshal(containersJSON, &containers)).To(O.Succeed())\n\tfor _, v := range containers {\n\t\tfor _, key := range filters.Get(\"status\") {\n\t\t\tif v.State == key {\n\t\t\t\tfilteredContainers = append(filteredContainers, v)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)\n}\n\nfunc getImageHandler(imageId t.ImageID, responseHandler http.HandlerFunc) http.HandlerFunc {\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"GET\", O.HaveSuffix(\"/images/%s/json\", imageId)),\n\t\tresponseHandler,\n\t)\n}\n\n// KillContainerHandler mocks the POST containers/{id}/kill endpoint\nfunc KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {\n\tresponseHandler := noContentStatusResponse\n\tif !found {\n\t\tresponseHandler = containerNotFoundResponse(containerID)\n\t}\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"POST\", O.HaveSuffix(\"containers/%s/kill\", containerID)),\n\t\tresponseHandler,\n\t)\n}\n\n// RemoveContainerHandler mocks the DELETE containers/{id} endpoint\nfunc RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {\n\tresponseHandler := noContentStatusResponse\n\tif !found {\n\t\tresponseHandler = containerNotFoundResponse(containerID)\n\t}\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"DELETE\", O.HaveSuffix(\"containers/%s\", containerID)),\n\t\tresponseHandler,\n\t)\n}\n\nfunc containerNotFoundResponse(containerID string) http.HandlerFunc {\n\treturn ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: \"No such container: \" + string(containerID)})\n}\n\nvar noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)\n\ntype FoundStatus bool\n\nconst (\n\tFound   FoundStatus = true\n\tMissing FoundStatus = false\n)\n\n// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents\nfunc RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {\n\treturn ghttp.CombineHandlers(\n\t\tghttp.VerifyRequest(\"DELETE\", O.MatchRegexp(\"/images/.*\")),\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tparts := strings.Split(r.URL.Path, `/`)\n\t\t\timage := parts[len(parts)-1]\n\n\t\t\tif parents, found := imagesWithParents[image]; found {\n\t\t\t\titems := []types.ImageDeleteResponseItem{\n\t\t\t\t\t{Untagged: image},\n\t\t\t\t\t{Deleted: image},\n\t\t\t\t}\n\t\t\t\tfor _, parent := range parents {\n\t\t\t\t\titems = append(items, types.ImageDeleteResponseItem{Deleted: parent})\n\t\t\t\t}\n\t\t\t\tghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)\n\t\t\t} else {\n\t\t\t\tghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{\n\t\t\t\t\tmessage: \"Something went wrong.\",\n\t\t\t\t})(w, r)\n\t\t\t}\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "pkg/container/mocks/FilterableContainer.go",
    "content": "package mocks\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// FilterableContainer is an autogenerated mock type for the FilterableContainer type\ntype FilterableContainer struct {\n\tmock.Mock\n}\n\n// Enabled provides a mock function with given fields:\nfunc (_m *FilterableContainer) Enabled() (bool, bool) {\n\tret := _m.Called()\n\n\tvar r0 bool\n\tif rf, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\tvar r1 bool\n\tif rf, ok := ret.Get(1).(func() bool); ok {\n\t\tr1 = rf()\n\t} else {\n\t\tr1 = ret.Get(1).(bool)\n\t}\n\n\treturn r0, r1\n}\n\n// IsWatchtower provides a mock function with given fields:\nfunc (_m *FilterableContainer) IsWatchtower() bool {\n\tret := _m.Called()\n\n\tvar r0 bool\n\tif rf, ok := ret.Get(0).(func() bool); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\treturn r0\n}\n\n// Name provides a mock function with given fields:\nfunc (_m *FilterableContainer) Name() string {\n\tret := _m.Called()\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\treturn r0\n}\n\n// Scope provides a mock function with given fields:\nfunc (_m *FilterableContainer) Scope() (string, bool) {\n\tret := _m.Called()\n\n\tvar r0 string\n\n\tif rf, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 bool\n\n\tif rf, ok := ret.Get(1).(func() bool); ok {\n\t\tr1 = rf()\n\t} else {\n\t\tr1 = ret.Get(1).(bool)\n\t}\n\n\treturn r0, r1\n}\n\n// ImageName provides a mock function with given fields:\nfunc (_m *FilterableContainer) ImageName() string {\n\tret := _m.Called()\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\treturn r0\n}\n"
  },
  {
    "path": "pkg/container/mocks/container_ref.go",
    "content": "package mocks\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\ntype imageRef struct {\n\tid   t.ImageID\n\tfile string\n}\n\nfunc (ir *imageRef) getFileName() string {\n\treturn fmt.Sprintf(\"./mocks/data/image_%v.json\", ir.file)\n}\n\ntype ContainerRef struct {\n\tname       string\n\tid         t.ContainerID\n\timage      *imageRef\n\tfile       string\n\treferences []*ContainerRef\n\tisMissing  bool\n}\n\nfunc (cr *ContainerRef) getContainerFile() (containerFile string, err error) {\n\tfile := cr.file\n\tif file == \"\" {\n\t\tfile = cr.name\n\t}\n\n\tcontainerFile = fmt.Sprintf(\"./mocks/data/container_%v.json\", file)\n\t_, err = os.Stat(containerFile)\n\n\treturn containerFile, err\n}\n\nfunc (cr *ContainerRef) ContainerID() t.ContainerID {\n\treturn cr.id\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_net_consumer-missing_supplier.json",
    "content": "{\n  \"Id\": \"1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6\",\n  \"Created\": \"2023-07-25T14:55:14.69155887Z\",\n  \"Path\": \"/docker-entrypoint.sh\",\n  \"Args\": [\n    \"nginx\",\n    \"-g\",\n    \"daemon off;\"\n  ],\n  \"State\": {\n    \"Status\": \"running\",\n    \"Running\": true,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 3743,\n    \"ExitCode\": 0,\n    \"Error\": \"\",\n    \"StartedAt\": \"2023-07-25T14:55:15.299654437Z\",\n    \"FinishedAt\": \"0001-01-01T00:00:00Z\"\n  },\n  \"Image\": \"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log\",\n  \"Name\": \"/wt-contnet-consumer-1\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": null,\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"container:badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"CgroupnsMode\": \"host\",\n    \"Dns\": null,\n    \"DnsOptions\": null,\n    \"DnsSearch\": null,\n    \"ExtraHosts\": [],\n    \"GroupAdd\": null,\n    \"IpcMode\": \"private\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": null,\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": null,\n    \"DeviceCgroupRules\": null,\n    \"DeviceRequests\": null,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": null,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [],\n  \"Config\": {\n    \"Hostname\": \"25e75393800b\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"ExposedPorts\": {\n      \"80/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"NGINX_VERSION=1.23.3\",\n      \"NJS_VERSION=0.7.9\",\n      \"PKG_RELEASE=1~bullseye\"\n    ],\n    \"Cmd\": [\n      \"nginx\",\n      \"-g\",\n      \"daemon off;\"\n    ],\n    \"Image\": \"nginx\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/docker-entrypoint.sh\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.docker.compose.config-hash\": \"8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa\",\n      \"com.docker.compose.container-number\": \"1\",\n      \"com.docker.compose.depends_on\": \"producer:service_started:false\",\n      \"com.docker.compose.image\": \"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\",\n      \"com.docker.compose.oneoff\": \"False\",\n      \"com.docker.compose.project\": \"wt-contnet\",\n      \"com.docker.compose.project.config_files\": \"/tmp/wt-contnet/docker-compose.yaml\",\n      \"com.docker.compose.project.working_dir\": \"/tmp/wt-contnet\",\n      \"com.docker.compose.replace\": \"07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668\",\n      \"com.docker.compose.service\": \"consumer\",\n      \"com.docker.compose.version\": \"2.19.1\",\n      \"desktop.docker.io/wsl-distro\": \"Ubuntu\",\n      \"maintainer\": \"NGINX Docker Maintainers \\u003cdocker-maint@nginx.com\\u003e\"\n    },\n    \"StopSignal\": \"SIGQUIT\"\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {},\n    \"SandboxKey\": \"\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {}\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_net_consumer.json",
    "content": "{\n  \"Id\": \"1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6\",\n  \"Created\": \"2023-07-25T14:55:14.69155887Z\",\n  \"Path\": \"/docker-entrypoint.sh\",\n  \"Args\": [\n    \"nginx\",\n    \"-g\",\n    \"daemon off;\"\n  ],\n  \"State\": {\n    \"Status\": \"running\",\n    \"Running\": true,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 3743,\n    \"ExitCode\": 0,\n    \"Error\": \"\",\n    \"StartedAt\": \"2023-07-25T14:55:15.299654437Z\",\n    \"FinishedAt\": \"0001-01-01T00:00:00Z\"\n  },\n  \"Image\": \"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log\",\n  \"Name\": \"/wt-contnet-consumer-1\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": null,\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"container:25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"CgroupnsMode\": \"host\",\n    \"Dns\": null,\n    \"DnsOptions\": null,\n    \"DnsSearch\": null,\n    \"ExtraHosts\": [],\n    \"GroupAdd\": null,\n    \"IpcMode\": \"private\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": null,\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": null,\n    \"DeviceCgroupRules\": null,\n    \"DeviceRequests\": null,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": null,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [],\n  \"Config\": {\n    \"Hostname\": \"25e75393800b\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"ExposedPorts\": {\n      \"80/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"NGINX_VERSION=1.23.3\",\n      \"NJS_VERSION=0.7.9\",\n      \"PKG_RELEASE=1~bullseye\"\n    ],\n    \"Cmd\": [\n      \"nginx\",\n      \"-g\",\n      \"daemon off;\"\n    ],\n    \"Image\": \"nginx\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/docker-entrypoint.sh\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.docker.compose.config-hash\": \"8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa\",\n      \"com.docker.compose.container-number\": \"1\",\n      \"com.docker.compose.depends_on\": \"producer:service_started:false\",\n      \"com.docker.compose.image\": \"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\",\n      \"com.docker.compose.oneoff\": \"False\",\n      \"com.docker.compose.project\": \"wt-contnet\",\n      \"com.docker.compose.project.config_files\": \"/tmp/wt-contnet/docker-compose.yaml\",\n      \"com.docker.compose.project.working_dir\": \"/tmp/wt-contnet\",\n      \"com.docker.compose.replace\": \"07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668\",\n      \"com.docker.compose.service\": \"consumer\",\n      \"com.docker.compose.version\": \"2.19.1\",\n      \"desktop.docker.io/wsl-distro\": \"Ubuntu\",\n      \"maintainer\": \"NGINX Docker Maintainers \\u003cdocker-maint@nginx.com\\u003e\"\n    },\n    \"StopSignal\": \"SIGQUIT\"\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {},\n    \"SandboxKey\": \"\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {}\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_net_supplier.json",
    "content": "{\n  \"Id\": \"25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2\",\n  \"Created\": \"2023-07-25T14:55:14.595662628Z\",\n  \"Path\": \"/gluetun-entrypoint\",\n  \"Args\": [],\n  \"State\": {\n    \"Status\": \"running\",\n    \"Running\": true,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 3648,\n    \"ExitCode\": 0,\n    \"Error\": \"\",\n    \"StartedAt\": \"2023-07-25T14:55:15.193430103Z\",\n    \"FinishedAt\": \"0001-01-01T00:00:00Z\",\n    \"Health\": {\n      \"Status\": \"healthy\",\n      \"FailingStreak\": 0,\n      \"Log\": [\n        {\n          \"Start\": \"2023-07-25T15:00:32.078491228Z\",\n          \"End\": \"2023-07-25T15:00:32.194554876Z\",\n          \"ExitCode\": 0,\n          \"Output\": \"\"\n        },\n        {\n          \"Start\": \"2023-07-25T15:00:37.199245496Z\",\n          \"End\": \"2023-07-25T15:00:37.294845687Z\",\n          \"ExitCode\": 0,\n          \"Output\": \"\"\n        },\n        {\n          \"Start\": \"2023-07-25T15:00:42.299676089Z\",\n          \"End\": \"2023-07-25T15:00:42.384213818Z\",\n          \"ExitCode\": 0,\n          \"Output\": \"\"\n        },\n        {\n          \"Start\": \"2023-07-25T15:00:47.389142447Z\",\n          \"End\": \"2023-07-25T15:00:47.514483294Z\",\n          \"ExitCode\": 0,\n          \"Output\": \"\"\n        },\n        {\n          \"Start\": \"2023-07-25T15:00:52.518770886Z\",\n          \"End\": \"2023-07-25T15:00:52.644288742Z\",\n          \"ExitCode\": 0,\n          \"Output\": \"\"\n        }\n      ]\n    }\n  },\n  \"Image\": \"sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2-json.log\",\n  \"Name\": \"/wt-contnet-producer-1\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": null,\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"wt-contnet_default\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"CapAdd\": [\n      \"NET_ADMIN\"\n    ],\n    \"CapDrop\": null,\n    \"CgroupnsMode\": \"host\",\n    \"Dns\": null,\n    \"DnsOptions\": null,\n    \"DnsSearch\": null,\n    \"ExtraHosts\": [],\n    \"GroupAdd\": null,\n    \"IpcMode\": \"private\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": null,\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": null,\n    \"DeviceCgroupRules\": null,\n    \"DeviceRequests\": null,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": null,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2-init/diff:/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff:/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [],\n  \"Config\": {\n    \"Hostname\": \"25e75393800b\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"ExposedPorts\": {\n      \"8000/tcp\": {},\n      \"8388/tcp\": {},\n      \"8388/udp\": {},\n      \"8888/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"OPENVPN_PASSWORD=\",\n      \"SERVER_COUNTRIES=Sweden\",\n      \"VPN_SERVICE_PROVIDER=nordvpn\",\n      \"OPENVPN_USER=\",\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"VPN_TYPE=openvpn\",\n      \"VPN_ENDPOINT_IP=\",\n      \"VPN_ENDPOINT_PORT=\",\n      \"VPN_INTERFACE=tun0\",\n      \"OPENVPN_PROTOCOL=udp\",\n      \"OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user\",\n      \"OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password\",\n      \"OPENVPN_VERSION=2.5\",\n      \"OPENVPN_VERBOSITY=1\",\n      \"OPENVPN_FLAGS=\",\n      \"OPENVPN_CIPHERS=\",\n      \"OPENVPN_AUTH=\",\n      \"OPENVPN_PROCESS_USER=root\",\n      \"OPENVPN_CUSTOM_CONFIG=\",\n      \"WIREGUARD_PRIVATE_KEY=\",\n      \"WIREGUARD_PRESHARED_KEY=\",\n      \"WIREGUARD_PUBLIC_KEY=\",\n      \"WIREGUARD_ALLOWED_IPS=\",\n      \"WIREGUARD_ADDRESSES=\",\n      \"WIREGUARD_MTU=1400\",\n      \"WIREGUARD_IMPLEMENTATION=auto\",\n      \"SERVER_REGIONS=\",\n      \"SERVER_CITIES=\",\n      \"SERVER_HOSTNAMES=\",\n      \"ISP=\",\n      \"OWNED_ONLY=no\",\n      \"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=\",\n      \"VPN_PORT_FORWARDING=off\",\n      \"VPN_PORT_FORWARDING_PROVIDER=\",\n      \"VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port\",\n      \"OPENVPN_CERT=\",\n      \"OPENVPN_KEY=\",\n      \"OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt\",\n      \"OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey\",\n      \"OPENVPN_ENCRYPTED_KEY=\",\n      \"OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key\",\n      \"OPENVPN_KEY_PASSPHRASE=\",\n      \"OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase\",\n      \"SERVER_NUMBER=\",\n      \"SERVER_NAMES=\",\n      \"FREE_ONLY=\",\n      \"MULTIHOP_ONLY=\",\n      \"PREMIUM_ONLY=\",\n      \"FIREWALL=on\",\n      \"FIREWALL_VPN_INPUT_PORTS=\",\n      \"FIREWALL_INPUT_PORTS=\",\n      \"FIREWALL_OUTBOUND_SUBNETS=\",\n      \"FIREWALL_DEBUG=off\",\n      \"LOG_LEVEL=info\",\n      \"HEALTH_SERVER_ADDRESS=127.0.0.1:9999\",\n      \"HEALTH_TARGET_ADDRESS=cloudflare.com:443\",\n      \"HEALTH_SUCCESS_WAIT_DURATION=5s\",\n      \"HEALTH_VPN_DURATION_INITIAL=6s\",\n      \"HEALTH_VPN_DURATION_ADDITION=5s\",\n      \"DOT=on\",\n      \"DOT_PROVIDERS=cloudflare\",\n      \"DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112\",\n      \"DOT_VERBOSITY=1\",\n      \"DOT_VERBOSITY_DETAILS=0\",\n      \"DOT_VALIDATION_LOGLEVEL=0\",\n      \"DOT_CACHING=on\",\n      \"DOT_IPV6=off\",\n      \"BLOCK_MALICIOUS=on\",\n      \"BLOCK_SURVEILLANCE=off\",\n      \"BLOCK_ADS=off\",\n      \"UNBLOCK=\",\n      \"DNS_UPDATE_PERIOD=24h\",\n      \"DNS_ADDRESS=127.0.0.1\",\n      \"DNS_KEEP_NAMESERVER=off\",\n      \"HTTPPROXY=\",\n      \"HTTPPROXY_LOG=off\",\n      \"HTTPPROXY_LISTENING_ADDRESS=:8888\",\n      \"HTTPPROXY_STEALTH=off\",\n      \"HTTPPROXY_USER=\",\n      \"HTTPPROXY_PASSWORD=\",\n      \"HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user\",\n      \"HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password\",\n      \"SHADOWSOCKS=off\",\n      \"SHADOWSOCKS_LOG=off\",\n      \"SHADOWSOCKS_LISTENING_ADDRESS=:8388\",\n      \"SHADOWSOCKS_PASSWORD=\",\n      \"SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password\",\n      \"SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305\",\n      \"HTTP_CONTROL_SERVER_LOG=on\",\n      \"HTTP_CONTROL_SERVER_ADDRESS=:8000\",\n      \"UPDATER_PERIOD=0\",\n      \"UPDATER_MIN_RATIO=0.8\",\n      \"UPDATER_VPN_SERVICE_PROVIDERS=\",\n      \"PUBLICIP_FILE=/tmp/gluetun/ip\",\n      \"PUBLICIP_PERIOD=12h\",\n      \"PPROF_ENABLED=no\",\n      \"PPROF_BLOCK_PROFILE_RATE=0\",\n      \"PPROF_MUTEX_PROFILE_RATE=0\",\n      \"PPROF_HTTP_SERVER_ADDRESS=:6060\",\n      \"VERSION_INFORMATION=on\",\n      \"TZ=\",\n      \"PUID=\",\n      \"PGID=\"\n    ],\n    \"Cmd\": null,\n    \"Healthcheck\": {\n      \"Test\": [\n        \"CMD-SHELL\",\n        \"/gluetun-entrypoint healthcheck\"\n      ],\n      \"Interval\": 5000000000,\n      \"Timeout\": 5000000000,\n      \"StartPeriod\": 10000000000,\n      \"Retries\": 1\n    },\n    \"Image\": \"qmcgaw/gluetun\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/gluetun-entrypoint\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.docker.compose.config-hash\": \"6dc7dc42a86edb47039de3650a9cb9bdcf4866c113b8f9d797722c9dfd20428b\",\n      \"com.docker.compose.container-number\": \"1\",\n      \"com.docker.compose.depends_on\": \"\",\n      \"com.docker.compose.image\": \"sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51\",\n      \"com.docker.compose.oneoff\": \"False\",\n      \"com.docker.compose.project\": \"wt-contnet\",\n      \"com.docker.compose.project.config_files\": \"/tmp/wt-contnet/docker-compose.yaml\",\n      \"com.docker.compose.project.working_dir\": \"/tmp/wt-contnet\",\n      \"com.docker.compose.replace\": \"9bd1ce000be81819fc915aa60a1674c7573b59a26ac4643ecf427a5732b9785f\",\n      \"com.docker.compose.service\": \"producer\",\n      \"com.docker.compose.version\": \"2.19.1\",\n      \"desktop.docker.io/wsl-distro\": \"Ubuntu\",\n      \"org.opencontainers.image.authors\": \"quentin.mcgaw@gmail.com\",\n      \"org.opencontainers.image.created\": \"2023-07-22T16:07:05.641Z\",\n      \"org.opencontainers.image.description\": \"VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.\",\n      \"org.opencontainers.image.documentation\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.licenses\": \"MIT\",\n      \"org.opencontainers.image.revision\": \"eecfb3952f202c0de3867d88e96d80c6b0f48359\",\n      \"org.opencontainers.image.source\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.title\": \"gluetun\",\n      \"org.opencontainers.image.url\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.version\": \"latest\"\n    }\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"34a321b64bb1b15f994dfccff0e235f881504f240c2028876ff6683962eaa10e\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {\n      \"8000/tcp\": null,\n      \"8388/tcp\": null,\n      \"8388/udp\": null,\n      \"8888/tcp\": null\n    },\n    \"SandboxKey\": \"/var/run/docker/netns/34a321b64bb1\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {\n      \"wt-contnet_default\": {\n        \"IPAMConfig\": null,\n        \"Links\": null,\n        \"Aliases\": [\n          \"wt-contnet-producer-1\",\n          \"producer\",\n          \"25e75393800b\"\n        ],\n        \"NetworkID\": \"f0f652a79efc54bcad52aafb4cbcc3b5dce1acaf11b172d8678d25f665faf63d\",\n        \"EndpointID\": \"2429c2b5d08db6c986bbd419a52ca4dd352715d80c5aeae04742efb84b0356fc\",\n        \"Gateway\": \"172.19.0.1\",\n        \"IPAddress\": \"172.19.0.2\",\n        \"IPPrefixLen\": 16,\n        \"IPv6Gateway\": \"\",\n        \"GlobalIPv6Address\": \"\",\n        \"GlobalIPv6PrefixLen\": 0,\n        \"MacAddress\": \"02:42:ac:13:00:02\",\n        \"DriverOpts\": null\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_restarting.json",
    "content": "{\n  \"Id\": \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67\",\n  \"Created\": \"2019-04-10T19:51:22.245041005Z\",\n  \"Path\": \"/watchtower\",\n  \"Args\": [],\n  \"State\": {\n    \"Status\": \"exited\",\n    \"Running\": false,\n    \"Paused\": false,\n    \"Restarting\": true,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 0,\n    \"ExitCode\": 1,\n    \"Error\": \"\",\n    \"StartedAt\": \"2019-04-10T19:51:22.918972606Z\",\n    \"FinishedAt\": \"2019-04-10T19:52:14.265091583Z\"\n  },\n  \"Image\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65-json.log\",\n  \"Name\": \"/watchtower-test\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": [\n      \"/var/run/docker.sock:/var/run/docker.sock\"\n    ],\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"default\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"no\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"Dns\": [],\n    \"DnsOptions\": [],\n    \"DnsSearch\": [],\n    \"ExtraHosts\": null,\n    \"GroupAdd\": null,\n    \"IpcMode\": \"shareable\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": [],\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": [],\n    \"DeviceCgroupRules\": null,\n    \"DiskQuota\": 0,\n    \"KernelMemory\": 0,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": 0,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc-init/diff:/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff:/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [\n    {\n      \"Type\": \"bind\",\n      \"Source\": \"/var/run/docker.sock\",\n      \"Destination\": \"/var/run/docker.sock\",\n      \"Mode\": \"\",\n      \"RW\": true,\n      \"Propagation\": \"rprivate\"\n    }\n  ],\n  \"Config\": {\n    \"Hostname\": \"ae8964ba86c7\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"containrrr/watchtower:latest\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/watchtower\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    }\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"05627d36c08ed994eebc44a2a8c9365a511756b55c500fb03fd5a14477cd4bf3\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {},\n    \"SandboxKey\": \"/var/run/docker/netns/05627d36c08e\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {\n      \"bridge\": {\n        \"IPAMConfig\": null,\n        \"Links\": null,\n        \"Aliases\": null,\n        \"NetworkID\": \"8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e\",\n        \"EndpointID\": \"\",\n        \"Gateway\": \"\",\n        \"IPAddress\": \"\",\n        \"IPPrefixLen\": 0,\n        \"IPv6Gateway\": \"\",\n        \"GlobalIPv6Address\": \"\",\n        \"GlobalIPv6PrefixLen\": 0,\n        \"MacAddress\": \"\",\n        \"DriverOpts\": null\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_running.json",
    "content": "{\n  \"Id\": \"b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008\",\n  \"Created\": \"2019-04-04T20:28:32.5710901Z\",\n  \"Path\": \"/portainer\",\n  \"Args\": [],\n  \"State\": {\n    \"Status\": \"running\",\n    \"Running\": true,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 3854,\n    \"ExitCode\": 0,\n    \"Error\": \"\",\n    \"StartedAt\": \"2019-04-13T22:38:24.498745809Z\",\n    \"FinishedAt\": \"2019-04-13T22:38:18.486292076Z\"\n  },\n  \"Image\": \"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008-json.log\",\n  \"Name\": \"/portainer\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": [\n      \"portainer_data:/data\",\n      \"/var/run/docker.sock:/var/run/docker.sock\"\n    ],\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"default\",\n    \"PortBindings\": {\n      \"9000/tcp\": [\n        {\n          \"HostIp\": \"\",\n          \"HostPort\": \"9000\"\n        }\n      ]\n    },\n    \"RestartPolicy\": {\n      \"Name\": \"always\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"Dns\": [],\n    \"DnsOptions\": [],\n    \"DnsSearch\": [],\n    \"ExtraHosts\": null,\n    \"GroupAdd\": null,\n    \"IpcMode\": \"shareable\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": [],\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": [],\n    \"DeviceCgroupRules\": null,\n    \"DiskQuota\": 0,\n    \"KernelMemory\": 0,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": 0,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c-init/diff:/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/diff:/var/lib/docker/overlay2/6c3f44131f6f13c9ea1a99a1b24bf348f70ba3eef244f29202faef3a2216ac11/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [\n    {\n      \"Type\": \"volume\",\n      \"Name\": \"portainer_data\",\n      \"Source\": \"/var/lib/docker/volumes/portainer_data/_data\",\n      \"Destination\": \"/data\",\n      \"Driver\": \"local\",\n      \"Mode\": \"z\",\n      \"RW\": true,\n      \"Propagation\": \"\"\n    },\n    {\n      \"Type\": \"bind\",\n      \"Source\": \"/var/run/docker.sock\",\n      \"Destination\": \"/var/run/docker.sock\",\n      \"Mode\": \"\",\n      \"RW\": true,\n      \"Propagation\": \"rprivate\"\n    }\n  ],\n  \"Config\": {\n    \"Hostname\": \"822f0f2efd78\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"9000/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"portainer/portainer:latest\",\n    \"Volumes\": {\n      \"/data\": {}\n    },\n    \"WorkingDir\": \"/\",\n    \"Entrypoint\": [\n      \"/portainer\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {}\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"8819e19588be798020f2d09e36a577c39a47809e68c2769a1525880c0bcd5b11\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {\n      \"9000/tcp\": [\n        {\n          \"HostIp\": \"0.0.0.0\",\n          \"HostPort\": \"9000\"\n        }\n      ]\n    },\n    \"SandboxKey\": \"/var/run/docker/netns/8819e19588be\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702\",\n    \"Gateway\": \"172.17.0.1\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"172.17.0.2\",\n    \"IPPrefixLen\": 16,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"02:42:ac:11:00:02\",\n    \"Networks\": {\n      \"bridge\": {\n        \"IPAMConfig\": null,\n        \"Links\": null,\n        \"Aliases\": null,\n        \"NetworkID\": \"9352796e0330dcf31ce3d44fae4b719304b8b3fd97b02ade3aefb8737251682b\",\n        \"EndpointID\": \"a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702\",\n        \"Gateway\": \"172.17.0.1\",\n        \"IPAddress\": \"172.17.0.2\",\n        \"IPPrefixLen\": 16,\n        \"IPv6Gateway\": \"\",\n        \"GlobalIPv6Address\": \"\",\n        \"GlobalIPv6PrefixLen\": 0,\n        \"MacAddress\": \"02:42:ac:11:00:02\",\n        \"DriverOpts\": null\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_stopped.json",
    "content": "{\n  \"Id\": \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65\",\n  \"Created\": \"2019-04-10T19:51:22.245041005Z\",\n  \"Path\": \"/watchtower\",\n  \"Args\": [],\n  \"State\": {\n    \"Status\": \"exited\",\n    \"Running\": false,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 0,\n    \"ExitCode\": 1,\n    \"Error\": \"\",\n    \"StartedAt\": \"2019-04-10T19:51:22.918972606Z\",\n    \"FinishedAt\": \"2019-04-10T19:52:14.265091583Z\"\n  },\n  \"Image\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65-json.log\",\n  \"Name\": \"/watchtower-stopped\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": [\n      \"/var/run/docker.sock:/var/run/docker.sock\"\n    ],\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"default\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"no\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"Dns\": [],\n    \"DnsOptions\": [],\n    \"DnsSearch\": [],\n    \"ExtraHosts\": null,\n    \"GroupAdd\": null,\n    \"IpcMode\": \"shareable\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": [],\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": [],\n    \"DeviceCgroupRules\": null,\n    \"DiskQuota\": 0,\n    \"KernelMemory\": 0,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": 0,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc-init/diff:/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff:/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [\n    {\n      \"Type\": \"bind\",\n      \"Source\": \"/var/run/docker.sock\",\n      \"Destination\": \"/var/run/docker.sock\",\n      \"Mode\": \"\",\n      \"RW\": true,\n      \"Propagation\": \"rprivate\"\n    }\n  ],\n  \"Config\": {\n    \"Hostname\": \"ae8964ba86c7\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"containrrr/watchtower:latest\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/watchtower\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    }\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"05627d36c08ed994eebc44a2a8c9365a511756b55c500fb03fd5a14477cd4bf3\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {},\n    \"SandboxKey\": \"/var/run/docker/netns/05627d36c08e\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {\n      \"bridge\": {\n        \"IPAMConfig\": null,\n        \"Links\": null,\n        \"Aliases\": null,\n        \"NetworkID\": \"8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e\",\n        \"EndpointID\": \"\",\n        \"Gateway\": \"\",\n        \"IPAddress\": \"\",\n        \"IPPrefixLen\": 0,\n        \"IPv6Gateway\": \"\",\n        \"GlobalIPv6Address\": \"\",\n        \"GlobalIPv6PrefixLen\": 0,\n        \"MacAddress\": \"\",\n        \"DriverOpts\": null\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/container_watchtower.json",
    "content": "{\n  \"Id\": \"3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134\",\n  \"Created\": \"2020-04-10T19:51:22.245041005Z\",\n  \"Path\": \"/watchtower\",\n  \"Args\": [],\n  \"State\": {\n    \"Status\": \"running\",\n    \"Running\": true,\n    \"Paused\": false,\n    \"Restarting\": false,\n    \"OOMKilled\": false,\n    \"Dead\": false,\n    \"Pid\": 3854,\n    \"ExitCode\": 0,\n    \"Error\": \"\",\n    \"StartedAt\": \"2019-04-13T22:38:24.498745809Z\",\n    \"FinishedAt\": \"2019-04-13T22:38:18.486292076Z\"\n  },\n  \"Image\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n  \"ResolvConfPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/resolv.conf\",\n  \"HostnamePath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hostname\",\n  \"HostsPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hosts\",\n  \"LogPath\": \"/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65-json.log\",\n  \"Name\": \"/watchtower-running\",\n  \"RestartCount\": 0,\n  \"Driver\": \"overlay2\",\n  \"Platform\": \"linux\",\n  \"MountLabel\": \"\",\n  \"ProcessLabel\": \"\",\n  \"AppArmorProfile\": \"\",\n  \"ExecIDs\": null,\n  \"HostConfig\": {\n    \"Binds\": [\n      \"/var/run/docker.sock:/var/run/docker.sock\"\n    ],\n    \"ContainerIDFile\": \"\",\n    \"LogConfig\": {\n      \"Type\": \"json-file\",\n      \"Config\": {}\n    },\n    \"NetworkMode\": \"default\",\n    \"PortBindings\": {},\n    \"RestartPolicy\": {\n      \"Name\": \"no\",\n      \"MaximumRetryCount\": 0\n    },\n    \"AutoRemove\": false,\n    \"VolumeDriver\": \"\",\n    \"VolumesFrom\": null,\n    \"CapAdd\": null,\n    \"CapDrop\": null,\n    \"Dns\": [],\n    \"DnsOptions\": [],\n    \"DnsSearch\": [],\n    \"ExtraHosts\": null,\n    \"GroupAdd\": null,\n    \"IpcMode\": \"shareable\",\n    \"Cgroup\": \"\",\n    \"Links\": null,\n    \"OomScoreAdj\": 0,\n    \"PidMode\": \"\",\n    \"Privileged\": false,\n    \"PublishAllPorts\": false,\n    \"ReadonlyRootfs\": false,\n    \"SecurityOpt\": null,\n    \"UTSMode\": \"\",\n    \"UsernsMode\": \"\",\n    \"ShmSize\": 67108864,\n    \"Runtime\": \"runc\",\n    \"ConsoleSize\": [\n      0,\n      0\n    ],\n    \"Isolation\": \"\",\n    \"CpuShares\": 0,\n    \"Memory\": 0,\n    \"NanoCpus\": 0,\n    \"CgroupParent\": \"\",\n    \"BlkioWeight\": 0,\n    \"BlkioWeightDevice\": [],\n    \"BlkioDeviceReadBps\": null,\n    \"BlkioDeviceWriteBps\": null,\n    \"BlkioDeviceReadIOps\": null,\n    \"BlkioDeviceWriteIOps\": null,\n    \"CpuPeriod\": 0,\n    \"CpuQuota\": 0,\n    \"CpuRealtimePeriod\": 0,\n    \"CpuRealtimeRuntime\": 0,\n    \"CpusetCpus\": \"\",\n    \"CpusetMems\": \"\",\n    \"Devices\": [],\n    \"DeviceCgroupRules\": null,\n    \"DiskQuota\": 0,\n    \"KernelMemory\": 0,\n    \"MemoryReservation\": 0,\n    \"MemorySwap\": 0,\n    \"MemorySwappiness\": null,\n    \"OomKillDisable\": false,\n    \"PidsLimit\": 0,\n    \"Ulimits\": null,\n    \"CpuCount\": 0,\n    \"CpuPercent\": 0,\n    \"IOMaximumIOps\": 0,\n    \"IOMaximumBandwidth\": 0,\n    \"MaskedPaths\": [\n      \"/proc/asound\",\n      \"/proc/acpi\",\n      \"/proc/kcore\",\n      \"/proc/keys\",\n      \"/proc/latency_stats\",\n      \"/proc/timer_list\",\n      \"/proc/timer_stats\",\n      \"/proc/sched_debug\",\n      \"/proc/scsi\",\n      \"/sys/firmware\"\n    ],\n    \"ReadonlyPaths\": [\n      \"/proc/bus\",\n      \"/proc/fs\",\n      \"/proc/irq\",\n      \"/proc/sys\",\n      \"/proc/sysrq-trigger\"\n    ]\n  },\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc-init/diff:/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff:/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"Mounts\": [\n    {\n      \"Type\": \"bind\",\n      \"Source\": \"/var/run/docker.sock\",\n      \"Destination\": \"/var/run/docker.sock\",\n      \"Mode\": \"\",\n      \"RW\": true,\n      \"Propagation\": \"rprivate\"\n    }\n  ],\n  \"Config\": {\n    \"Hostname\": \"ae8964ba86c7\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": true,\n    \"AttachStderr\": true,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"containrrr/watchtower:latest\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/watchtower\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    }\n  },\n  \"NetworkSettings\": {\n    \"Bridge\": \"\",\n    \"SandboxID\": \"05627d36c08ed994eebc44a2a8c9365a511756b55c500fb03fd5a14477cd4bf3\",\n    \"HairpinMode\": false,\n    \"LinkLocalIPv6Address\": \"\",\n    \"LinkLocalIPv6PrefixLen\": 0,\n    \"Ports\": {},\n    \"SandboxKey\": \"/var/run/docker/netns/05627d36c08e\",\n    \"SecondaryIPAddresses\": null,\n    \"SecondaryIPv6Addresses\": null,\n    \"EndpointID\": \"\",\n    \"Gateway\": \"\",\n    \"GlobalIPv6Address\": \"\",\n    \"GlobalIPv6PrefixLen\": 0,\n    \"IPAddress\": \"\",\n    \"IPPrefixLen\": 0,\n    \"IPv6Gateway\": \"\",\n    \"MacAddress\": \"\",\n    \"Networks\": {\n      \"bridge\": {\n        \"IPAMConfig\": null,\n        \"Links\": null,\n        \"Aliases\": null,\n        \"NetworkID\": \"8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e\",\n        \"EndpointID\": \"\",\n        \"Gateway\": \"\",\n        \"IPAddress\": \"\",\n        \"IPPrefixLen\": 0,\n        \"IPv6Gateway\": \"\",\n        \"GlobalIPv6Address\": \"\",\n        \"GlobalIPv6PrefixLen\": 0,\n        \"MacAddress\": \"\",\n        \"DriverOpts\": null\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/containers.json",
    "content": "[\n  {\n    \"Id\": \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65\",\n    \"Names\": [\n      \"/watchtower-stopped\"\n    ],\n    \"Image\": \"containrrr/watchtower:latest\",\n    \"ImageID\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n    \"Command\": \"/watchtower\",\n    \"Created\": 1554925882,\n    \"Ports\": [],\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    },\n    \"State\": \"exited\",\n    \"Status\": \"Exited (1) 6 days ago\",\n    \"HostConfig\": {\n      \"NetworkMode\": \"default\"\n    },\n    \"NetworkSettings\": {\n      \"Networks\": {\n        \"bridge\": {\n          \"IPAMConfig\": null,\n          \"Links\": null,\n          \"Aliases\": null,\n          \"NetworkID\": \"8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e\",\n          \"EndpointID\": \"\",\n          \"Gateway\": \"\",\n          \"IPAddress\": \"\",\n          \"IPPrefixLen\": 0,\n          \"IPv6Gateway\": \"\",\n          \"GlobalIPv6Address\": \"\",\n          \"GlobalIPv6PrefixLen\": 0,\n          \"MacAddress\": \"\",\n          \"DriverOpts\": null\n        }\n      }\n    },\n    \"Mounts\": [\n      {\n        \"Type\": \"bind\",\n        \"Source\": \"/var/run/docker.sock\",\n        \"Destination\": \"/var/run/docker.sock\",\n        \"Mode\": \"\",\n        \"RW\": true,\n        \"Propagation\": \"rprivate\"\n      }\n    ]\n  },\n  {\n    \"Id\": \"3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134\",\n    \"Names\": [\n      \"/watchtower-running\"\n    ],\n    \"Image\": \"containrrr/watchtower:latest\",\n    \"ImageID\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n    \"Command\": \"/watchtower\",\n    \"Created\": 1554925882,\n    \"Ports\": [],\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    },\n    \"State\": \"running\",\n    \"Status\": \"Up 3 days\",\n    \"HostConfig\": {\n      \"NetworkMode\": \"default\"\n    },\n    \"NetworkSettings\": {\n      \"Networks\": {\n        \"bridge\": {\n          \"IPAMConfig\": null,\n          \"Links\": null,\n          \"Aliases\": null,\n          \"NetworkID\": \"8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e\",\n          \"EndpointID\": \"\",\n          \"Gateway\": \"\",\n          \"IPAddress\": \"\",\n          \"IPPrefixLen\": 0,\n          \"IPv6Gateway\": \"\",\n          \"GlobalIPv6Address\": \"\",\n          \"GlobalIPv6PrefixLen\": 0,\n          \"MacAddress\": \"\",\n          \"DriverOpts\": null\n        }\n      }\n    },\n    \"Mounts\": [\n      {\n        \"Type\": \"bind\",\n        \"Source\": \"/var/run/docker.sock\",\n        \"Destination\": \"/var/run/docker.sock\",\n        \"Mode\": \"\",\n        \"RW\": true,\n        \"Propagation\": \"rprivate\"\n      }\n    ]\n  },\n  {\n    \"Id\": \"b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008\",\n    \"Names\": [\n      \"/portainer\"\n    ],\n    \"Image\": \"portainer/portainer:latest\",\n    \"ImageID\": \"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd\",\n    \"Command\": \"/portainer\",\n    \"Created\": 1554409712,\n    \"Ports\": [\n      {\n        \"IP\": \"0.0.0.0\",\n        \"PrivatePort\": 9000,\n        \"PublicPort\": 9000,\n        \"Type\": \"tcp\"\n      }\n    ],\n    \"Labels\": {},\n    \"State\": \"running\",\n    \"Status\": \"Up 3 days\",\n    \"HostConfig\": {\n      \"NetworkMode\": \"default\"\n    },\n    \"NetworkSettings\": {\n      \"Networks\": {\n        \"bridge\": {\n          \"IPAMConfig\": null,\n          \"Links\": null,\n          \"Aliases\": null,\n          \"NetworkID\": \"9352796e0330dcf31ce3d44fae4b719304b8b3fd97b02ade3aefb8737251682b\",\n          \"EndpointID\": \"a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702\",\n          \"Gateway\": \"172.17.0.1\",\n          \"IPAddress\": \"172.17.0.2\",\n          \"IPPrefixLen\": 16,\n          \"IPv6Gateway\": \"\",\n          \"GlobalIPv6Address\": \"\",\n          \"GlobalIPv6PrefixLen\": 0,\n          \"MacAddress\": \"02:42:ac:11:00:02\",\n          \"DriverOpts\": null\n        }\n      }\n    },\n    \"Mounts\": [\n      {\n        \"Type\": \"volume\",\n        \"Name\": \"portainer_data\",\n        \"Source\": \"/var/lib/docker/volumes/portainer_data/_data\",\n        \"Destination\": \"/data\",\n        \"Driver\": \"local\",\n        \"Mode\": \"z\",\n        \"RW\": true,\n        \"Propagation\": \"\"\n      },\n      {\n        \"Type\": \"bind\",\n        \"Source\": \"/var/run/docker.sock\",\n        \"Destination\": \"/var/run/docker.sock\",\n        \"Mode\": \"\",\n        \"RW\": true,\n        \"Propagation\": \"rprivate\"\n      }\n    ]\n  },\n  {\n    \"Id\": \"ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67\",\n    \"Names\": [\n      \"/portainer\"\n    ],\n    \"Image\": \"portainer/portainer:latest\",\n    \"ImageID\": \"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd\",\n    \"Command\": \"/portainer\",\n    \"Created\": 1554409712,\n    \"Ports\": [\n      {\n        \"IP\": \"0.0.0.0\",\n        \"PrivatePort\": 9000,\n        \"PublicPort\": 9000,\n        \"Type\": \"tcp\"\n      }\n    ],\n    \"Labels\": {},\n    \"State\": \"restarting\",\n    \"Status\": \"Restarting (0) 35 seconds ago\",\n    \"HostConfig\": {\n      \"NetworkMode\": \"default\"\n    },\n    \"NetworkSettings\": {\n      \"Networks\": {\n        \"bridge\": {\n          \"IPAMConfig\": null,\n          \"Links\": null,\n          \"Aliases\": null,\n          \"NetworkID\": \"9352796e0330dcf31ce3d44fae4b719304b8b3fd97b02ade3aefb8737251682b\",\n          \"EndpointID\": \"a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702\",\n          \"Gateway\": \"172.17.0.1\",\n          \"IPAddress\": \"172.17.0.2\",\n          \"IPPrefixLen\": 16,\n          \"IPv6Gateway\": \"\",\n          \"GlobalIPv6Address\": \"\",\n          \"GlobalIPv6PrefixLen\": 0,\n          \"MacAddress\": \"02:42:ac:11:00:02\",\n          \"DriverOpts\": null\n        }\n      }\n    },\n    \"Mounts\": [\n      {\n        \"Type\": \"volume\",\n        \"Name\": \"portainer_data\",\n        \"Source\": \"/var/lib/docker/volumes/portainer_data/_data\",\n        \"Destination\": \"/data\",\n        \"Driver\": \"local\",\n        \"Mode\": \"z\",\n        \"RW\": true,\n        \"Propagation\": \"\"\n      },\n      {\n        \"Type\": \"bind\",\n        \"Source\": \"/var/run/docker.sock\",\n        \"Destination\": \"/var/run/docker.sock\",\n        \"Mode\": \"\",\n        \"RW\": true,\n        \"Propagation\": \"rprivate\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "pkg/container/mocks/data/image_default.json",
    "content": "{\n  \"Id\": \"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd\",\n  \"RepoTags\": [\n    \"portainer/portainer:latest\"\n  ],\n  \"RepoDigests\": [\n    \"portainer/portainer@sha256:d6cc2c20c0af38d8d557ab994c419c799a10fe825e4aa57fea2e2e507a13747d\"\n  ],\n  \"Parent\": \"\",\n  \"Comment\": \"\",\n  \"Created\": \"2019-03-05T04:41:17.612066939Z\",\n  \"Container\": \"022100cf79dfee27867d5ff7aa3ff7ecc5cbd486747e808a59b6accd393d65f5\",\n  \"ContainerConfig\": {\n    \"Hostname\": \"022100cf79df\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"9000/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": [\n      \"/bin/sh\",\n      \"-c\",\n      \"#(nop) \",\n      \"ENTRYPOINT [\\\"/portainer\\\"]\"\n    ],\n    \"Image\": \"sha256:9cf3ead5068a16f1bc1e18d6e730940f05fd59f60dfe1f6b3a5956196191dc77\",\n    \"Volumes\": {\n      \"/data\": {}\n    },\n    \"WorkingDir\": \"/\",\n    \"Entrypoint\": [\n      \"/portainer\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {}\n  },\n  \"DockerVersion\": \"18.09.2\",\n  \"Author\": \"\",\n  \"Config\": {\n    \"Hostname\": \"\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"9000/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"sha256:9cf3ead5068a16f1bc1e18d6e730940f05fd59f60dfe1f6b3a5956196191dc77\",\n    \"Volumes\": {\n      \"/data\": {}\n    },\n    \"WorkingDir\": \"/\",\n    \"Entrypoint\": [\n      \"/portainer\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": null\n  },\n  \"Architecture\": \"amd64\",\n  \"Os\": \"linux\",\n  \"Size\": 74089106,\n  \"VirtualSize\": 74089106,\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/6c3f44131f6f13c9ea1a99a1b24bf348f70ba3eef244f29202faef3a2216ac11/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"RootFS\": {\n    \"Type\": \"layers\",\n    \"Layers\": [\n      \"sha256:dd4969f97241b9aefe2a70f560ce399ee9fa0354301c9aef841082ad52161ec5\",\n      \"sha256:e7260fd2a5f240122129b2d421726d7a4a2bda0cc292e962b694196af8856f20\"\n    ]\n  },\n  \"Metadata\": {\n    \"LastTagTime\": \"0001-01-01T00:00:00Z\"\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/image_net_consumer.json",
    "content": "{\n  \"Id\": \"sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8\",\n  \"RepoTags\": [\n    \"nginx:latest\"\n  ],\n  \"RepoDigests\": [\n    \"nginx@sha256:aa0afebbb3cfa473099a62c4b32e9b3fb73ed23f2a75a65ce1d4b4f55a5c2ef2\"\n  ],\n  \"Parent\": \"\",\n  \"Comment\": \"\",\n  \"Created\": \"2023-03-01T18:43:12.914398123Z\",\n  \"Container\": \"71a4c9a59d252d7c54812429bfe5df477e54e91ebfff1939ae39ecdf055d445c\",\n  \"ContainerConfig\": {\n    \"Hostname\": \"71a4c9a59d25\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"80/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"NGINX_VERSION=1.23.3\",\n      \"NJS_VERSION=0.7.9\",\n      \"PKG_RELEASE=1~bullseye\"\n    ],\n    \"Cmd\": [\n      \"/bin/sh\",\n      \"-c\",\n      \"#(nop) \",\n      \"CMD [\\\"nginx\\\" \\\"-g\\\" \\\"daemon off;\\\"]\"\n    ],\n    \"Image\": \"sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/docker-entrypoint.sh\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"maintainer\": \"NGINX Docker Maintainers <docker-maint@nginx.com>\"\n    },\n    \"StopSignal\": \"SIGQUIT\"\n  },\n  \"DockerVersion\": \"20.10.23\",\n  \"Author\": \"\",\n  \"Config\": {\n    \"Hostname\": \"\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"80/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"NGINX_VERSION=1.23.3\",\n      \"NJS_VERSION=0.7.9\",\n      \"PKG_RELEASE=1~bullseye\"\n    ],\n    \"Cmd\": [\n      \"nginx\",\n      \"-g\",\n      \"daemon off;\"\n    ],\n    \"Image\": \"sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/docker-entrypoint.sh\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"maintainer\": \"NGINX Docker Maintainers <docker-maint@nginx.com>\"\n    },\n    \"StopSignal\": \"SIGQUIT\"\n  },\n  \"Architecture\": \"amd64\",\n  \"Os\": \"linux\",\n  \"Size\": 141838643,\n  \"VirtualSize\": 141838643,\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"RootFS\": {\n    \"Type\": \"layers\",\n    \"Layers\": [\n      \"sha256:650abce4b096b06ac8bec2046d821d66d801af34f1f1d4c5e272ad030c7873db\",\n      \"sha256:4dc5cd799a08ff49a603870c8378ea93083bfc2a4176f56e5531997e94c195d0\",\n      \"sha256:e161c82b34d21179db1f546c1cd84153d28a17d865ccaf2dedeb06a903fec12c\",\n      \"sha256:83ba6d8ffb8c2974174c02d3ba549e7e0656ebb1bc075a6b6ee89b6c609c6a71\",\n      \"sha256:d8466e142d8710abf5b495ebb536478f7e19d9d03b151b5d5bd09df4cfb49248\",\n      \"sha256:101af4ba983b04be266217ecee414e88b23e394f62e9801c7c1bdb37cb37bcaa\"\n    ]\n  },\n  \"Metadata\": {\n    \"LastTagTime\": \"0001-01-01T00:00:00Z\"\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/image_net_producer.json",
    "content": "{\n  \"Id\": \"sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51\",\n  \"RepoTags\": [\n    \"qmcgaw/gluetun:latest\"\n  ],\n  \"RepoDigests\": [\n    \"qmcgaw/gluetun@sha256:cd532bf4ef88a348a915c6dc62a9867a2eca89aa70559b0b4a1ea15cc0e595d1\"\n  ],\n  \"Parent\": \"\",\n  \"Comment\": \"buildkit.dockerfile.v0\",\n  \"Created\": \"2023-07-22T16:10:29.457146856Z\",\n  \"Container\": \"\",\n  \"ContainerConfig\": {\n    \"Hostname\": \"\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": null,\n    \"Cmd\": null,\n    \"Image\": \"\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": null,\n    \"OnBuild\": null,\n    \"Labels\": null\n  },\n  \"DockerVersion\": \"\",\n  \"Author\": \"\",\n  \"Config\": {\n    \"Hostname\": \"\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"ExposedPorts\": {\n      \"8000/tcp\": {},\n      \"8388/tcp\": {},\n      \"8388/udp\": {},\n      \"8888/tcp\": {}\n    },\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\n      \"VPN_SERVICE_PROVIDER=pia\",\n      \"VPN_TYPE=openvpn\",\n      \"VPN_ENDPOINT_IP=\",\n      \"VPN_ENDPOINT_PORT=\",\n      \"VPN_INTERFACE=tun0\",\n      \"OPENVPN_PROTOCOL=udp\",\n      \"OPENVPN_USER=\",\n      \"OPENVPN_PASSWORD=\",\n      \"OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user\",\n      \"OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password\",\n      \"OPENVPN_VERSION=2.5\",\n      \"OPENVPN_VERBOSITY=1\",\n      \"OPENVPN_FLAGS=\",\n      \"OPENVPN_CIPHERS=\",\n      \"OPENVPN_AUTH=\",\n      \"OPENVPN_PROCESS_USER=root\",\n      \"OPENVPN_CUSTOM_CONFIG=\",\n      \"WIREGUARD_PRIVATE_KEY=\",\n      \"WIREGUARD_PRESHARED_KEY=\",\n      \"WIREGUARD_PUBLIC_KEY=\",\n      \"WIREGUARD_ALLOWED_IPS=\",\n      \"WIREGUARD_ADDRESSES=\",\n      \"WIREGUARD_MTU=1400\",\n      \"WIREGUARD_IMPLEMENTATION=auto\",\n      \"SERVER_REGIONS=\",\n      \"SERVER_COUNTRIES=\",\n      \"SERVER_CITIES=\",\n      \"SERVER_HOSTNAMES=\",\n      \"ISP=\",\n      \"OWNED_ONLY=no\",\n      \"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=\",\n      \"VPN_PORT_FORWARDING=off\",\n      \"VPN_PORT_FORWARDING_PROVIDER=\",\n      \"VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port\",\n      \"OPENVPN_CERT=\",\n      \"OPENVPN_KEY=\",\n      \"OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt\",\n      \"OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey\",\n      \"OPENVPN_ENCRYPTED_KEY=\",\n      \"OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key\",\n      \"OPENVPN_KEY_PASSPHRASE=\",\n      \"OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase\",\n      \"SERVER_NUMBER=\",\n      \"SERVER_NAMES=\",\n      \"FREE_ONLY=\",\n      \"MULTIHOP_ONLY=\",\n      \"PREMIUM_ONLY=\",\n      \"FIREWALL=on\",\n      \"FIREWALL_VPN_INPUT_PORTS=\",\n      \"FIREWALL_INPUT_PORTS=\",\n      \"FIREWALL_OUTBOUND_SUBNETS=\",\n      \"FIREWALL_DEBUG=off\",\n      \"LOG_LEVEL=info\",\n      \"HEALTH_SERVER_ADDRESS=127.0.0.1:9999\",\n      \"HEALTH_TARGET_ADDRESS=cloudflare.com:443\",\n      \"HEALTH_SUCCESS_WAIT_DURATION=5s\",\n      \"HEALTH_VPN_DURATION_INITIAL=6s\",\n      \"HEALTH_VPN_DURATION_ADDITION=5s\",\n      \"DOT=on\",\n      \"DOT_PROVIDERS=cloudflare\",\n      \"DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112\",\n      \"DOT_VERBOSITY=1\",\n      \"DOT_VERBOSITY_DETAILS=0\",\n      \"DOT_VALIDATION_LOGLEVEL=0\",\n      \"DOT_CACHING=on\",\n      \"DOT_IPV6=off\",\n      \"BLOCK_MALICIOUS=on\",\n      \"BLOCK_SURVEILLANCE=off\",\n      \"BLOCK_ADS=off\",\n      \"UNBLOCK=\",\n      \"DNS_UPDATE_PERIOD=24h\",\n      \"DNS_ADDRESS=127.0.0.1\",\n      \"DNS_KEEP_NAMESERVER=off\",\n      \"HTTPPROXY=\",\n      \"HTTPPROXY_LOG=off\",\n      \"HTTPPROXY_LISTENING_ADDRESS=:8888\",\n      \"HTTPPROXY_STEALTH=off\",\n      \"HTTPPROXY_USER=\",\n      \"HTTPPROXY_PASSWORD=\",\n      \"HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user\",\n      \"HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password\",\n      \"SHADOWSOCKS=off\",\n      \"SHADOWSOCKS_LOG=off\",\n      \"SHADOWSOCKS_LISTENING_ADDRESS=:8388\",\n      \"SHADOWSOCKS_PASSWORD=\",\n      \"SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password\",\n      \"SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305\",\n      \"HTTP_CONTROL_SERVER_LOG=on\",\n      \"HTTP_CONTROL_SERVER_ADDRESS=:8000\",\n      \"UPDATER_PERIOD=0\",\n      \"UPDATER_MIN_RATIO=0.8\",\n      \"UPDATER_VPN_SERVICE_PROVIDERS=\",\n      \"PUBLICIP_FILE=/tmp/gluetun/ip\",\n      \"PUBLICIP_PERIOD=12h\",\n      \"PPROF_ENABLED=no\",\n      \"PPROF_BLOCK_PROFILE_RATE=0\",\n      \"PPROF_MUTEX_PROFILE_RATE=0\",\n      \"PPROF_HTTP_SERVER_ADDRESS=:6060\",\n      \"VERSION_INFORMATION=on\",\n      \"TZ=\",\n      \"PUID=\",\n      \"PGID=\"\n    ],\n    \"Cmd\": null,\n    \"Healthcheck\": {\n      \"Test\": [\n        \"CMD-SHELL\",\n        \"/gluetun-entrypoint healthcheck\"\n      ],\n      \"Interval\": 5000000000,\n      \"Timeout\": 5000000000,\n      \"StartPeriod\": 10000000000,\n      \"Retries\": 1\n    },\n    \"Image\": \"\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/gluetun-entrypoint\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"org.opencontainers.image.authors\": \"quentin.mcgaw@gmail.com\",\n      \"org.opencontainers.image.created\": \"2023-07-22T16:07:05.641Z\",\n      \"org.opencontainers.image.description\": \"VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.\",\n      \"org.opencontainers.image.documentation\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.licenses\": \"MIT\",\n      \"org.opencontainers.image.revision\": \"eecfb3952f202c0de3867d88e96d80c6b0f48359\",\n      \"org.opencontainers.image.source\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.title\": \"gluetun\",\n      \"org.opencontainers.image.url\": \"https://github.com/qdm12/gluetun\",\n      \"org.opencontainers.image.version\": \"latest\"\n    }\n  },\n  \"Architecture\": \"amd64\",\n  \"Os\": \"linux\",\n  \"Size\": 42602255,\n  \"VirtualSize\": 42602255,\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"RootFS\": {\n    \"Type\": \"layers\",\n    \"Layers\": [\n      \"sha256:78a822fe2a2d2c84f3de4a403188c45f623017d6a4521d23047c9fbb0801794c\",\n      \"sha256:122dbeefc08382d88b3fe57ad81c1e2428af5b81c172d112723a33e2a20fe880\",\n      \"sha256:3d215e55b88a99dcd7cf4349618326ab129771e12fdf6c6ef5cbb71a265dbb6c\"\n    ]\n  },\n  \"Metadata\": {\n    \"LastTagTime\": \"0001-01-01T00:00:00Z\"\n  }\n}\n"
  },
  {
    "path": "pkg/container/mocks/data/image_running.json",
    "content": "{\n  \"Id\": \"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa\",\n  \"RepoTags\": [\n    \"containrrr/watchtower:latest\"\n  ],\n  \"RepoDigests\": [],\n  \"Parent\": \"sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447\",\n  \"Comment\": \"\",\n  \"Created\": \"2019-04-10T19:49:07.970840451Z\",\n  \"Container\": \"b8387976426946f5c5191255204a66514c5e64be157f792c5bac329bb055041c\",\n  \"ContainerConfig\": {\n    \"Hostname\": \"b83879764269\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": [\n      \"/bin/sh\",\n      \"-c\",\n      \"#(nop) \",\n      \"ENTRYPOINT [\\\"/watchtower\\\"]\"\n    ],\n    \"Image\": \"sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/watchtower\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    }\n  },\n  \"DockerVersion\": \"18.09.1\",\n  \"Author\": \"\",\n  \"Config\": {\n    \"Hostname\": \"\",\n    \"Domainname\": \"\",\n    \"User\": \"\",\n    \"AttachStdin\": false,\n    \"AttachStdout\": false,\n    \"AttachStderr\": false,\n    \"Tty\": false,\n    \"OpenStdin\": false,\n    \"StdinOnce\": false,\n    \"Env\": [\n      \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n    ],\n    \"Cmd\": null,\n    \"Image\": \"sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447\",\n    \"Volumes\": null,\n    \"WorkingDir\": \"\",\n    \"Entrypoint\": [\n      \"/watchtower\"\n    ],\n    \"OnBuild\": null,\n    \"Labels\": {\n      \"com.centurylinklabs.watchtower\": \"true\"\n    }\n  },\n  \"Architecture\": \"amd64\",\n  \"Os\": \"linux\",\n  \"Size\": 13005733,\n  \"VirtualSize\": 13005733,\n  \"GraphDriver\": {\n    \"Data\": {\n      \"LowerDir\": \"/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff\",\n      \"MergedDir\": \"/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/merged\",\n      \"UpperDir\": \"/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff\",\n      \"WorkDir\": \"/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/work\"\n    },\n    \"Name\": \"overlay2\"\n  },\n  \"RootFS\": {\n    \"Type\": \"layers\",\n    \"Layers\": [\n      \"sha256:1d3ad125af2c636cdd793fcf94c9d4fd2b5c4c7d63a770a01056719db13c2271\",\n      \"sha256:06cfe8fe0892ba4a91cb93e3a25344d4a1c4771cf7297a93e3bd86a1e0fba6eb\",\n      \"sha256:f58d451769dc30a938d8dcae22fda2acd816899f65fc6b6fa519ddf230dab447\"\n    ]\n  },\n  \"Metadata\": {\n    \"LastTagTime\": \"2019-04-10T19:49:08.03921105Z\"\n  }\n}\n"
  },
  {
    "path": "pkg/container/util_test.go",
    "content": "package container_test\n\nimport (\n\twt \"github.com/containrrr/watchtower/pkg/types\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"container utils\", func() {\n\tDescribe(\"ShortID\", func() {\n\t\tWhen(\"given a normal image ID\", func() {\n\t\t\tWhen(\"it contains a sha256 prefix\", func() {\n\t\t\t\tIt(\"should return that ID in short version\", func() {\n\t\t\t\t\tactual := shortID(\"sha256:0123456789abcd00000000001111111111222222222233333333334444444444\")\n\t\t\t\t\tExpect(actual).To(Equal(\"0123456789ab\"))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"it doesn't contain a prefix\", func() {\n\t\t\t\tIt(\"should return that ID in short version\", func() {\n\t\t\t\t\tactual := shortID(\"0123456789abcd00000000001111111111222222222233333333334444444444\")\n\t\t\t\t\tExpect(actual).To(Equal(\"0123456789ab\"))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t\tWhen(\"given a short image ID\", func() {\n\t\t\tWhen(\"it contains no prefix\", func() {\n\t\t\t\tIt(\"should return the same string\", func() {\n\t\t\t\t\tExpect(shortID(\"0123456789ab\")).To(Equal(\"0123456789ab\"))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"it contains a the sha256 prefix\", func() {\n\t\t\t\tIt(\"should return the ID without the prefix\", func() {\n\t\t\t\t\tExpect(shortID(\"sha256:0123456789ab\")).To(Equal(\"0123456789ab\"))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t\tWhen(\"given an ID with an unknown prefix\", func() {\n\t\t\tIt(\"should return a short version of that ID including the prefix\", func() {\n\t\t\t\tExpect(shortID(\"md5:0123456789ab\")).To(Equal(\"md5:0123456789ab\"))\n\t\t\t\tExpect(shortID(\"md5:0123456789abcdefg\")).To(Equal(\"md5:0123456789ab\"))\n\t\t\t\tExpect(shortID(\"md5:01\")).To(Equal(\"md5:01\"))\n\t\t\t})\n\t\t})\n\t})\n})\n\nfunc shortID(id string) string {\n\t// Proxy to the types implementation, relocated due to package dependency resolution\n\treturn wt.ImageID(id).ShortID()\n}\n"
  },
  {
    "path": "pkg/filters/filters.go",
    "content": "package filters\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// WatchtowerContainersFilter filters only watchtower containers\nfunc WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }\n\n// NoFilter will not filter out any containers\nfunc NoFilter(t.FilterableContainer) bool { return true }\n\n// FilterByNames returns all containers that match one of the specified names\nfunc FilterByNames(names []string, baseFilter t.Filter) t.Filter {\n\tif len(names) == 0 {\n\t\treturn baseFilter\n\t}\n\n\treturn func(c t.FilterableContainer) bool {\n\t\tfor _, name := range names {\n\t\t\tif name == c.Name() || name == c.Name()[1:] {\n\t\t\t\treturn baseFilter(c)\n\t\t\t}\n\n\t\t\tif re, err := regexp.Compile(name); err == nil {\n\t\t\t\tindices := re.FindStringIndex(c.Name())\n\t\t\t\tif indices == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tstart := indices[0]\n\t\t\t\tend := indices[1]\n\t\t\t\tif start <= 1 && end >= len(c.Name())-1 {\n\t\t\t\t\treturn baseFilter(c)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n}\n\n// FilterByDisableNames returns all containers that don't match any of the specified names\nfunc FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {\n\tif len(disableNames) == 0 {\n\t\treturn baseFilter\n\t}\n\n\treturn func(c t.FilterableContainer) bool {\n\t\tfor _, name := range disableNames {\n\t\t\tif name == c.Name() || name == c.Name()[1:] {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn baseFilter(c)\n\t}\n}\n\n// FilterByEnableLabel returns all containers that have the enabled label set\nfunc FilterByEnableLabel(baseFilter t.Filter) t.Filter {\n\treturn func(c t.FilterableContainer) bool {\n\t\t// If label filtering is enabled, containers should only be considered\n\t\t// if the label is specifically set.\n\t\t_, ok := c.Enabled()\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\treturn baseFilter(c)\n\t}\n}\n\n// FilterByDisabledLabel returns all containers that have the enabled label set to disable\nfunc FilterByDisabledLabel(baseFilter t.Filter) t.Filter {\n\treturn func(c t.FilterableContainer) bool {\n\t\tenabledLabel, ok := c.Enabled()\n\t\tif ok && !enabledLabel {\n\t\t\t// If the label has been set and it demands a disable\n\t\t\treturn false\n\t\t}\n\n\t\treturn baseFilter(c)\n\t}\n}\n\n// FilterByScope returns all containers that belongs to a specific scope\nfunc FilterByScope(scope string, baseFilter t.Filter) t.Filter {\n\treturn func(c t.FilterableContainer) bool {\n\t\tcontainerScope, containerHasScope := c.Scope()\n\n\t\tif !containerHasScope || containerScope == \"\" {\n\t\t\tcontainerScope = \"none\"\n\t\t}\n\n\t\tif containerScope == scope {\n\t\t\treturn baseFilter(c)\n\t\t}\n\n\t\treturn false\n\t}\n}\n\n// FilterByImage returns all containers that have a specific image\nfunc FilterByImage(images []string, baseFilter t.Filter) t.Filter {\n\tif images == nil {\n\t\treturn baseFilter\n\t}\n\n\treturn func(c t.FilterableContainer) bool {\n\t\timage := strings.Split(c.ImageName(), \":\")[0]\n\t\tfor _, targetImage := range images {\n\t\t\tif image == targetImage {\n\t\t\t\treturn baseFilter(c)\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n}\n\n// BuildFilter creates the needed filter of containers\nfunc BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {\n\tsb := strings.Builder{}\n\tfilter := NoFilter\n\tfilter = FilterByNames(names, filter)\n\tfilter = FilterByDisableNames(disableNames, filter)\n\n\tif len(names) > 0 {\n\t\tsb.WriteString(\"which name matches \\\"\")\n\t\tfor i, n := range names {\n\t\t\tsb.WriteString(n)\n\t\t\tif i < len(names)-1 {\n\t\t\t\tsb.WriteString(`\" or \"`)\n\t\t\t}\n\t\t}\n\t\tsb.WriteString(`\", `)\n\t}\n\tif len(disableNames) > 0 {\n\t\tsb.WriteString(\"not named one of \\\"\")\n\t\tfor i, n := range disableNames {\n\t\t\tsb.WriteString(n)\n\t\t\tif i < len(disableNames)-1 {\n\t\t\t\tsb.WriteString(`\" or \"`)\n\t\t\t}\n\t\t}\n\t\tsb.WriteString(`\", `)\n\t}\n\n\tif enableLabel {\n\t\t// If label filtering is enabled, containers should only be considered\n\t\t// if the label is specifically set.\n\t\tfilter = FilterByEnableLabel(filter)\n\t\tsb.WriteString(\"using enable label, \")\n\t}\n\n\tif scope == \"none\" {\n\t\t// If a scope has explicitly defined as \"none\", containers should only be considered\n\t\t// if they do not have a scope defined, or if it's explicitly set to \"none\".\n\t\tfilter = FilterByScope(scope, filter)\n\t\tsb.WriteString(`without a scope, \"`)\n\t} else if scope != \"\" {\n\t\t// If a scope has been defined, containers should only be considered\n\t\t// if the scope is specifically set.\n\t\tfilter = FilterByScope(scope, filter)\n\t\tsb.WriteString(`in scope \"`)\n\t\tsb.WriteString(scope)\n\t\tsb.WriteString(`\", `)\n\t}\n\tfilter = FilterByDisabledLabel(filter)\n\n\tfilterDesc := \"Checking all containers (except explicitly disabled with label)\"\n\tif sb.Len() > 0 {\n\t\tfilterDesc = \"Only checking containers \" + sb.String()\n\n\t\t// Remove the last \", \"\n\t\tfilterDesc = filterDesc[:len(filterDesc)-2]\n\t}\n\n\treturn filter, filterDesc\n}\n"
  },
  {
    "path": "pkg/filters/filters_test.go",
    "content": "package filters\n\nimport (\n\t\"testing\"\n\n\t\"github.com/containrrr/watchtower/pkg/container/mocks\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWatchtowerContainersFilter(t *testing.T) {\n\tcontainer := new(mocks.FilterableContainer)\n\n\tcontainer.On(\"IsWatchtower\").Return(true)\n\n\tassert.True(t, WatchtowerContainersFilter(container))\n\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestNoFilter(t *testing.T) {\n\tcontainer := new(mocks.FilterableContainer)\n\n\tassert.True(t, NoFilter(container))\n\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByNames(t *testing.T) {\n\tvar names []string\n\n\tfilter := FilterByNames(names, nil)\n\tassert.Nil(t, filter)\n\n\tnames = append(names, \"test\")\n\n\tfilter = FilterByNames(names, NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"test\")\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"NoTest\")\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByNamesRegex(t *testing.T) {\n\tnames := []string{`ba(b|ll)oon`}\n\n\tfilter := FilterByNames(names, NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"balloon\")\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"spoon\")\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"baboonious\")\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByEnableLabel(t *testing.T) {\n\tfilter := FilterByEnableLabel(NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByScope(t *testing.T) {\n\tscope := \"testscope\"\n\n\tfilter := FilterByScope(scope, NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"testscope\", true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"nottestscope\", true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"\", false)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByNoneScope(t *testing.T) {\n\tscope := \"none\"\n\n\tfilter := FilterByScope(scope, NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"anyscope\", true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"\", false)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"\", true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Scope\").Return(\"none\", true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestBuildFilterNoneScope(t *testing.T) {\n\tfilter, desc := BuildFilter(nil, nil, false, \"none\")\n\n\tassert.Contains(t, desc, \"without a scope\")\n\n\tscoped := new(mocks.FilterableContainer)\n\tscoped.On(\"Enabled\").Return(false, false)\n\tscoped.On(\"Scope\").Return(\"anyscope\", true)\n\n\tunscoped := new(mocks.FilterableContainer)\n\tunscoped.On(\"Enabled\").Return(false, false)\n\tunscoped.On(\"Scope\").Return(\"\", false)\n\n\tassert.False(t, filter(scoped))\n\tassert.True(t, filter(unscoped))\n\n\tscoped.AssertExpectations(t)\n\tunscoped.AssertExpectations(t)\n}\n\nfunc TestFilterByDisabledLabel(t *testing.T) {\n\tfilter := FilterByDisabledLabel(NoFilter)\n\tassert.NotNil(t, filter)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestFilterByImage(t *testing.T) {\n\tfilterEmpty := FilterByImage(nil, NoFilter)\n\tfilterSingle := FilterByImage([]string{\"registry\"}, NoFilter)\n\tfilterMultiple := FilterByImage([]string{\"registry\", \"bla\"}, NoFilter)\n\tassert.NotNil(t, filterSingle)\n\tassert.NotNil(t, filterMultiple)\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"ImageName\").Return(\"registry:2\")\n\tassert.True(t, filterEmpty(container))\n\tassert.True(t, filterSingle(container))\n\tassert.True(t, filterMultiple(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"ImageName\").Return(\"registry:latest\")\n\tassert.True(t, filterEmpty(container))\n\tassert.True(t, filterSingle(container))\n\tassert.True(t, filterMultiple(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"ImageName\").Return(\"abcdef1234\")\n\tassert.True(t, filterEmpty(container))\n\tassert.False(t, filterSingle(container))\n\tassert.False(t, filterMultiple(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"ImageName\").Return(\"bla:latest\")\n\tassert.True(t, filterEmpty(container))\n\tassert.False(t, filterSingle(container))\n\tassert.True(t, filterMultiple(container))\n\tcontainer.AssertExpectations(t)\n\n}\n\nfunc TestBuildFilter(t *testing.T) {\n\tnames := []string{\"test\", \"valid\"}\n\n\tfilter, desc := BuildFilter(names, []string{}, false, \"\")\n\tassert.Contains(t, desc, \"test\")\n\tassert.Contains(t, desc, \"or\")\n\tassert.Contains(t, desc, \"valid\")\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"Invalid\")\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"test\")\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"Invalid\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"test\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestBuildFilterEnableLabel(t *testing.T) {\n\tvar names []string\n\tnames = append(names, \"test\")\n\n\tfilter, desc := BuildFilter(names, []string{}, true, \"\")\n\tassert.Contains(t, desc, \"using enable label\")\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"Invalid\")\n\tcontainer.On(\"Enabled\").Twice().Return(true, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"test\")\n\tcontainer.On(\"Enabled\").Twice().Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n\nfunc TestBuildFilterDisableContainer(t *testing.T) {\n\tfilter, desc := BuildFilter([]string{}, []string{\"excluded\", \"notfound\"}, false, \"\")\n\tassert.Contains(t, desc, \"not named\")\n\tassert.Contains(t, desc, \"excluded\")\n\tassert.Contains(t, desc, \"or\")\n\tassert.Contains(t, desc, \"notfound\")\n\n\tcontainer := new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"Another\")\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"AnotherOne\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"test\")\n\tcontainer.On(\"Enabled\").Return(false, false)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"excluded\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"excludedAsSubstring\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.True(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Name\").Return(\"notfound\")\n\tcontainer.On(\"Enabled\").Return(true, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n\n\tcontainer = new(mocks.FilterableContainer)\n\tcontainer.On(\"Enabled\").Return(false, true)\n\tassert.False(t, filter(container))\n\tcontainer.AssertExpectations(t)\n}\n"
  },
  {
    "path": "pkg/lifecycle/lifecycle.go",
    "content": "package lifecycle\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/container\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// ExecutePreChecks tries to run the pre-check lifecycle hook for all containers included by the current filter.\nfunc ExecutePreChecks(client container.Client, params types.UpdateParams) {\n\tcontainers, err := client.ListContainers(params.Filter)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, currentContainer := range containers {\n\t\tExecutePreCheckCommand(client, currentContainer)\n\t}\n}\n\n// ExecutePostChecks tries to run the post-check lifecycle hook for all containers included by the current filter.\nfunc ExecutePostChecks(client container.Client, params types.UpdateParams) {\n\tcontainers, err := client.ListContainers(params.Filter)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor _, currentContainer := range containers {\n\t\tExecutePostCheckCommand(client, currentContainer)\n\t}\n}\n\n// ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container.\nfunc ExecutePreCheckCommand(client container.Client, container types.Container) {\n\tclog := log.WithField(\"container\", container.Name())\n\tcommand := container.GetLifecyclePreCheckCommand()\n\tif len(command) == 0 {\n\t\tclog.Debug(\"No pre-check command supplied. Skipping\")\n\t\treturn\n\t}\n\n\tclog.Debug(\"Executing pre-check command.\")\n\t_, err := client.ExecuteCommand(container.ID(), command, 1)\n\tif err != nil {\n\t\tclog.Error(err)\n\t}\n}\n\n// ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container.\nfunc ExecutePostCheckCommand(client container.Client, container types.Container) {\n\tclog := log.WithField(\"container\", container.Name())\n\tcommand := container.GetLifecyclePostCheckCommand()\n\tif len(command) == 0 {\n\t\tclog.Debug(\"No post-check command supplied. Skipping\")\n\t\treturn\n\t}\n\n\tclog.Debug(\"Executing post-check command.\")\n\t_, err := client.ExecuteCommand(container.ID(), command, 1)\n\tif err != nil {\n\t\tclog.Error(err)\n\t}\n}\n\n// ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.\nfunc ExecutePreUpdateCommand(client container.Client, container types.Container) (SkipUpdate bool, err error) {\n\ttimeout := container.PreUpdateTimeout()\n\tcommand := container.GetLifecyclePreUpdateCommand()\n\tclog := log.WithField(\"container\", container.Name())\n\n\tif len(command) == 0 {\n\t\tclog.Debug(\"No pre-update command supplied. Skipping\")\n\t\treturn false, nil\n\t}\n\n\tif !container.IsRunning() || container.IsRestarting() {\n\t\tclog.Debug(\"Container is not running. Skipping pre-update command.\")\n\t\treturn false, nil\n\t}\n\n\tclog.Debug(\"Executing pre-update command.\")\n\treturn client.ExecuteCommand(container.ID(), command, timeout)\n}\n\n// ExecutePostUpdateCommand tries to run the post-update lifecycle hook for a single container.\nfunc ExecutePostUpdateCommand(client container.Client, newContainerID types.ContainerID) {\n\tnewContainer, err := client.GetContainer(newContainerID)\n\ttimeout := newContainer.PostUpdateTimeout()\n\n\tif err != nil {\n\t\tlog.WithField(\"containerID\", newContainerID.ShortID()).Error(err)\n\t\treturn\n\t}\n\tclog := log.WithField(\"container\", newContainer.Name())\n\n\tcommand := newContainer.GetLifecyclePostUpdateCommand()\n\tif len(command) == 0 {\n\t\tclog.Debug(\"No post-update command supplied. Skipping\")\n\t\treturn\n\t}\n\n\tclog.Debug(\"Executing post-update command.\")\n\t_, err = client.ExecuteCommand(newContainerID, command, timeout)\n\n\tif err != nil {\n\t\tclog.Error(err)\n\t}\n}\n"
  },
  {
    "path": "pkg/metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar metrics *Metrics\n\n// Metric is the data points of a single scan\ntype Metric struct {\n\tScanned int\n\tUpdated int\n\tFailed  int\n}\n\n// Metrics is the handler processing all individual scan metrics\ntype Metrics struct {\n\tchannel chan *Metric\n\tscanned prometheus.Gauge\n\tupdated prometheus.Gauge\n\tfailed  prometheus.Gauge\n\ttotal   prometheus.Counter\n\tskipped prometheus.Counter\n}\n\n// NewMetric returns a Metric with the counts taken from the appropriate types.Report fields\nfunc NewMetric(report types.Report) *Metric {\n\treturn &Metric{\n\t\tScanned: len(report.Scanned()),\n\t\t// Note: This is for backwards compatibility. ideally, stale containers should be counted separately\n\t\tUpdated: len(report.Updated()) + len(report.Stale()),\n\t\tFailed:  len(report.Failed()),\n\t}\n}\n\n// QueueIsEmpty checks whether any messages are enqueued in the channel\nfunc (metrics *Metrics) QueueIsEmpty() bool {\n\treturn len(metrics.channel) == 0\n}\n\n// Register registers metrics for an executed scan\nfunc (metrics *Metrics) Register(metric *Metric) {\n\tmetrics.channel <- metric\n}\n\n// Default creates a new metrics handler if none exists, otherwise returns the existing one\nfunc Default() *Metrics {\n\tif metrics != nil {\n\t\treturn metrics\n\t}\n\n\tmetrics = &Metrics{\n\t\tscanned: promauto.NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"watchtower_containers_scanned\",\n\t\t\tHelp: \"Number of containers scanned for changes by watchtower during the last scan\",\n\t\t}),\n\t\tupdated: promauto.NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"watchtower_containers_updated\",\n\t\t\tHelp: \"Number of containers updated by watchtower during the last scan\",\n\t\t}),\n\t\tfailed: promauto.NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"watchtower_containers_failed\",\n\t\t\tHelp: \"Number of containers where update failed during the last scan\",\n\t\t}),\n\t\ttotal: promauto.NewCounter(prometheus.CounterOpts{\n\t\t\tName: \"watchtower_scans_total\",\n\t\t\tHelp: \"Number of scans since the watchtower started\",\n\t\t}),\n\t\tskipped: promauto.NewCounter(prometheus.CounterOpts{\n\t\t\tName: \"watchtower_scans_skipped\",\n\t\t\tHelp: \"Number of skipped scans since watchtower started\",\n\t\t}),\n\t\tchannel: make(chan *Metric, 10),\n\t}\n\n\tgo metrics.HandleUpdate(metrics.channel)\n\n\treturn metrics\n}\n\n// RegisterScan fetches a metric handler and enqueues a metric\nfunc RegisterScan(metric *Metric) {\n\tmetrics := Default()\n\tmetrics.Register(metric)\n}\n\n// HandleUpdate dequeue the metric channel and processes it\nfunc (metrics *Metrics) HandleUpdate(channel <-chan *Metric) {\n\tfor change := range channel {\n\t\tif change == nil {\n\t\t\t// Update was skipped and rescheduled\n\t\t\tmetrics.total.Inc()\n\t\t\tmetrics.skipped.Inc()\n\t\t\tmetrics.scanned.Set(0)\n\t\t\tmetrics.updated.Set(0)\n\t\t\tmetrics.failed.Set(0)\n\t\t\tcontinue\n\t\t}\n\t\t// Update metrics with the new values\n\t\tmetrics.total.Inc()\n\t\tmetrics.scanned.Set(float64(change.Scanned))\n\t\tmetrics.updated.Set(float64(change.Updated))\n\t\tmetrics.failed.Set(float64(change.Failed))\n\t}\n}\n"
  },
  {
    "path": "pkg/notifications/common_templates.go",
    "content": "package notifications\n\nvar commonTemplates = map[string]string{\n\t`default-legacy`: \"{{range .}}{{.Message}}{{println}}{{end}}\",\n\n\t`default`: `\n{{- if .Report -}}\n  {{- with .Report -}}\n    {{- if ( or .Updated .Failed ) -}}\n{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed\n      {{- range .Updated}}\n- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}\n      {{- end -}}\n      {{- range .Fresh}}\n- {{.Name}} ({{.ImageName}}): {{.State}}\n\t  {{- end -}}\n\t  {{- range .Skipped}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n\t  {{- end -}}\n\t  {{- range .Failed}}\n- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}\n\t  {{- end -}}\n    {{- end -}}\n  {{- end -}}\n{{- else -}}\n  {{range .Entries -}}{{.Message}}{{\"\\n\"}}{{- end -}}\n{{- end -}}`,\n\n\t`porcelain.v1.summary-no-log`: `\n{{- if .Report -}}\n  {{- range .Report.All }}\n    {{- .Name}} ({{.ImageName}}): {{.State -}}\n    {{- with .Error}} Error: {{.}}{{end}}{{ println }}\n  {{- else -}}\n    no containers matched filter\n  {{- end -}}\n{{- end -}}`,\n\n\t`json.v1`: `{{ . | ToJSON }}`,\n}\n"
  },
  {
    "path": "pkg/notifications/email.go",
    "content": "package notifications\n\nimport (\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\tshoutrrrSmtp \"github.com/containrrr/shoutrrr/pkg/services/smtp\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst (\n\temailType = \"email\"\n)\n\ntype emailTypeNotifier struct {\n\tFrom, To               string\n\tServer, User, Password string\n\tPort                   int\n\ttlsSkipVerify          bool\n\tentries                []*log.Entry\n\tdelay                  time.Duration\n}\n\nfunc newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier {\n\tflags := c.Flags()\n\n\tfrom, _ := flags.GetString(\"notification-email-from\")\n\tto, _ := flags.GetString(\"notification-email-to\")\n\tserver, _ := flags.GetString(\"notification-email-server\")\n\tuser, _ := flags.GetString(\"notification-email-server-user\")\n\tpassword, _ := flags.GetString(\"notification-email-server-password\")\n\tport, _ := flags.GetInt(\"notification-email-server-port\")\n\ttlsSkipVerify, _ := flags.GetBool(\"notification-email-server-tls-skip-verify\")\n\tdelay, _ := flags.GetInt(\"notification-email-delay\")\n\n\tn := &emailTypeNotifier{\n\t\tentries:       []*log.Entry{},\n\t\tFrom:          from,\n\t\tTo:            to,\n\t\tServer:        server,\n\t\tUser:          user,\n\t\tPassword:      password,\n\t\tPort:          port,\n\t\ttlsSkipVerify: tlsSkipVerify,\n\t\tdelay:         time.Duration(delay) * time.Second,\n\t}\n\n\treturn n\n}\n\nfunc (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {\n\tconf := &shoutrrrSmtp.Config{\n\t\tFromAddress: e.From,\n\t\tFromName:    \"Watchtower\",\n\t\tToAddresses: []string{e.To},\n\t\tPort:        uint16(e.Port),\n\t\tHost:        e.Server,\n\t\tUsername:    e.User,\n\t\tPassword:    e.Password,\n\t\tUseStartTLS: !e.tlsSkipVerify,\n\t\tUseHTML:     false,\n\t\tEncryption:  shoutrrrSmtp.EncMethods.Auto,\n\t\tAuth:        shoutrrrSmtp.AuthTypes.None,\n\t\tClientHost:  \"localhost\",\n\t}\n\n\tif len(e.User) > 0 {\n\t\tconf.Auth = shoutrrrSmtp.AuthTypes.Plain\n\t}\n\n\tif e.tlsSkipVerify {\n\t\tconf.Encryption = shoutrrrSmtp.EncMethods.None\n\t}\n\n\treturn conf.GetURL().String(), nil\n}\n\nfunc (e *emailTypeNotifier) GetDelay() time.Duration {\n\treturn e.delay\n}\n"
  },
  {
    "path": "pkg/notifications/gotify.go",
    "content": "package notifications\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\tshoutrrrGotify \"github.com/containrrr/shoutrrr/pkg/services/gotify\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n)\n\nconst (\n\tgotifyType = \"gotify\"\n)\n\ntype gotifyTypeNotifier struct {\n\tgotifyURL                string\n\tgotifyAppToken           string\n\tgotifyInsecureSkipVerify bool\n}\n\nfunc newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier {\n\tflags := c.Flags()\n\n\tapiURL := getGotifyURL(flags)\n\ttoken := getGotifyToken(flags)\n\n\tskipVerify, _ := flags.GetBool(\"notification-gotify-tls-skip-verify\")\n\n\tn := &gotifyTypeNotifier{\n\t\tgotifyURL:                apiURL,\n\t\tgotifyAppToken:           token,\n\t\tgotifyInsecureSkipVerify: skipVerify,\n\t}\n\n\treturn n\n}\n\nfunc getGotifyToken(flags *pflag.FlagSet) string {\n\tgotifyToken, _ := flags.GetString(\"notification-gotify-token\")\n\tif len(gotifyToken) < 1 {\n\t\tlog.Fatal(\"Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.\")\n\t}\n\treturn gotifyToken\n}\n\nfunc getGotifyURL(flags *pflag.FlagSet) string {\n\tgotifyURL, _ := flags.GetString(\"notification-gotify-url\")\n\n\tif len(gotifyURL) < 1 {\n\t\tlog.Fatal(\"Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.\")\n\t} else if !(strings.HasPrefix(gotifyURL, \"http://\") || strings.HasPrefix(gotifyURL, \"https://\")) {\n\t\tlog.Fatal(\"Gotify URL must start with \\\"http://\\\" or \\\"https://\\\"\")\n\t} else if strings.HasPrefix(gotifyURL, \"http://\") {\n\t\tlog.Warn(\"Using an HTTP url for Gotify is insecure\")\n\t}\n\n\treturn gotifyURL\n}\n\nfunc (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) {\n\tapiURL, err := url.Parse(n.gotifyURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconfig := &shoutrrrGotify.Config{\n\t\tHost:       apiURL.Host,\n\t\tPath:       apiURL.Path,\n\t\tDisableTLS: apiURL.Scheme == \"http\",\n\t\tToken:      n.gotifyAppToken,\n\t}\n\n\treturn config.GetURL().String(), nil\n}\n"
  },
  {
    "path": "pkg/notifications/json.go",
    "content": "package notifications\n\nimport (\n\t\"encoding/json\"\n\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n)\n\ntype jsonMap = map[string]interface{}\n\n// MarshalJSON implements json.Marshaler\nfunc (d Data) MarshalJSON() ([]byte, error) {\n\tvar entries = make([]jsonMap, len(d.Entries))\n\tfor i, entry := range d.Entries {\n\t\tentries[i] = jsonMap{\n\t\t\t`level`:   entry.Level,\n\t\t\t`message`: entry.Message,\n\t\t\t`data`:    entry.Data,\n\t\t\t`time`:    entry.Time,\n\t\t}\n\t}\n\n\tvar report jsonMap\n\tif d.Report != nil {\n\t\treport = jsonMap{\n\t\t\t`scanned`: marshalReports(d.Report.Scanned()),\n\t\t\t`updated`: marshalReports(d.Report.Updated()),\n\t\t\t`failed`:  marshalReports(d.Report.Failed()),\n\t\t\t`skipped`: marshalReports(d.Report.Skipped()),\n\t\t\t`stale`:   marshalReports(d.Report.Stale()),\n\t\t\t`fresh`:   marshalReports(d.Report.Fresh()),\n\t\t}\n\t}\n\n\treturn json.Marshal(jsonMap{\n\t\t`report`:  report,\n\t\t`title`:   d.Title,\n\t\t`host`:    d.Host,\n\t\t`entries`: entries,\n\t})\n}\n\nfunc marshalReports(reports []t.ContainerReport) []jsonMap {\n\tjsonReports := make([]jsonMap, len(reports))\n\tfor i, report := range reports {\n\t\tjsonReports[i] = jsonMap{\n\t\t\t`id`:             report.ID().ShortID(),\n\t\t\t`name`:           report.Name(),\n\t\t\t`currentImageId`: report.CurrentImageID().ShortID(),\n\t\t\t`latestImageId`:  report.LatestImageID().ShortID(),\n\t\t\t`imageName`:      report.ImageName(),\n\t\t\t`state`:          report.State(),\n\t\t}\n\t\tif errorMessage := report.Error(); errorMessage != \"\" {\n\t\t\tjsonReports[i][`error`] = errorMessage\n\t\t}\n\t}\n\treturn jsonReports\n}\n\nvar _ json.Marshaler = &Data{}\n"
  },
  {
    "path": "pkg/notifications/json_test.go",
    "content": "package notifications\n\nimport (\n\ts \"github.com/containrrr/watchtower/pkg/session\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"JSON template\", func() {\n\tWhen(\"using report templates\", func() {\n\t\tWhen(\"JSON template is used\", func() {\n\t\t\tIt(\"should format the messages to the expected format\", func() {\n\t\t\t\texpected := `{\n\t\"entries\": [\n\t\t\t{\n\t\t\t\t\"data\": null,\n\t\t\t\t\"level\": \"info\",\n\t\t\t\t\"message\": \"foo Bar\",\n\t\t\t\t\"time\": \"0001-01-01T00:00:00Z\"\n\t\t\t}\n\t\t],\n\t\t\"host\": \"Mock\",\n\t\t\"report\": {\n\t\t\"failed\": [\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d210000000\",\n\t\t\t\t\"error\": \"accidentally the whole container\",\n\t\t\t\t\"id\": \"c79210000000\",\n\t\t\t\t\"imageName\": \"mock/fail1:latest\",\n\t\t\t\t\"latestImageId\": \"d0a210000000\",\n\t\t\t\t\"name\": \"fail1\",\n\t\t\t\t\"state\": \"Failed\"\n\t\t\t}\n\t\t],\n\t\t\"fresh\": [\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d310000000\",\n\t\t\t\t\"id\": \"c79310000000\",\n\t\t\t\t\"imageName\": \"mock/frsh1:latest\",\n\t\t\t\t\"latestImageId\": \"01d310000000\",\n\t\t\t\t\"name\": \"frsh1\",\n\t\t\t\t\"state\": \"Fresh\"\n\t\t\t}\n\t\t],\n\t\t\"scanned\": [\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d110000000\",\n\t\t\t\t\"id\": \"c79110000000\",\n\t\t\t\t\"imageName\": \"mock/updt1:latest\",\n\t\t\t\t\"latestImageId\": \"d0a110000000\",\n\t\t\t\t\"name\": \"updt1\",\n\t\t\t\t\"state\": \"Updated\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d120000000\",\n\t\t\t\t\"id\": \"c79120000000\",\n\t\t\t\t\"imageName\": \"mock/updt2:latest\",\n\t\t\t\t\"latestImageId\": \"d0a120000000\",\n\t\t\t\t\"name\": \"updt2\",\n\t\t\t\t\"state\": \"Updated\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d210000000\",\n\t\t\t\t\"error\": \"accidentally the whole container\",\n\t\t\t\t\"id\": \"c79210000000\",\n\t\t\t\t\"imageName\": \"mock/fail1:latest\",\n\t\t\t\t\"latestImageId\": \"d0a210000000\",\n\t\t\t\t\"name\": \"fail1\",\n\t\t\t\t\"state\": \"Failed\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d310000000\",\n\t\t\t\t\"id\": \"c79310000000\",\n\t\t\t\t\"imageName\": \"mock/frsh1:latest\",\n\t\t\t\t\"latestImageId\": \"01d310000000\",\n\t\t\t\t\"name\": \"frsh1\",\n\t\t\t\t\"state\": \"Fresh\"\n\t\t\t}\n\t\t],\n\t\t\"skipped\": [\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d410000000\",\n\t\t\t\t\"error\": \"unpossible\",\n\t\t\t\t\"id\": \"c79410000000\",\n\t\t\t\t\"imageName\": \"mock/skip1:latest\",\n\t\t\t\t\"latestImageId\": \"01d410000000\",\n\t\t\t\t\"name\": \"skip1\",\n\t\t\t\t\"state\": \"Skipped\"\n\t\t\t}\n\t\t],\n\t\t\"stale\": [],\n\t\t\"updated\": [\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d110000000\",\n\t\t\t\t\"id\": \"c79110000000\",\n\t\t\t\t\"imageName\": \"mock/updt1:latest\",\n\t\t\t\t\"latestImageId\": \"d0a110000000\",\n\t\t\t\t\"name\": \"updt1\",\n\t\t\t\t\"state\": \"Updated\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"currentImageId\": \"01d120000000\",\n\t\t\t\t\"id\": \"c79120000000\",\n\t\t\t\t\"imageName\": \"mock/updt2:latest\",\n\t\t\t\t\"latestImageId\": \"d0a120000000\",\n\t\t\t\t\"name\": \"updt2\",\n\t\t\t\t\"state\": \"Updated\"\n\t\t\t}\n\t\t]\n\t\t},\n\t\t\"title\": \"Watchtower updates on Mock\"\n}`\n\t\t\t\tdata := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)\n\t\t\t\tExpect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected))\n\t\t\t})\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "pkg/notifications/model.go",
    "content": "package notifications\n\nimport (\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// StaticData is the part of the notification template data model set upon initialization\ntype StaticData struct {\n\tTitle string\n\tHost  string\n}\n\n// Data is the notification template data model\ntype Data struct {\n\tStaticData\n\tEntries []*log.Entry\n\tReport  t.Report\n}\n"
  },
  {
    "path": "pkg/notifications/msteams.go",
    "content": "package notifications\n\nimport (\n\t\"net/url\"\n\n\tshoutrrrTeams \"github.com/containrrr/shoutrrr/pkg/services/teams\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tmsTeamsType = \"msteams\"\n)\n\ntype msTeamsTypeNotifier struct {\n\twebHookURL string\n\tdata       bool\n}\n\nfunc newMsTeamsNotifier(cmd *cobra.Command) t.ConvertibleNotifier {\n\n\tflags := cmd.Flags()\n\n\twebHookURL, _ := flags.GetString(\"notification-msteams-hook\")\n\tif len(webHookURL) <= 0 {\n\t\tlog.Fatal(\"Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.\")\n\t}\n\n\twithData, _ := flags.GetBool(\"notification-msteams-data\")\n\tn := &msTeamsTypeNotifier{\n\t\twebHookURL: webHookURL,\n\t\tdata:       withData,\n\t}\n\n\treturn n\n}\n\nfunc (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) {\n\twebhookURL, err := url.Parse(n.webHookURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconfig, err := shoutrrrTeams.ConfigFromWebhookURL(*webhookURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconfig.Color = ColorHex\n\n\treturn config.GetURL().String(), nil\n}\n"
  },
  {
    "path": "pkg/notifications/notifications_suite_test.go",
    "content": "package notifications_test\n\nimport (\n\t\"github.com/onsi/gomega/format\"\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestNotifications(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tformat.CharactersAroundMismatchToInclude = 20\n\tRunSpecs(t, \"Notifications Suite\")\n}\n"
  },
  {
    "path": "pkg/notifications/notifier.go",
    "content": "package notifications\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tty \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// NewNotifier creates and returns a new Notifier, using global configuration.\nfunc NewNotifier(c *cobra.Command) ty.Notifier {\n\tf := c.Flags()\n\n\tlevel, _ := f.GetString(\"notifications-level\")\n\tlogLevel, err := log.ParseLevel(level)\n\tif err != nil {\n\t\tlog.Fatalf(\"Notifications invalid log level: %s\", err.Error())\n\t}\n\n\treportTemplate, _ := f.GetBool(\"notification-report\")\n\tstdout, _ := f.GetBool(\"notification-log-stdout\")\n\ttplString, _ := f.GetString(\"notification-template\")\n\turls, _ := f.GetStringArray(\"notification-url\")\n\n\tdata := GetTemplateData(c)\n\turls, delay := AppendLegacyUrls(urls, c)\n\n\treturn createNotifier(urls, logLevel, tplString, !reportTemplate, data, stdout, delay)\n}\n\n// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags\nfunc AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) {\n\n\t// Parse types and create notifiers.\n\ttypes, err := cmd.Flags().GetStringSlice(\"notifications\")\n\tif err != nil {\n\t\tlog.WithError(err).Fatal(\"could not read notifications argument\")\n\t}\n\n\tlegacyDelay := time.Duration(0)\n\n\tfor _, t := range types {\n\n\t\tvar legacyNotifier ty.ConvertibleNotifier\n\t\tvar err error\n\n\t\tswitch t {\n\t\tcase emailType:\n\t\t\tlegacyNotifier = newEmailNotifier(cmd)\n\t\tcase slackType:\n\t\t\tlegacyNotifier = newSlackNotifier(cmd)\n\t\tcase msTeamsType:\n\t\t\tlegacyNotifier = newMsTeamsNotifier(cmd)\n\t\tcase gotifyType:\n\t\t\tlegacyNotifier = newGotifyNotifier(cmd)\n\t\tcase shoutrrrType:\n\t\t\tcontinue\n\t\tdefault:\n\t\t\tlog.Fatalf(\"Unknown notification type %q\", t)\n\t\t\t// Not really needed, used for nil checking static analysis\n\t\t\tcontinue\n\t\t}\n\n\t\tshoutrrrURL, err := legacyNotifier.GetURL(cmd)\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"failed to create notification config: \", err)\n\t\t}\n\t\turls = append(urls, shoutrrrURL)\n\n\t\tif delayNotifier, ok := legacyNotifier.(ty.DelayNotifier); ok {\n\t\t\tlegacyDelay = delayNotifier.GetDelay()\n\t\t}\n\n\t\tlog.WithField(\"URL\", shoutrrrURL).Trace(\"created Shoutrrr URL from legacy notifier\")\n\t}\n\n\tdelay := GetDelay(cmd, legacyDelay)\n\treturn urls, delay\n}\n\n// GetDelay returns the legacy delay if defined, otherwise the delay as set by args is returned\nfunc GetDelay(c *cobra.Command, legacyDelay time.Duration) time.Duration {\n\tif legacyDelay > 0 {\n\t\treturn legacyDelay\n\t}\n\n\tdelay, _ := c.PersistentFlags().GetInt(\"notifications-delay\")\n\tif delay > 0 {\n\t\treturn time.Duration(delay) * time.Second\n\t}\n\treturn time.Duration(0)\n}\n\n// GetTitle formats the title based on the passed hostname and tag\nfunc GetTitle(hostname string, tag string) string {\n\ttb := strings.Builder{}\n\n\tif tag != \"\" {\n\t\ttb.WriteRune('[')\n\t\ttb.WriteString(tag)\n\t\ttb.WriteRune(']')\n\t\ttb.WriteRune(' ')\n\t}\n\n\ttb.WriteString(\"Watchtower updates\")\n\n\tif hostname != \"\" {\n\t\ttb.WriteString(\" on \")\n\t\ttb.WriteString(hostname)\n\t}\n\n\treturn tb.String()\n}\n\n// GetTemplateData populates the static notification data from flags and environment\nfunc GetTemplateData(c *cobra.Command) StaticData {\n\tf := c.PersistentFlags()\n\n\thostname, _ := f.GetString(\"notifications-hostname\")\n\tif hostname == \"\" {\n\t\thostname, _ = os.Hostname()\n\t}\n\n\ttitle := \"\"\n\tif skip, _ := f.GetBool(\"notification-skip-title\"); !skip {\n\t\ttag, _ := f.GetString(\"notification-title-tag\")\n\t\tif tag == \"\" {\n\t\t\t// For legacy email support\n\t\t\ttag, _ = f.GetString(\"notification-email-subjecttag\")\n\t\t}\n\t\ttitle = GetTitle(hostname, tag)\n\t}\n\n\treturn StaticData{\n\t\tHost:  hostname,\n\t\tTitle: title,\n\t}\n}\n\n// ColorHex is the default notification color used for services that support it (formatted as a CSS hex string)\nconst ColorHex = \"#406170\"\n\n// ColorInt is the default notification color used for services that support it (as an int value)\nconst ColorInt = 0x406170\n"
  },
  {
    "path": "pkg/notifications/notifier_test.go",
    "content": "package notifications_test\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/cmd\"\n\t\"github.com/containrrr/watchtower/internal/flags\"\n\t\"github.com/containrrr/watchtower/pkg/notifications\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"notifications\", func() {\n\tDescribe(\"the notifier\", func() {\n\t\tWhen(\"only empty notifier types are provided\", func() {\n\n\t\t\tcommand := cmd.NewRootCommand()\n\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\terr := command.ParseFlags([]string{\n\t\t\t\t\"--notifications\",\n\t\t\t\t\"shoutrrr\",\n\t\t\t})\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tnotif := notifications.NewNotifier(command)\n\n\t\t\tExpect(notif.GetNames()).To(BeEmpty())\n\t\t})\n\t\tWhen(\"title is overriden in flag\", func() {\n\t\t\tIt(\"should use the specified hostname in the title\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\terr := command.ParseFlags([]string{\n\t\t\t\t\t\"--notifications-hostname\",\n\t\t\t\t\t\"test.host\",\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tdata := notifications.GetTemplateData(command)\n\t\t\t\ttitle := data.Title\n\t\t\t\tExpect(title).To(Equal(\"Watchtower updates on test.host\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"no hostname can be resolved\", func() {\n\t\t\tIt(\"should use the default simple title\", func() {\n\t\t\t\ttitle := notifications.GetTitle(\"\", \"\")\n\t\t\t\tExpect(title).To(Equal(\"Watchtower updates\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"title tag is set\", func() {\n\t\t\tIt(\"should use the prefix in the title\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\tExpect(command.ParseFlags([]string{\n\t\t\t\t\t\"--notification-title-tag\",\n\t\t\t\t\t\"PREFIX\",\n\t\t\t\t})).To(Succeed())\n\n\t\t\t\tdata := notifications.GetTemplateData(command)\n\t\t\t\tExpect(data.Title).To(HavePrefix(\"[PREFIX]\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"legacy email tag is set\", func() {\n\t\t\tIt(\"should use the prefix in the title\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\tExpect(command.ParseFlags([]string{\n\t\t\t\t\t\"--notification-email-subjecttag\",\n\t\t\t\t\t\"PREFIX\",\n\t\t\t\t})).To(Succeed())\n\n\t\t\t\tdata := notifications.GetTemplateData(command)\n\t\t\t\tExpect(data.Title).To(HavePrefix(\"[PREFIX]\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"the skip title flag is set\", func() {\n\t\t\tIt(\"should return an empty title\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\tExpect(command.ParseFlags([]string{\n\t\t\t\t\t\"--notification-skip-title\",\n\t\t\t\t})).To(Succeed())\n\n\t\t\t\tdata := notifications.GetTemplateData(command)\n\t\t\t\tExpect(data.Title).To(BeEmpty())\n\t\t\t})\n\t\t})\n\t\tWhen(\"no delay is defined\", func() {\n\t\t\tIt(\"should use the default delay\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\tdelay := notifications.GetDelay(command, time.Duration(0))\n\t\t\t\tExpect(delay).To(Equal(time.Duration(0)))\n\t\t\t})\n\t\t})\n\t\tWhen(\"delay is defined\", func() {\n\t\t\tIt(\"should use the specified delay\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\terr := command.ParseFlags([]string{\n\t\t\t\t\t\"--notifications-delay\",\n\t\t\t\t\t\"5\",\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tdelay := notifications.GetDelay(command, time.Duration(0))\n\t\t\t\tExpect(delay).To(Equal(time.Duration(5) * time.Second))\n\t\t\t})\n\t\t})\n\t\tWhen(\"legacy delay is defined\", func() {\n\t\t\tIt(\"should use the specified legacy delay\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\t\t\t\tdelay := notifications.GetDelay(command, time.Duration(5)*time.Second)\n\t\t\t\tExpect(delay).To(Equal(time.Duration(5) * time.Second))\n\t\t\t})\n\t\t})\n\t\tWhen(\"legacy delay and delay is defined\", func() {\n\t\t\tIt(\"should use the specified legacy delay and ignore the specified delay\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\terr := command.ParseFlags([]string{\n\t\t\t\t\t\"--notifications-delay\",\n\t\t\t\t\t\"0\",\n\t\t\t\t})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tdelay := notifications.GetDelay(command, time.Duration(7)*time.Second)\n\t\t\t\tExpect(delay).To(Equal(time.Duration(7) * time.Second))\n\t\t\t})\n\t\t})\n\t})\n\tDescribe(\"the slack notifier\", func() {\n\t\t// builderFn := notifications.NewSlackNotifier\n\n\t\tWhen(\"passing a discord url to the slack notifier\", func() {\n\t\t\tcommand := cmd.NewRootCommand()\n\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\tchannel := \"123456789\"\n\t\t\ttoken := \"abvsihdbau\"\n\t\t\tcolor := notifications.ColorInt\n\t\t\tusername := \"containrrrbot\"\n\t\t\ticonURL := \"https://containrrr.dev/watchtower-sq180.png\"\n\t\t\texpected := fmt.Sprintf(\"discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=watchtower\", token, channel, color)\n\t\t\tbuildArgs := func(url string) []string {\n\t\t\t\treturn []string{\n\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\"slack\",\n\t\t\t\t\t\"--notification-slack-hook-url\",\n\t\t\t\t\turl,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tIt(\"should return a discord url when using a hook url with the domain discord.com\", func() {\n\t\t\t\thookURL := fmt.Sprintf(\"https://%s/api/webhooks/%s/%s/slack\", \"discord.com\", channel, token)\n\t\t\t\ttestURL(buildArgs(hookURL), expected, time.Duration(0))\n\t\t\t})\n\t\t\tIt(\"should return a discord url when using a hook url with the domain discordapp.com\", func() {\n\t\t\t\thookURL := fmt.Sprintf(\"https://%s/api/webhooks/%s/%s/slack\", \"discordapp.com\", channel, token)\n\t\t\t\ttestURL(buildArgs(hookURL), expected, time.Duration(0))\n\t\t\t})\n\t\t\tWhen(\"icon URL and username are specified\", func() {\n\t\t\t\tIt(\"should return the expected URL\", func() {\n\t\t\t\t\thookURL := fmt.Sprintf(\"https://%s/api/webhooks/%s/%s/slack\", \"discord.com\", channel, token)\n\t\t\t\t\texpectedOutput := fmt.Sprintf(\"discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=%s\", token, channel, url.QueryEscape(iconURL), color, username)\n\t\t\t\t\texpectedDelay := time.Duration(7) * time.Second\n\t\t\t\t\targs := []string{\n\t\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\t\"slack\",\n\t\t\t\t\t\t\"--notification-slack-hook-url\",\n\t\t\t\t\t\thookURL,\n\t\t\t\t\t\t\"--notification-slack-identifier\",\n\t\t\t\t\t\tusername,\n\t\t\t\t\t\t\"--notification-slack-icon-url\",\n\t\t\t\t\t\ticonURL,\n\t\t\t\t\t\t\"--notifications-delay\",\n\t\t\t\t\t\tfmt.Sprint(expectedDelay.Seconds()),\n\t\t\t\t\t}\n\n\t\t\t\t\ttestURL(args, expectedOutput, expectedDelay)\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t\tWhen(\"converting a slack service config into a shoutrrr url\", func() {\n\t\t\tcommand := cmd.NewRootCommand()\n\t\t\tflags.RegisterNotificationFlags(command)\n\t\t\tusername := \"containrrrbot\"\n\t\t\ttokenA := \"AAAAAAAAA\"\n\t\t\ttokenB := \"BBBBBBBBB\"\n\t\t\ttokenC := \"123456789123456789123456\"\n\t\t\tcolor := url.QueryEscape(notifications.ColorHex)\n\t\t\ticonURL := \"https://containrrr.dev/watchtower-sq180.png\"\n\t\t\ticonEmoji := \"whale\"\n\n\t\t\tWhen(\"icon URL is specified\", func() {\n\t\t\t\tIt(\"should return the expected URL\", func() {\n\n\t\t\t\t\thookURL := fmt.Sprintf(\"https://hooks.slack.com/services/%s/%s/%s\", tokenA, tokenB, tokenC)\n\t\t\t\t\texpectedOutput := fmt.Sprintf(\"slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s\", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL))\n\t\t\t\t\texpectedDelay := time.Duration(7) * time.Second\n\n\t\t\t\t\targs := []string{\n\t\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\t\"slack\",\n\t\t\t\t\t\t\"--notification-slack-hook-url\",\n\t\t\t\t\t\thookURL,\n\t\t\t\t\t\t\"--notification-slack-identifier\",\n\t\t\t\t\t\tusername,\n\t\t\t\t\t\t\"--notification-slack-icon-url\",\n\t\t\t\t\t\ticonURL,\n\t\t\t\t\t\t\"--notifications-delay\",\n\t\t\t\t\t\tfmt.Sprint(expectedDelay.Seconds()),\n\t\t\t\t\t}\n\n\t\t\t\t\ttestURL(args, expectedOutput, expectedDelay)\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tWhen(\"icon emoji is specified\", func() {\n\t\t\t\tIt(\"should return the expected URL\", func() {\n\t\t\t\t\thookURL := fmt.Sprintf(\"https://hooks.slack.com/services/%s/%s/%s\", tokenA, tokenB, tokenC)\n\t\t\t\t\texpectedOutput := fmt.Sprintf(\"slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s\", tokenA, tokenB, tokenC, username, color, iconEmoji)\n\n\t\t\t\t\targs := []string{\n\t\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\t\"slack\",\n\t\t\t\t\t\t\"--notification-slack-hook-url\",\n\t\t\t\t\t\thookURL,\n\t\t\t\t\t\t\"--notification-slack-identifier\",\n\t\t\t\t\t\tusername,\n\t\t\t\t\t\t\"--notification-slack-icon-emoji\",\n\t\t\t\t\t\ticonEmoji,\n\t\t\t\t\t}\n\n\t\t\t\t\ttestURL(args, expectedOutput, time.Duration(0))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n\n\tDescribe(\"the gotify notifier\", func() {\n\t\tWhen(\"converting a gotify service config into a shoutrrr url\", func() {\n\t\t\tIt(\"should return the expected URL\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\ttoken := \"aaa\"\n\t\t\t\thost := \"shoutrrr.local\"\n\n\t\t\t\texpectedOutput := fmt.Sprintf(\"gotify://%s/%s?title=\", host, token)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\"gotify\",\n\t\t\t\t\t\"--notification-gotify-url\",\n\t\t\t\t\tfmt.Sprintf(\"https://%s\", host),\n\t\t\t\t\t\"--notification-gotify-token\",\n\t\t\t\t\ttoken,\n\t\t\t\t}\n\n\t\t\t\ttestURL(args, expectedOutput, time.Duration(0))\n\t\t\t})\n\t\t})\n\t})\n\n\tDescribe(\"the teams notifier\", func() {\n\t\tWhen(\"converting a teams service config into a shoutrrr url\", func() {\n\t\t\tIt(\"should return the expected URL\", func() {\n\t\t\t\tcommand := cmd.NewRootCommand()\n\t\t\t\tflags.RegisterNotificationFlags(command)\n\n\t\t\t\ttokenA := \"11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc\"\n\t\t\t\ttokenB := \"33333333012222222222333333333344\"\n\t\t\t\ttokenC := \"44444444-4444-4444-8444-cccccccccccc\"\n\t\t\t\tcolor := url.QueryEscape(notifications.ColorHex)\n\n\t\t\t\thookURL := fmt.Sprintf(\"https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s\", tokenA, tokenB, tokenC)\n\t\t\t\texpectedOutput := fmt.Sprintf(\"teams://%s/%s/%s?color=%s\", tokenA, tokenB, tokenC, color)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\"msteams\",\n\t\t\t\t\t\"--notification-msteams-hook\",\n\t\t\t\t\thookURL,\n\t\t\t\t}\n\n\t\t\t\ttestURL(args, expectedOutput, time.Duration(0))\n\t\t\t})\n\t\t})\n\t})\n\n\tDescribe(\"the email notifier\", func() {\n\t\tWhen(\"converting an email service config into a shoutrrr url\", func() {\n\t\t\tIt(\"should set the from address in the URL\", func() {\n\t\t\t\tfromAddress := \"lala@example.com\"\n\t\t\t\texpectedOutput := buildExpectedURL(\"containrrrbot\", \"secret-password\", \"mail.containrrr.dev\", 25, fromAddress, \"mail@example.com\", \"Plain\")\n\t\t\t\texpectedDelay := time.Duration(7) * time.Second\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\"email\",\n\t\t\t\t\t\"--notification-email-from\",\n\t\t\t\t\tfromAddress,\n\t\t\t\t\t\"--notification-email-to\",\n\t\t\t\t\t\"mail@example.com\",\n\t\t\t\t\t\"--notification-email-server-user\",\n\t\t\t\t\t\"containrrrbot\",\n\t\t\t\t\t\"--notification-email-server-password\",\n\t\t\t\t\t\"secret-password\",\n\t\t\t\t\t\"--notification-email-server\",\n\t\t\t\t\t\"mail.containrrr.dev\",\n\t\t\t\t\t\"--notifications-delay\",\n\t\t\t\t\tfmt.Sprint(expectedDelay.Seconds()),\n\t\t\t\t}\n\t\t\t\ttestURL(args, expectedOutput, expectedDelay)\n\t\t\t})\n\n\t\t\tIt(\"should return the expected URL\", func() {\n\n\t\t\t\tfromAddress := \"sender@example.com\"\n\t\t\t\ttoAddress := \"receiver@example.com\"\n\t\t\t\texpectedOutput := buildExpectedURL(\"containrrrbot\", \"secret-password\", \"mail.containrrr.dev\", 25, fromAddress, toAddress, \"Plain\")\n\t\t\t\texpectedDelay := time.Duration(7) * time.Second\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"--notifications\",\n\t\t\t\t\t\"email\",\n\t\t\t\t\t\"--notification-email-from\",\n\t\t\t\t\tfromAddress,\n\t\t\t\t\t\"--notification-email-to\",\n\t\t\t\t\ttoAddress,\n\t\t\t\t\t\"--notification-email-server-user\",\n\t\t\t\t\t\"containrrrbot\",\n\t\t\t\t\t\"--notification-email-server-password\",\n\t\t\t\t\t\"secret-password\",\n\t\t\t\t\t\"--notification-email-server\",\n\t\t\t\t\t\"mail.containrrr.dev\",\n\t\t\t\t\t\"--notification-email-delay\",\n\t\t\t\t\tfmt.Sprint(expectedDelay.Seconds()),\n\t\t\t\t}\n\n\t\t\t\ttestURL(args, expectedOutput, expectedDelay)\n\t\t\t})\n\t\t})\n\t})\n})\n\nfunc buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string {\n\tvar template = \"smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s\"\n\treturn fmt.Sprintf(template,\n\t\turl.QueryEscape(username),\n\t\turl.QueryEscape(password),\n\t\thost, port, auth,\n\t\turl.QueryEscape(from),\n\t\turl.QueryEscape(to))\n}\n\nfunc testURL(args []string, expectedURL string, expectedDelay time.Duration) {\n\tdefer GinkgoRecover()\n\n\tcommand := cmd.NewRootCommand()\n\tflags.RegisterNotificationFlags(command)\n\n\tExpect(command.ParseFlags(args)).To(Succeed())\n\n\turls, delay := notifications.AppendLegacyUrls([]string{}, command)\n\n\tExpect(urls).To(ContainElement(expectedURL))\n\tExpect(delay).To(Equal(expectedDelay))\n}\n"
  },
  {
    "path": "pkg/notifications/preview/data/data.go",
    "content": "package data\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\ntype previewData struct {\n\trand           *rand.Rand\n\tlastTime       time.Time\n\treport         *report\n\tcontainerCount int\n\tEntries        []*logEntry\n\tStaticData     staticData\n}\n\ntype staticData struct {\n\tTitle string\n\tHost  string\n}\n\n// New initializes a new preview data struct\nfunc New() *previewData {\n\treturn &previewData{\n\t\trand:           rand.New(rand.NewSource(1)),\n\t\tlastTime:       time.Now().Add(-30 * time.Minute),\n\t\treport:         nil,\n\t\tcontainerCount: 0,\n\t\tEntries:        []*logEntry{},\n\t\tStaticData: staticData{\n\t\t\tTitle: \"Title\",\n\t\t\tHost:  \"Host\",\n\t\t},\n\t}\n}\n\n// AddFromState adds a container status entry to the report with the given state\nfunc (pb *previewData) AddFromState(state State) {\n\tcid := types.ContainerID(pb.generateID())\n\told := types.ImageID(pb.generateID())\n\tnew := types.ImageID(pb.generateID())\n\tname := pb.generateName()\n\timage := pb.generateImageName(name)\n\tvar err error\n\tif state == FailedState {\n\t\terr = errors.New(pb.randomEntry(errorMessages))\n\t} else if state == SkippedState {\n\t\terr = errors.New(pb.randomEntry(skippedMessages))\n\t}\n\tpb.addContainer(containerStatus{\n\t\tcontainerID:   cid,\n\t\toldImage:      old,\n\t\tnewImage:      new,\n\t\tcontainerName: name,\n\t\timageName:     image,\n\t\terror:         err,\n\t\tstate:         state,\n\t})\n}\n\nfunc (pb *previewData) addContainer(c containerStatus) {\n\tif pb.report == nil {\n\t\tpb.report = &report{}\n\t}\n\tswitch c.state {\n\tcase ScannedState:\n\t\tpb.report.scanned = append(pb.report.scanned, &c)\n\tcase UpdatedState:\n\t\tpb.report.updated = append(pb.report.updated, &c)\n\tcase FailedState:\n\t\tpb.report.failed = append(pb.report.failed, &c)\n\tcase SkippedState:\n\t\tpb.report.skipped = append(pb.report.skipped, &c)\n\tcase StaleState:\n\t\tpb.report.stale = append(pb.report.stale, &c)\n\tcase FreshState:\n\t\tpb.report.fresh = append(pb.report.fresh, &c)\n\tdefault:\n\t\treturn\n\t}\n\tpb.containerCount += 1\n}\n\n// AddLogEntry adds a preview log entry of the given level\nfunc (pd *previewData) AddLogEntry(level LogLevel) {\n\tvar msg string\n\tswitch level {\n\tcase FatalLevel:\n\t\tfallthrough\n\tcase ErrorLevel:\n\t\tfallthrough\n\tcase WarnLevel:\n\t\tmsg = pd.randomEntry(logErrors)\n\tdefault:\n\t\tmsg = pd.randomEntry(logMessages)\n\t}\n\tpd.Entries = append(pd.Entries, &logEntry{\n\t\tMessage: msg,\n\t\tData:    map[string]any{},\n\t\tTime:    pd.generateTime(),\n\t\tLevel:   level,\n\t})\n}\n\n// Report returns a preview report\nfunc (pb *previewData) Report() types.Report {\n\treturn pb.report\n}\n\nfunc (pb *previewData) generateID() string {\n\tbuf := make([]byte, 32)\n\t_, _ = pb.rand.Read(buf)\n\treturn hex.EncodeToString(buf)\n}\n\nfunc (pb *previewData) generateTime() time.Time {\n\tpb.lastTime = pb.lastTime.Add(time.Duration(pb.rand.Intn(30)) * time.Second)\n\treturn pb.lastTime\n}\n\nfunc (pb *previewData) randomEntry(arr []string) string {\n\treturn arr[pb.rand.Intn(len(arr))]\n}\n\nfunc (pb *previewData) generateName() string {\n\tindex := pb.containerCount\n\tif index <= len(containerNames) {\n\t\treturn \"/\" + containerNames[index]\n\t}\n\tsuffix := index / len(containerNames)\n\tindex %= len(containerNames)\n\treturn \"/\" + containerNames[index] + strconv.FormatInt(int64(suffix), 10)\n}\n\nfunc (pb *previewData) generateImageName(name string) string {\n\tindex := pb.containerCount % len(organizationNames)\n\treturn organizationNames[index] + name + \":latest\"\n}\n"
  },
  {
    "path": "pkg/notifications/preview/data/logs.go",
    "content": "package data\n\nimport (\n\t\"time\"\n)\n\ntype logEntry struct {\n\tMessage string\n\tData    map[string]any\n\tTime    time.Time\n\tLevel   LogLevel\n}\n\n// LogLevel is the analog of logrus.Level\ntype LogLevel string\n\nconst (\n\tTraceLevel LogLevel = \"trace\"\n\tDebugLevel LogLevel = \"debug\"\n\tInfoLevel  LogLevel = \"info\"\n\tWarnLevel  LogLevel = \"warning\"\n\tErrorLevel LogLevel = \"error\"\n\tFatalLevel LogLevel = \"fatal\"\n\tPanicLevel LogLevel = \"panic\"\n)\n\n// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels\nfunc LevelsFromString(str string) []LogLevel {\n\tlevels := make([]LogLevel, 0, len(str))\n\tfor _, c := range str {\n\t\tswitch c {\n\t\tcase 'p':\n\t\t\tlevels = append(levels, PanicLevel)\n\t\tcase 'f':\n\t\t\tlevels = append(levels, FatalLevel)\n\t\tcase 'e':\n\t\t\tlevels = append(levels, ErrorLevel)\n\t\tcase 'w':\n\t\t\tlevels = append(levels, WarnLevel)\n\t\tcase 'i':\n\t\t\tlevels = append(levels, InfoLevel)\n\t\tcase 'd':\n\t\t\tlevels = append(levels, DebugLevel)\n\t\tcase 't':\n\t\t\tlevels = append(levels, TraceLevel)\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn levels\n}\n\n// String returns the log level as a string\nfunc (level LogLevel) String() string {\n\treturn string(level)\n}\n"
  },
  {
    "path": "pkg/notifications/preview/data/preview_strings.go",
    "content": "package data\n\nvar containerNames = []string{\n\t\"cyberscribe\",\n\t\"datamatrix\",\n\t\"nexasync\",\n\t\"quantumquill\",\n\t\"aerosphere\",\n\t\"virtuos\",\n\t\"fusionflow\",\n\t\"neuralink\",\n\t\"pixelpulse\",\n\t\"synthwave\",\n\t\"codecraft\",\n\t\"zapzone\",\n\t\"robologic\",\n\t\"dreamstream\",\n\t\"infinisync\",\n\t\"megamesh\",\n\t\"novalink\",\n\t\"xenogenius\",\n\t\"ecosim\",\n\t\"innovault\",\n\t\"techtracer\",\n\t\"fusionforge\",\n\t\"quantumquest\",\n\t\"neuronest\",\n\t\"codefusion\",\n\t\"datadyno\",\n\t\"pixelpioneer\",\n\t\"vortexvision\",\n\t\"cybercraft\",\n\t\"synthsphere\",\n\t\"infinitescript\",\n\t\"roborhythm\",\n\t\"dreamengine\",\n\t\"aquasync\",\n\t\"geniusgrid\",\n\t\"megamind\",\n\t\"novasync-pro\",\n\t\"xenonwave\",\n\t\"ecologic\",\n\t\"innoscan\",\n}\n\nvar organizationNames = []string{\n\t\"techwave\",\n\t\"codecrafters\",\n\t\"innotechlabs\",\n\t\"fusionsoft\",\n\t\"cyberpulse\",\n\t\"quantumscribe\",\n\t\"datadynamo\",\n\t\"neuralink\",\n\t\"pixelpro\",\n\t\"synthwizards\",\n\t\"virtucorplabs\",\n\t\"robologic\",\n\t\"dreamstream\",\n\t\"novanest\",\n\t\"megamind\",\n\t\"xenonwave\",\n\t\"ecologic\",\n\t\"innosync\",\n\t\"techgenius\",\n\t\"nexasoft\",\n\t\"codewave\",\n\t\"zapzone\",\n\t\"techsphere\",\n\t\"aquatech\",\n\t\"quantumcraft\",\n\t\"neuronest\",\n\t\"datafusion\",\n\t\"pixelpioneer\",\n\t\"synthsphere\",\n\t\"infinitescribe\",\n\t\"roborhythm\",\n\t\"dreamengine\",\n\t\"vortexvision\",\n\t\"geniusgrid\",\n\t\"megamesh\",\n\t\"novasync\",\n\t\"xenogeniuslabs\",\n\t\"ecosim\",\n\t\"innovault\",\n}\n\nvar errorMessages = []string{\n\t\"Error 404: Resource not found\",\n\t\"Critical Error: System meltdown imminent\",\n\t\"Error 500: Internal server error\",\n\t\"Invalid input: Please check your data\",\n\t\"Access denied: Unauthorized access detected\",\n\t\"Network connection lost: Please check your connection\",\n\t\"Error 403: Forbidden access\",\n\t\"Fatal error: System crash imminent\",\n\t\"File not found: Check the file path\",\n\t\"Invalid credentials: Authentication failed\",\n\t\"Error 502: Bad Gateway\",\n\t\"Database connection failed: Please try again later\",\n\t\"Security breach detected: Take immediate action\",\n\t\"Error 400: Bad request\",\n\t\"Out of memory: Close unnecessary applications\",\n\t\"Invalid configuration: Check your settings\",\n\t\"Error 503: Service unavailable\",\n\t\"File is read-only: Cannot modify\",\n\t\"Data corruption detected: Backup your data\",\n\t\"Error 401: Unauthorized\",\n\t\"Disk space full: Free up disk space\",\n\t\"Connection timeout: Retry your request\",\n\t\"Error 504: Gateway timeout\",\n\t\"File access denied: Permission denied\",\n\t\"Unexpected error: Please contact support\",\n\t\"Error 429: Too many requests\",\n\t\"Invalid URL: Check the URL format\",\n\t\"Database query failed: Try again later\",\n\t\"Error 408: Request timeout\",\n\t\"File is in use: Close the file and try again\",\n\t\"Invalid parameter: Check your input\",\n\t\"Error 502: Proxy error\",\n\t\"Database connection lost: Reconnect and try again\",\n\t\"File size exceeds limit: Reduce the file size\",\n\t\"Error 503: Overloaded server\",\n\t\"Operation aborted: Try again\",\n\t\"Invalid API key: Check your API key\",\n\t\"Error 507: Insufficient storage\",\n\t\"Database deadlock: Retry your transaction\",\n\t\"Error 405: Method not allowed\",\n\t\"File format not supported: Choose a different format\",\n\t\"Unknown error: Contact system administrator\",\n}\n\nvar skippedMessages = []string{\n\t\"Fear of introducing new bugs\",\n\t\"Don't have time for the update process\",\n\t\"Current version works fine for my needs\",\n\t\"Concerns about compatibility with other software\",\n\t\"Limited bandwidth for downloading updates\",\n\t\"Worries about losing custom settings or configurations\",\n\t\"Lack of trust in the software developer's updates\",\n\t\"Dislike changes to the user interface\",\n\t\"Avoiding potential subscription fees\",\n\t\"Suspicion of hidden data collection in updates\",\n\t\"Apprehension about changes in privacy policies\",\n\t\"Prefer the older version's features or design\",\n\t\"Worry about software becoming more resource-intensive\",\n\t\"Avoiding potential changes in licensing terms\",\n\t\"Waiting for initial bugs to be resolved in the update\",\n\t\"Concerns about update breaking third-party plugins or extensions\",\n\t\"Belief that the software is already secure enough\",\n\t\"Don't want to relearn how to use the software\",\n\t\"Fear of losing access to older file formats\",\n\t\"Avoiding the hassle of having to update multiple devices\",\n}\n\nvar logMessages = []string{\n\t\"Checking for available updates...\",\n\t\"Downloading update package...\",\n\t\"Verifying update integrity...\",\n\t\"Preparing to install update...\",\n\t\"Backing up existing configuration...\",\n\t\"Installing update...\",\n\t\"Update installation complete.\",\n\t\"Applying configuration settings...\",\n\t\"Cleaning up temporary files...\",\n\t\"Update successful! Software is now up-to-date.\",\n\t\"Restarting the application...\",\n\t\"Restart complete. Enjoy the latest features!\",\n\t\"Update rollback complete. Your software remains at the previous version.\",\n}\n\nvar logErrors = []string{\n\t\"Unable to check for updates. Please check your internet connection.\",\n\t\"Update package download failed. Try again later.\",\n\t\"Update verification failed. Please contact support.\",\n\t\"Update installation failed. Rolling back to the previous version...\",\n\t\"Your configuration settings may have been reset to defaults.\",\n}\n"
  },
  {
    "path": "pkg/notifications/preview/data/report.go",
    "content": "package data\n\nimport (\n\t\"sort\"\n\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// State is the outcome of a container in a session report\ntype State string\n\nconst (\n\tScannedState State = \"scanned\"\n\tUpdatedState State = \"updated\"\n\tFailedState  State = \"failed\"\n\tSkippedState State = \"skipped\"\n\tStaleState   State = \"stale\"\n\tFreshState   State = \"fresh\"\n)\n\n// StatesFromString parses a string of state characters and returns a slice of the corresponding report states\nfunc StatesFromString(str string) []State {\n\tstates := make([]State, 0, len(str))\n\tfor _, c := range str {\n\t\tswitch c {\n\t\tcase 'c':\n\t\t\tstates = append(states, ScannedState)\n\t\tcase 'u':\n\t\t\tstates = append(states, UpdatedState)\n\t\tcase 'e':\n\t\t\tstates = append(states, FailedState)\n\t\tcase 'k':\n\t\t\tstates = append(states, SkippedState)\n\t\tcase 't':\n\t\t\tstates = append(states, StaleState)\n\t\tcase 'f':\n\t\t\tstates = append(states, FreshState)\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn states\n}\n\ntype report struct {\n\tscanned []types.ContainerReport\n\tupdated []types.ContainerReport\n\tfailed  []types.ContainerReport\n\tskipped []types.ContainerReport\n\tstale   []types.ContainerReport\n\tfresh   []types.ContainerReport\n}\n\nfunc (r *report) Scanned() []types.ContainerReport {\n\treturn r.scanned\n}\nfunc (r *report) Updated() []types.ContainerReport {\n\treturn r.updated\n}\nfunc (r *report) Failed() []types.ContainerReport {\n\treturn r.failed\n}\nfunc (r *report) Skipped() []types.ContainerReport {\n\treturn r.skipped\n}\nfunc (r *report) Stale() []types.ContainerReport {\n\treturn r.stale\n}\nfunc (r *report) Fresh() []types.ContainerReport {\n\treturn r.fresh\n}\n\nfunc (r *report) All() []types.ContainerReport {\n\tallLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)\n\tall := make([]types.ContainerReport, 0, allLen)\n\n\tpresentIds := map[types.ContainerID][]string{}\n\n\tappendUnique := func(reports []types.ContainerReport) {\n\t\tfor _, cr := range reports {\n\t\t\tif _, found := presentIds[cr.ID()]; found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tall = append(all, cr)\n\t\t\tpresentIds[cr.ID()] = nil\n\t\t}\n\t}\n\n\tappendUnique(r.updated)\n\tappendUnique(r.failed)\n\tappendUnique(r.skipped)\n\tappendUnique(r.stale)\n\tappendUnique(r.fresh)\n\tappendUnique(r.scanned)\n\n\tsort.Sort(sortableContainers(all))\n\n\treturn all\n}\n\ntype sortableContainers []types.ContainerReport\n\n// Len implements sort.Interface.Len\nfunc (s sortableContainers) Len() int { return len(s) }\n\n// Less implements sort.Interface.Less\nfunc (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }\n\n// Swap implements sort.Interface.Swap\nfunc (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\n"
  },
  {
    "path": "pkg/notifications/preview/data/status.go",
    "content": "package data\n\nimport wt \"github.com/containrrr/watchtower/pkg/types\"\n\ntype containerStatus struct {\n\tcontainerID   wt.ContainerID\n\toldImage      wt.ImageID\n\tnewImage      wt.ImageID\n\tcontainerName string\n\timageName     string\n\terror\n\tstate State\n}\n\nfunc (u *containerStatus) ID() wt.ContainerID {\n\treturn u.containerID\n}\n\nfunc (u *containerStatus) Name() string {\n\treturn u.containerName\n}\n\nfunc (u *containerStatus) CurrentImageID() wt.ImageID {\n\treturn u.oldImage\n}\n\nfunc (u *containerStatus) LatestImageID() wt.ImageID {\n\treturn u.newImage\n}\n\nfunc (u *containerStatus) ImageName() string {\n\treturn u.imageName\n}\n\nfunc (u *containerStatus) Error() string {\n\tif u.error == nil {\n\t\treturn \"\"\n\t}\n\treturn u.error.Error()\n}\n\nfunc (u *containerStatus) State() string {\n\treturn string(u.state)\n}\n"
  },
  {
    "path": "pkg/notifications/preview/tplprev.go",
    "content": "package preview\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/containrrr/watchtower/pkg/notifications/preview/data\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/templates\"\n)\n\nfunc Render(input string, states []data.State, loglevels []data.LogLevel) (string, error) {\n\n\tdata := data.New()\n\n\ttpl, err := template.New(\"\").Funcs(templates.Funcs).Parse(input)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse %v\", err)\n\t}\n\n\tfor _, state := range states {\n\t\tdata.AddFromState(state)\n\t}\n\n\tfor _, level := range loglevels {\n\t\tdata.AddLogEntry(level)\n\t}\n\n\tvar buf strings.Builder\n\terr = tpl.Execute(&buf, data)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to execute template: %v\", err)\n\t}\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "pkg/notifications/shoutrrr.go",
    "content": "package notifications\n\nimport (\n\t\"bytes\"\n\tstdlog \"log\"\n\t\"os\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/containrrr/shoutrrr\"\n\t\"github.com/containrrr/shoutrrr/pkg/types\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/templates\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// LocalLog is a logrus logger that does not send entries as notifications\nvar LocalLog = log.WithField(\"notify\", \"no\")\n\nconst (\n\tshoutrrrType = \"shoutrrr\"\n)\n\ntype router interface {\n\tSend(message string, params *types.Params) []error\n}\n\n// Implements Notifier, logrus.Hook\ntype shoutrrrTypeNotifier struct {\n\tUrls           []string\n\tRouter         router\n\tentries        []*log.Entry\n\tlogLevel       log.Level\n\ttemplate       *template.Template\n\tmessages       chan string\n\tdone           chan bool\n\tlegacyTemplate bool\n\tparams         *types.Params\n\tdata           StaticData\n\treceiving      bool\n\tdelay          time.Duration\n}\n\n// GetScheme returns the scheme part of a Shoutrrr URL\nfunc GetScheme(url string) string {\n\tschemeEnd := strings.Index(url, \":\")\n\tif schemeEnd <= 0 {\n\t\treturn \"invalid\"\n\t}\n\treturn url[:schemeEnd]\n}\n\n// GetNames returns a list of notification services that has been added\nfunc (n *shoutrrrTypeNotifier) GetNames() []string {\n\tnames := make([]string, len(n.Urls))\n\tfor i, u := range n.Urls {\n\t\tnames[i] = GetScheme(u)\n\t}\n\treturn names\n}\n\n// GetURLs returns a list of URLs for notification services that has been added\nfunc (n *shoutrrrTypeNotifier) GetURLs() []string {\n\treturn n.Urls\n}\n\n// AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them\nfunc (n *shoutrrrTypeNotifier) AddLogHook() {\n\tif n.receiving {\n\t\treturn\n\t}\n\tn.receiving = true\n\tlog.AddHook(n)\n\n\t// Do the sending in a separate goroutine, so we don't block the main process.\n\tgo sendNotifications(n)\n}\n\nfunc createNotifier(urls []string, level log.Level, tplString string, legacy bool, data StaticData, stdout bool, delay time.Duration) *shoutrrrTypeNotifier {\n\ttpl, err := getShoutrrrTemplate(tplString, legacy)\n\tif err != nil {\n\t\tlog.Errorf(\"Could not use configured notification template: %s. Using default template\", err)\n\t}\n\n\tvar logger types.StdLogger\n\tif stdout {\n\t\tlogger = stdlog.New(os.Stdout, ``, 0)\n\t} else {\n\t\tlogger = stdlog.New(log.StandardLogger().WriterLevel(log.TraceLevel), \"Shoutrrr: \", 0)\n\t}\n\tr, err := shoutrrr.NewSender(logger, urls...)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to initialize Shoutrrr notifications: %s\\n\", err.Error())\n\t}\n\n\tparams := &types.Params{}\n\tif data.Title != \"\" {\n\t\tparams.SetTitle(data.Title)\n\t}\n\n\treturn &shoutrrrTypeNotifier{\n\t\tUrls:           urls,\n\t\tRouter:         r,\n\t\tmessages:       make(chan string, 1),\n\t\tdone:           make(chan bool),\n\t\tlogLevel:       level,\n\t\ttemplate:       tpl,\n\t\tlegacyTemplate: legacy,\n\t\tdata:           data,\n\t\tparams:         params,\n\t\tdelay:          delay,\n\t}\n}\n\nfunc sendNotifications(n *shoutrrrTypeNotifier) {\n\tfor msg := range n.messages {\n\t\ttime.Sleep(n.delay)\n\t\terrs := n.Router.Send(msg, n.params)\n\n\t\tfor i, err := range errs {\n\t\t\tif err != nil {\n\t\t\t\tscheme := GetScheme(n.Urls[i])\n\t\t\t\t// Use fmt so it doesn't trigger another notification.\n\t\t\t\tLocalLog.WithFields(log.Fields{\n\t\t\t\t\t\"service\": scheme,\n\t\t\t\t\t\"index\":   i,\n\t\t\t\t}).WithError(err).Error(\"Failed to send shoutrrr notification\")\n\t\t\t}\n\t\t}\n\t}\n\n\tn.done <- true\n}\n\nfunc (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) {\n\tvar body bytes.Buffer\n\tvar templateData interface{} = data\n\tif n.legacyTemplate {\n\t\ttemplateData = data.Entries\n\t}\n\tif err := n.template.Execute(&body, templateData); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn body.String(), nil\n}\n\nfunc (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {\n\tmsg, err := n.buildMessage(Data{n.data, entries, report})\n\n\tif msg == \"\" {\n\t\t// Log in go func in case we entered from Fire to avoid stalling\n\t\tgo func() {\n\t\t\tif err != nil {\n\t\t\t\tLocalLog.WithError(err).Fatal(\"Notification template error\")\n\t\t\t} else if len(n.Urls) > 1 {\n\t\t\t\tLocalLog.Info(\"Skipping notification due to empty message\")\n\t\t\t}\n\t\t}()\n\t\treturn\n\t}\n\tn.messages <- msg\n}\n\n// StartNotification begins queueing up messages to send them as a batch\nfunc (n *shoutrrrTypeNotifier) StartNotification() {\n\tif n.entries == nil {\n\t\tn.entries = make([]*log.Entry, 0, 10)\n\t}\n}\n\n// SendNotification sends the queued up messages as a notification\nfunc (n *shoutrrrTypeNotifier) SendNotification(report t.Report) {\n\tn.sendEntries(n.entries, report)\n\tn.entries = nil\n}\n\n// Close prevents further messages from being queued and waits until all the currently queued up messages have been sent\nfunc (n *shoutrrrTypeNotifier) Close() {\n\tclose(n.messages)\n\n\t// Use fmt so it doesn't trigger another notification.\n\tLocalLog.Info(\"Waiting for the notification goroutine to finish\")\n\n\t<-n.done\n}\n\n// Levels return what log levels trigger notifications\nfunc (n *shoutrrrTypeNotifier) Levels() []log.Level {\n\treturn log.AllLevels[:n.logLevel+1]\n}\n\n// Fire is the hook that logrus calls on a new log message\nfunc (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {\n\tif entry.Data[\"notify\"] == \"no\" {\n\t\t// Skip logging if explicitly tagged as non-notify\n\t\treturn nil\n\t}\n\tif n.entries != nil {\n\t\tn.entries = append(n.entries, entry)\n\t} else {\n\t\t// Log output generated outside a cycle is sent immediately.\n\t\tn.sendEntries([]*log.Entry{entry}, nil)\n\t}\n\treturn nil\n}\n\nfunc getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {\n\n\ttplBase := template.New(\"\").Funcs(templates.Funcs)\n\n\tif builtin, found := commonTemplates[tplString]; found {\n\t\tlog.WithField(`template`, tplString).Debug(`Using common template`)\n\t\ttplString = builtin\n\t}\n\n\t// If we succeed in getting a non-empty template configuration\n\t// try to parse the template string.\n\tif tplString != \"\" {\n\t\ttpl, err = tplBase.Parse(tplString)\n\t}\n\n\t// If we had an error (either from parsing the template string\n\t// or from getting the template configuration) or a\n\t// template wasn't configured (the empty template string)\n\t// fallback to using the default template.\n\tif err != nil || tplString == \"\" {\n\t\tdefaultKey := `default`\n\t\tif legacy {\n\t\t\tdefaultKey = `default-legacy`\n\t\t}\n\n\t\ttpl = template.Must(tplBase.Parse(commonTemplates[defaultKey]))\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/notifications/shoutrrr_test.go",
    "content": "package notifications\n\nimport (\n\t\"time\"\n\n\t\"github.com/containrrr/shoutrrr/pkg/types\"\n\t\"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t\"github.com/containrrr/watchtower/internal/flags\"\n\ts \"github.com/containrrr/watchtower/pkg/session\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\t\"github.com/onsi/gomega/gbytes\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar allButTrace = logrus.DebugLevel\n\nvar legacyMockData = Data{\n\tEntries: []*logrus.Entry{\n\t\t{\n\t\t\tLevel:   logrus.InfoLevel,\n\t\t\tMessage: \"foo Bar\",\n\t\t},\n\t},\n}\n\nvar mockDataMultipleEntries = Data{\n\tEntries: []*logrus.Entry{\n\t\t{\n\t\t\tLevel:   logrus.InfoLevel,\n\t\t\tMessage: \"The situation is under control\",\n\t\t},\n\t\t{\n\t\t\tLevel:   logrus.WarnLevel,\n\t\t\tMessage: \"All the smoke might be covering up some problems\",\n\t\t},\n\t\t{\n\t\t\tLevel:   logrus.ErrorLevel,\n\t\t\tMessage: \"Turns out everything is on fire\",\n\t\t},\n\t},\n}\n\nvar mockDataAllFresh = Data{\n\tEntries: []*logrus.Entry{},\n\tReport:  mocks.CreateMockProgressReport(s.FreshState),\n}\n\nfunc mockDataFromStates(states ...s.State) Data {\n\thostname := \"Mock\"\n\tprefix := \"\"\n\treturn Data{\n\t\tEntries: legacyMockData.Entries,\n\t\tReport:  mocks.CreateMockProgressReport(states...),\n\t\tStaticData: StaticData{\n\t\t\tTitle: GetTitle(hostname, prefix),\n\t\t\tHost:  hostname,\n\t\t},\n\t}\n}\n\nvar _ = Describe(\"Shoutrrr\", func() {\n\tvar logBuffer *gbytes.Buffer\n\n\tBeforeEach(func() {\n\t\tlogBuffer = gbytes.NewBuffer()\n\t\tlogrus.SetOutput(logBuffer)\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t\tlogrus.SetFormatter(&logrus.TextFormatter{\n\t\t\tDisableColors:    true,\n\t\t\tDisableTimestamp: true,\n\t\t})\n\t})\n\n\tWhen(\"passing a common template name\", func() {\n\t\tIt(\"should format using that template\", func() {\n\t\t\texpected := `\nupdt1 (mock/updt1:latest): Updated\n`[1:]\n\t\t\tdata := mockDataFromStates(s.UpdatedState)\n\t\t\tExpect(getTemplatedResult(`porcelain.v1.summary-no-log`, false, data)).To(Equal(expected))\n\t\t})\n\t})\n\n\tWhen(\"adding a log hook\", func() {\n\t\tWhen(\"it has not been added before\", func() {\n\t\t\tIt(\"should be added to the logrus hooks\", func() {\n\t\t\t\tlevel := logrus.TraceLevel\n\t\t\t\thooksBefore := len(logrus.StandardLogger().Hooks[level])\n\t\t\t\tshoutrrr := createNotifier([]string{}, level, \"\", true, StaticData{}, false, time.Second)\n\t\t\t\tshoutrrr.AddLogHook()\n\t\t\t\thooksAfter := len(logrus.StandardLogger().Hooks[level])\n\t\t\t\tExpect(hooksAfter).To(BeNumerically(\">\", hooksBefore))\n\t\t\t})\n\t\t})\n\t\tWhen(\"it is being added a second time\", func() {\n\t\t\tIt(\"should not be added to the logrus hooks\", func() {\n\t\t\t\tlevel := logrus.TraceLevel\n\t\t\t\tshoutrrr := createNotifier([]string{}, level, \"\", true, StaticData{}, false, time.Second)\n\t\t\t\tshoutrrr.AddLogHook()\n\t\t\t\thooksBefore := len(logrus.StandardLogger().Hooks[level])\n\t\t\t\tshoutrrr.AddLogHook()\n\t\t\t\thooksAfter := len(logrus.StandardLogger().Hooks[level])\n\t\t\t\tExpect(hooksAfter).To(Equal(hooksBefore))\n\t\t\t})\n\t\t})\n\t})\n\n\tWhen(\"using legacy templates\", func() {\n\n\t\tWhen(\"no custom template is provided\", func() {\n\t\t\tIt(\"should format the messages using the default template\", func() {\n\t\t\t\tcmd := new(cobra.Command)\n\t\t\t\tflags.RegisterNotificationFlags(cmd)\n\n\t\t\t\tshoutrrr := createNotifier([]string{}, logrus.TraceLevel, \"\", true, StaticData{}, false, time.Second)\n\n\t\t\t\tentries := []*logrus.Entry{\n\t\t\t\t\t{\n\t\t\t\t\t\tMessage: \"foo bar\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\ts, err := shoutrrr.buildMessage(Data{Entries: entries})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(s).To(Equal(\"foo bar\\n\"))\n\t\t\t})\n\t\t})\n\t\tWhen(\"given a valid custom template\", func() {\n\t\t\tIt(\"should format the messages using the custom template\", func() {\n\n\t\t\t\ttplString := `{{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}`\n\t\t\t\ttpl, err := getShoutrrrTemplate(tplString, true)\n\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\n\t\t\t\tshoutrrr := &shoutrrrTypeNotifier{\n\t\t\t\t\ttemplate:       tpl,\n\t\t\t\t\tlegacyTemplate: true,\n\t\t\t\t}\n\n\t\t\t\tentries := []*logrus.Entry{\n\t\t\t\t\t{\n\t\t\t\t\t\tLevel:   logrus.InfoLevel,\n\t\t\t\t\t\tMessage: \"foo bar\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\ts, err := shoutrrr.buildMessage(Data{Entries: entries})\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tExpect(s).To(Equal(\"info: foo bar\\n\"))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"the default template\", func() {\n\t\t\tWhen(\"all containers are fresh\", func() {\n\t\t\t\tIt(\"should return an empty string\", func() {\n\t\t\t\t\tExpect(getTemplatedResult(``, true, mockDataAllFresh)).To(Equal(\"\"))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"given an invalid custom template\", func() {\n\t\t\tIt(\"should format the messages using the default template\", func() {\n\t\t\t\tinvNotif, err := createNotifierWithTemplate(`{{ intentionalSyntaxError`, true)\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tinvMsg, err := invNotif.buildMessage(legacyMockData)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t\tdefNotif, err := createNotifierWithTemplate(``, true)\n\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\t\t\t\tdefMsg, err := defNotif.buildMessage(legacyMockData)\n\t\t\t\tExpect(err).ToNot(HaveOccurred())\n\n\t\t\t\tExpect(invMsg).To(Equal(defMsg))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"given a template that is using ToUpper function\", func() {\n\t\t\tIt(\"should return the text in UPPER CASE\", func() {\n\t\t\t\ttplString := `{{range .}}{{ .Message | ToUpper }}{{end}}`\n\t\t\t\tExpect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal(\"FOO BAR\"))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"given a template that is using ToLower function\", func() {\n\t\t\tIt(\"should return the text in lower case\", func() {\n\t\t\t\ttplString := `{{range .}}{{ .Message | ToLower }}{{end}}`\n\t\t\t\tExpect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal(\"foo bar\"))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"given a template that is using Title function\", func() {\n\t\t\tIt(\"should return the text in Title Case\", func() {\n\t\t\t\ttplString := `{{range .}}{{ .Message | Title }}{{end}}`\n\t\t\t\tExpect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal(\"Foo Bar\"))\n\t\t\t})\n\t\t})\n\n\t})\n\n\tWhen(\"using report templates\", func() {\n\t\tWhen(\"no custom template is provided\", func() {\n\t\t\tIt(\"should format the messages using the default template\", func() {\n\t\t\t\texpected := `4 Scanned, 2 Updated, 1 Failed\n- updt1 (mock/updt1:latest): 01d110000000 updated to d0a110000000\n- updt2 (mock/updt2:latest): 01d120000000 updated to d0a120000000\n- frsh1 (mock/frsh1:latest): Fresh\n- skip1 (mock/skip1:latest): Skipped: unpossible\n- fail1 (mock/fail1:latest): Failed: accidentally the whole container`\n\t\t\t\tdata := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)\n\t\t\t\tExpect(getTemplatedResult(``, false, data)).To(Equal(expected))\n\t\t\t})\n\n\t\t})\n\n\t\tWhen(\"using a template referencing Title\", func() {\n\t\t\tIt(\"should contain the title in the output\", func() {\n\t\t\t\texpected := `Watchtower updates on Mock`\n\t\t\t\tdata := mockDataFromStates(s.UpdatedState)\n\t\t\t\tExpect(getTemplatedResult(`{{ .Title }}`, false, data)).To(Equal(expected))\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"using a template referencing Host\", func() {\n\t\t\tIt(\"should contain the hostname in the output\", func() {\n\t\t\t\texpected := `Mock`\n\t\t\t\tdata := mockDataFromStates(s.UpdatedState)\n\t\t\t\tExpect(getTemplatedResult(`{{ .Host }}`, false, data)).To(Equal(expected))\n\t\t\t})\n\t\t})\n\n\t\tDescribe(\"the default template\", func() {\n\t\t\tWhen(\"all containers are fresh\", func() {\n\t\t\t\tIt(\"should return an empty string\", func() {\n\t\t\t\t\tExpect(getTemplatedResult(``, false, mockDataAllFresh)).To(Equal(\"\"))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"at least one container was updated\", func() {\n\t\t\t\tIt(\"should send a report\", func() {\n\t\t\t\t\texpected := `1 Scanned, 1 Updated, 0 Failed\n- updt1 (mock/updt1:latest): 01d110000000 updated to d0a110000000`\n\t\t\t\t\tdata := mockDataFromStates(s.UpdatedState)\n\t\t\t\t\tExpect(getTemplatedResult(``, false, data)).To(Equal(expected))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"at least one container failed to update\", func() {\n\t\t\t\tIt(\"should send a report\", func() {\n\t\t\t\t\texpected := `1 Scanned, 0 Updated, 1 Failed\n- fail1 (mock/fail1:latest): Failed: accidentally the whole container`\n\t\t\t\t\tdata := mockDataFromStates(s.FailedState)\n\t\t\t\t\tExpect(getTemplatedResult(``, false, data)).To(Equal(expected))\n\t\t\t\t})\n\t\t\t})\n\t\t\tWhen(\"the report is nil\", func() {\n\t\t\t\tIt(\"should return the logged entries\", func() {\n\t\t\t\t\texpected := `The situation is under control\nAll the smoke might be covering up some problems\nTurns out everything is on fire\n`\n\t\t\t\t\tExpect(getTemplatedResult(``, false, mockDataMultipleEntries)).To(Equal(expected))\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t})\n\n\tWhen(\"batching notifications\", func() {\n\t\tWhen(\"no messages are queued\", func() {\n\t\t\tIt(\"should not send any notification\", func() {\n\t\t\t\tshoutrrr := createNotifier([]string{\"logger://\"}, allButTrace, \"\", true, StaticData{}, false, time.Duration(0))\n\t\t\t\tshoutrrr.StartNotification()\n\t\t\t\tshoutrrr.SendNotification(nil)\n\t\t\t\tConsistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))\n\t\t\t})\n\t\t})\n\t\tWhen(\"at least one message is queued\", func() {\n\t\t\tIt(\"should send a notification\", func() {\n\t\t\t\tshoutrrr := createNotifier([]string{\"logger://\"}, allButTrace, \"\", true, StaticData{}, false, time.Duration(0))\n\t\t\t\tshoutrrr.AddLogHook()\n\t\t\t\tshoutrrr.StartNotification()\n\t\t\t\tlogrus.Info(\"This log message is sponsored by ContainrrrVPN\")\n\t\t\t\tshoutrrr.SendNotification(nil)\n\t\t\t\tEventually(logBuffer).Should(gbytes.Say(`Shoutrrr: This log message is sponsored by ContainrrrVPN`))\n\t\t\t})\n\t\t})\n\t})\n\n\tWhen(\"the title data field is empty\", func() {\n\t\tIt(\"should not have set the title param\", func() {\n\t\t\tshoutrrr := createNotifier([]string{\"logger://\"}, allButTrace, \"\", true, StaticData{\n\t\t\t\tHost:  \"test.host\",\n\t\t\t\tTitle: \"\",\n\t\t\t}, false, time.Second)\n\t\t\t_, found := shoutrrr.params.Title()\n\t\t\tExpect(found).ToNot(BeTrue())\n\t\t})\n\t})\n\n\tWhen(\"sending notifications\", func() {\n\n\t\tIt(\"SlowNotificationNotSent\", func() {\n\t\t\t_, blockingRouter := sendNotificationsWithBlockingRouter(true)\n\n\t\t\tEventually(blockingRouter.sent).Should(Not(Receive()))\n\n\t\t})\n\n\t\tIt(\"SlowNotificationSent\", func() {\n\t\t\tshoutrrr, blockingRouter := sendNotificationsWithBlockingRouter(true)\n\n\t\t\tblockingRouter.unlock <- true\n\t\t\tshoutrrr.Close()\n\n\t\t\tEventually(blockingRouter.sent).Should(Receive(BeTrue()))\n\t\t})\n\t})\n})\n\ntype blockingRouter struct {\n\tunlock chan bool\n\tsent   chan bool\n}\n\nfunc (b blockingRouter) Send(_ string, _ *types.Params) []error {\n\t<-b.unlock\n\tb.sent <- true\n\treturn nil\n}\n\nfunc sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *blockingRouter) {\n\n\trouter := &blockingRouter{\n\t\tunlock: make(chan bool, 1),\n\t\tsent:   make(chan bool, 1),\n\t}\n\n\ttpl, err := getShoutrrrTemplate(\"\", legacy)\n\tExpect(err).NotTo(HaveOccurred())\n\n\tshoutrrr := &shoutrrrTypeNotifier{\n\t\ttemplate:       tpl,\n\t\tmessages:       make(chan string, 1),\n\t\tdone:           make(chan bool),\n\t\tRouter:         router,\n\t\tlegacyTemplate: legacy,\n\t\tparams:         &types.Params{},\n\t\tdelay:          time.Duration(0),\n\t}\n\n\tentry := &logrus.Entry{\n\t\tMessage: \"foo bar\",\n\t}\n\n\tgo sendNotifications(shoutrrr)\n\n\tshoutrrr.StartNotification()\n\t_ = shoutrrr.Fire(entry)\n\n\tshoutrrr.SendNotification(nil)\n\n\treturn shoutrrr, router\n}\n\nfunc createNotifierWithTemplate(tplString string, legacy bool) (*shoutrrrTypeNotifier, error) {\n\ttpl, err := getShoutrrrTemplate(tplString, legacy)\n\n\treturn &shoutrrrTypeNotifier{\n\t\ttemplate:       tpl,\n\t\tlegacyTemplate: legacy,\n\t}, err\n}\n\nfunc getTemplatedResult(tplString string, legacy bool, data Data) (msg string) {\n\tnotifier, err := createNotifierWithTemplate(tplString, legacy)\n\tExpectWithOffset(1, err).NotTo(HaveOccurred())\n\tmsg, err = notifier.buildMessage(data)\n\tExpectWithOffset(1, err).NotTo(HaveOccurred())\n\treturn msg\n}\n"
  },
  {
    "path": "pkg/notifications/slack.go",
    "content": "package notifications\n\nimport (\n\t\"strings\"\n\n\tshoutrrrDisco \"github.com/containrrr/shoutrrr/pkg/services/discord\"\n\tshoutrrrSlack \"github.com/containrrr/shoutrrr/pkg/services/slack\"\n\tt \"github.com/containrrr/watchtower/pkg/types\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tslackType = \"slack\"\n)\n\ntype slackTypeNotifier struct {\n\tHookURL   string\n\tUsername  string\n\tChannel   string\n\tIconEmoji string\n\tIconURL   string\n}\n\nfunc newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier {\n\tflags := c.Flags()\n\n\thookURL, _ := flags.GetString(\"notification-slack-hook-url\")\n\tuserName, _ := flags.GetString(\"notification-slack-identifier\")\n\tchannel, _ := flags.GetString(\"notification-slack-channel\")\n\temoji, _ := flags.GetString(\"notification-slack-icon-emoji\")\n\ticonURL, _ := flags.GetString(\"notification-slack-icon-url\")\n\n\tn := &slackTypeNotifier{\n\t\tHookURL:   hookURL,\n\t\tUsername:  userName,\n\t\tChannel:   channel,\n\t\tIconEmoji: emoji,\n\t\tIconURL:   iconURL,\n\t}\n\treturn n\n}\n\nfunc (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) {\n\ttrimmedURL := strings.TrimRight(s.HookURL, \"/\")\n\ttrimmedURL = strings.TrimPrefix(trimmedURL, \"https://\")\n\tparts := strings.Split(trimmedURL, \"/\")\n\n\tif parts[0] == \"discord.com\" || parts[0] == \"discordapp.com\" {\n\t\tlog.Debug(\"Detected a discord slack wrapper URL, using shoutrrr discord service\")\n\t\tconf := &shoutrrrDisco.Config{\n\t\t\tWebhookID:  parts[len(parts)-3],\n\t\t\tToken:      parts[len(parts)-2],\n\t\t\tColor:      ColorInt,\n\t\t\tSplitLines: true,\n\t\t\tUsername:   s.Username,\n\t\t}\n\n\t\tif s.IconURL != \"\" {\n\t\t\tconf.Avatar = s.IconURL\n\t\t}\n\n\t\treturn conf.GetURL().String(), nil\n\t}\n\n\twebhookToken := strings.Replace(s.HookURL, \"https://hooks.slack.com/services/\", \"\", 1)\n\n\tconf := &shoutrrrSlack.Config{\n\t\tBotName: s.Username,\n\t\tColor:   ColorHex,\n\t\tChannel: \"webhook\",\n\t}\n\n\tif s.IconURL != \"\" {\n\t\tconf.Icon = s.IconURL\n\t} else if s.IconEmoji != \"\" {\n\t\tconf.Icon = s.IconEmoji\n\t}\n\n\tif err := conf.Token.SetFromProp(webhookToken); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn conf.GetURL().String(), nil\n}\n"
  },
  {
    "path": "pkg/notifications/templates/funcs.go",
    "content": "package templates\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\nvar Funcs = template.FuncMap{\n\t\"ToUpper\": strings.ToUpper,\n\t\"ToLower\": strings.ToLower,\n\t\"ToJSON\":  toJSON,\n\t\"Title\":   cases.Title(language.AmericanEnglish).String,\n}\n\nfunc toJSON(v interface{}) string {\n\tvar bytes []byte\n\tvar err error\n\tif bytes, err = json.MarshalIndent(v, \"\", \"  \"); err != nil {\n\t\treturn fmt.Sprintf(\"failed to marshal JSON in notification template: %v\", err)\n\t}\n\treturn string(bytes)\n}\n"
  },
  {
    "path": "pkg/registry/auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/containrrr/watchtower/pkg/registry/helpers\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tref \"github.com/distribution/reference\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// ChallengeHeader is the HTTP Header containing challenge instructions\nconst ChallengeHeader = \"WWW-Authenticate\"\n\n// GetToken fetches a token for the registry hosting the provided image\nfunc GetToken(container types.Container, registryAuth string) (string, error) {\n\tnormalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tURL := GetChallengeURL(normalizedRef)\n\tlogrus.WithField(\"URL\", URL.String()).Debug(\"Built challenge URL\")\n\n\tvar req *http.Request\n\tif req, err = GetChallengeRequest(URL); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := &http.Client{}\n\tvar res *http.Response\n\tif res, err = client.Do(req); err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\tv := res.Header.Get(ChallengeHeader)\n\n\tlogrus.WithFields(logrus.Fields{\n\t\t\"status\": res.Status,\n\t\t\"header\": v,\n\t}).Debug(\"Got response to challenge request\")\n\n\tchallenge := strings.ToLower(v)\n\tif strings.HasPrefix(challenge, \"basic\") {\n\t\tif registryAuth == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"no credentials available\")\n\t\t}\n\n\t\treturn fmt.Sprintf(\"Basic %s\", registryAuth), nil\n\t}\n\tif strings.HasPrefix(challenge, \"bearer\") {\n\t\treturn GetBearerHeader(challenge, normalizedRef, registryAuth)\n\t}\n\n\treturn \"\", errors.New(\"unsupported challenge type from registry\")\n}\n\n// GetChallengeRequest creates a request for getting challenge instructions\nfunc GetChallengeRequest(URL url.URL) (*http.Request, error) {\n\treq, err := http.NewRequest(\"GET\", URL.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Accept\", \"*/*\")\n\treq.Header.Set(\"User-Agent\", \"Watchtower (Docker)\")\n\treturn req, nil\n}\n\n// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions\nfunc GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {\n\tclient := http.Client{}\n\tauthURL, err := GetAuthURL(challenge, imageRef)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar r *http.Request\n\tif r, err = http.NewRequest(\"GET\", authURL.String(), nil); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif registryAuth != \"\" {\n\t\tlogrus.Debug(\"Credentials found.\")\n\t\t// CREDENTIAL: Uncomment to log registry credentials\n\t\t// logrus.Tracef(\"Credentials: %v\", registryAuth)\n\t\tr.Header.Add(\"Authorization\", fmt.Sprintf(\"Basic %s\", registryAuth))\n\t} else {\n\t\tlogrus.Debug(\"No credentials found.\")\n\t}\n\n\tvar authResponse *http.Response\n\tif authResponse, err = client.Do(r); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbody, _ := io.ReadAll(authResponse.Body)\n\ttokenResponse := &types.TokenResponse{}\n\n\terr = json.Unmarshal(body, tokenResponse)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fmt.Sprintf(\"Bearer %s\", tokenResponse.Token), nil\n}\n\n// GetAuthURL from the instructions in the challenge\nfunc GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {\n\tloweredChallenge := strings.ToLower(challenge)\n\traw := strings.TrimPrefix(loweredChallenge, \"bearer\")\n\n\tpairs := strings.Split(raw, \",\")\n\tvalues := make(map[string]string, len(pairs))\n\n\tfor _, pair := range pairs {\n\t\ttrimmed := strings.Trim(pair, \" \")\n\t\tif key, val, ok := strings.Cut(trimmed, \"=\"); ok {\n\t\t\tvalues[key] = strings.Trim(val, `\"`)\n\t\t}\n\t}\n\tlogrus.WithFields(logrus.Fields{\n\t\t\"realm\":   values[\"realm\"],\n\t\t\"service\": values[\"service\"],\n\t}).Debug(\"Checking challenge header content\")\n\tif values[\"realm\"] == \"\" || values[\"service\"] == \"\" {\n\n\t\treturn nil, fmt.Errorf(\"challenge header did not include all values needed to construct an auth url\")\n\t}\n\n\tauthURL, _ := url.Parse(values[\"realm\"])\n\tq := authURL.Query()\n\tq.Add(\"service\", values[\"service\"])\n\n\tscopeImage := ref.Path(imageRef)\n\n\tscope := fmt.Sprintf(\"repository:%s:pull\", scopeImage)\n\tlogrus.WithFields(logrus.Fields{\"scope\": scope, \"image\": imageRef.Name()}).Debug(\"Setting scope for auth token\")\n\tq.Add(\"scope\", scope)\n\n\tauthURL.RawQuery = q.Encode()\n\treturn authURL, nil\n}\n\n// GetChallengeURL returns the URL to check auth requirements\n// for access to a given image\nfunc GetChallengeURL(imageRef ref.Named) url.URL {\n\thost, _ := helpers.GetRegistryAddress(imageRef.Name())\n\n\tURL := url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   host,\n\t\tPath:   \"/v2/\",\n\t}\n\treturn URL\n}\n"
  },
  {
    "path": "pkg/registry/auth/auth_test.go",
    "content": "package auth_test\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t\"github.com/containrrr/watchtower/pkg/registry/auth\"\n\n\twtTypes \"github.com/containrrr/watchtower/pkg/types\"\n\tref \"github.com/distribution/reference\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestAuth(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"Registry Auth Suite\")\n}\nfunc SkipIfCredentialsEmpty(credentials *wtTypes.RegistryCredentials, fn func()) func() {\n\tif credentials.Username == \"\" {\n\t\treturn func() {\n\t\t\tSkip(\"Username missing. Skipping integration test\")\n\t\t}\n\t} else if credentials.Password == \"\" {\n\t\treturn func() {\n\t\t\tSkip(\"Password missing. Skipping integration test\")\n\t\t}\n\t} else {\n\t\treturn fn\n\t}\n}\n\nvar GHCRCredentials = &wtTypes.RegistryCredentials{\n\tUsername: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_GH_USERNAME\"),\n\tPassword: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_GH_PASSWORD\"),\n}\n\nvar _ = Describe(\"the auth module\", func() {\n\tmockId := \"mock-id\"\n\tmockName := \"mock-container\"\n\tmockImage := \"ghcr.io/k6io/operator:latest\"\n\tmockCreated := time.Now()\n\tmockDigest := \"ghcr.io/k6io/operator@sha256:d68e1e532088964195ad3a0a71526bc2f11a78de0def85629beb75e2265f0547\"\n\n\tmockContainer := mocks.CreateMockContainerWithDigest(\n\t\tmockId,\n\t\tmockName,\n\t\tmockImage,\n\t\tmockCreated,\n\t\tmockDigest)\n\n\tDescribe(\"GetToken\", func() {\n\t\tIt(\"should parse the token from the response\",\n\t\t\tSkipIfCredentialsEmpty(GHCRCredentials, func() {\n\t\t\t\tcreds := fmt.Sprintf(\"%s:%s\", GHCRCredentials.Username, GHCRCredentials.Password)\n\t\t\t\ttoken, err := auth.GetToken(mockContainer, creds)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(token).NotTo(Equal(\"\"))\n\t\t\t}),\n\t\t)\n\t})\n\n\tDescribe(\"GetAuthURL\", func() {\n\t\tIt(\"should create a valid auth url object based on the challenge header supplied\", func() {\n\t\t\tchallenge := `bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:user/image:pull\"`\n\t\t\timageRef, err := ref.ParseNormalizedNamed(\"containrrr/watchtower\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\texpected := &url.URL{\n\t\t\t\tHost:     \"ghcr.io\",\n\t\t\t\tScheme:   \"https\",\n\t\t\t\tPath:     \"/token\",\n\t\t\t\tRawQuery: \"scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io\",\n\t\t\t}\n\n\t\t\tURL, err := auth.GetAuthURL(challenge, imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(URL).To(Equal(expected))\n\t\t})\n\n\t\tWhen(\"given an invalid challenge header\", func() {\n\t\t\tIt(\"should return an error\", func() {\n\t\t\t\tchallenge := `bearer realm=\"https://ghcr.io/token\"`\n\t\t\t\timageRef, err := ref.ParseNormalizedNamed(\"containrrr/watchtower\")\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tURL, err := auth.GetAuthURL(challenge, imageRef)\n\t\t\t\tExpect(err).To(HaveOccurred())\n\t\t\t\tExpect(URL).To(BeNil())\n\t\t\t})\n\t\t})\n\n\t\tWhen(\"deriving the auth scope from an image name\", func() {\n\t\t\tIt(\"should prepend official dockerhub images with \\\"library/\\\"\", func() {\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"registry\")).To(Equal(\"library/registry\"))\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"docker.io/registry\")).To(Equal(\"library/registry\"))\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"index.docker.io/registry\")).To(Equal(\"library/registry\"))\n\t\t\t})\n\t\t\tIt(\"should not include vanity hosts\\\"\", func() {\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"docker.io/containrrr/watchtower\")).To(Equal(\"containrrr/watchtower\"))\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"index.docker.io/containrrr/watchtower\")).To(Equal(\"containrrr/watchtower\"))\n\t\t\t})\n\t\t\tIt(\"should not destroy three segment image names\\\"\", func() {\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"piksel/containrrr/watchtower\")).To(Equal(\"piksel/containrrr/watchtower\"))\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"ghcr.io/piksel/containrrr/watchtower\")).To(Equal(\"piksel/containrrr/watchtower\"))\n\t\t\t})\n\t\t\tIt(\"should not prepend library/ to image names if they're not on dockerhub\", func() {\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"ghcr.io/watchtower\")).To(Equal(\"watchtower\"))\n\t\t\t\tExpect(getScopeFromImageAuthURL(\"ghcr.io/containrrr/watchtower\")).To(Equal(\"containrrr/watchtower\"))\n\t\t\t})\n\t\t})\n\t\tIt(\"should not crash when an empty field is received\", func() {\n\t\t\tinput := `bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:user/image:pull\",`\n\t\t\timageRef, err := ref.ParseNormalizedNamed(\"containrrr/watchtower\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tres, err := auth.GetAuthURL(input, imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).NotTo(BeNil())\n\t\t})\n\t\tIt(\"should not crash when a field without a value is received\", func() {\n\t\t\tinput := `bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:user/image:pull\",valuelesskey`\n\t\t\timageRef, err := ref.ParseNormalizedNamed(\"containrrr/watchtower\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tres, err := auth.GetAuthURL(input, imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(res).NotTo(BeNil())\n\t\t})\n\t})\n\n\tDescribe(\"GetChallengeURL\", func() {\n\t\tIt(\"should create a valid challenge url object based on the image ref supplied\", func() {\n\t\t\texpected := url.URL{Host: \"ghcr.io\", Scheme: \"https\", Path: \"/v2/\"}\n\t\t\timageRef, _ := ref.ParseNormalizedNamed(\"ghcr.io/containrrr/watchtower:latest\")\n\t\t\tExpect(auth.GetChallengeURL(imageRef)).To(Equal(expected))\n\t\t})\n\t\tIt(\"should assume Docker Hub for image refs with no explicit registry\", func() {\n\t\t\texpected := url.URL{Host: \"index.docker.io\", Scheme: \"https\", Path: \"/v2/\"}\n\t\t\timageRef, _ := ref.ParseNormalizedNamed(\"containrrr/watchtower:latest\")\n\t\t\tExpect(auth.GetChallengeURL(imageRef)).To(Equal(expected))\n\t\t})\n\t\tIt(\"should use index.docker.io if the image ref specifies docker.io\", func() {\n\t\t\texpected := url.URL{Host: \"index.docker.io\", Scheme: \"https\", Path: \"/v2/\"}\n\t\t\timageRef, _ := ref.ParseNormalizedNamed(\"docker.io/containrrr/watchtower:latest\")\n\t\t\tExpect(auth.GetChallengeURL(imageRef)).To(Equal(expected))\n\t\t})\n\t})\n})\n\nvar scopeImageRegexp = MatchRegexp(\"^repository:[a-z0-9]+(/[a-z0-9]+)*:pull$\")\n\nfunc getScopeFromImageAuthURL(imageName string) string {\n\tnormalizedRef, _ := ref.ParseNormalizedNamed(imageName)\n\tchallenge := `bearer realm=\"https://dummy.host/token\",service=\"dummy.host\",scope=\"repository:user/image:pull\"`\n\tURL, _ := auth.GetAuthURL(challenge, normalizedRef)\n\n\tscope := URL.Query().Get(\"scope\")\n\tExpect(scopeImageRegexp.Match(scope)).To(BeTrue())\n\treturn strings.Replace(scope[11:], \":pull\", \"\", 1)\n}\n"
  },
  {
    "path": "pkg/registry/digest/digest.go",
    "content": "package digest\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/meta\"\n\t\"github.com/containrrr/watchtower/pkg/registry/auth\"\n\t\"github.com/containrrr/watchtower/pkg/registry/manifest\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// ContentDigestHeader is the key for the key-value pair containing the digest header\nconst ContentDigestHeader = \"Docker-Content-Digest\"\n\n// CompareDigest ...\nfunc CompareDigest(container types.Container, registryAuth string) (bool, error) {\n\tif !container.HasImageInfo() {\n\t\treturn false, errors.New(\"container image info missing\")\n\t}\n\n\tvar digest string\n\n\tregistryAuth = TransformAuth(registryAuth)\n\ttoken, err := auth.GetToken(container, registryAuth)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tdigestURL, err := manifest.BuildManifestURL(container)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif digest, err = GetDigest(digestURL, token); err != nil {\n\t\treturn false, err\n\t}\n\n\tlogrus.WithField(\"remote\", digest).Debug(\"Found a remote digest to compare with\")\n\n\tfor _, dig := range container.ImageInfo().RepoDigests {\n\t\tlocalDigest := strings.Split(dig, \"@\")[1]\n\t\tfields := logrus.Fields{\"local\": localDigest, \"remote\": digest}\n\t\tlogrus.WithFields(fields).Debug(\"Comparing\")\n\n\t\tif localDigest == digest {\n\t\t\tlogrus.Debug(\"Found a match\")\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// TransformAuth from a base64 encoded json object to base64 encoded string\nfunc TransformAuth(registryAuth string) string {\n\tb, _ := base64.StdEncoding.DecodeString(registryAuth)\n\tcredentials := &types.RegistryCredentials{}\n\t_ = json.Unmarshal(b, credentials)\n\n\tif credentials.Username != \"\" && credentials.Password != \"\" {\n\t\tba := []byte(fmt.Sprintf(\"%s:%s\", credentials.Username, credentials.Password))\n\t\tregistryAuth = base64.StdEncoding.EncodeToString(ba)\n\t}\n\n\treturn registryAuth\n}\n\n// GetDigest from registry using a HEAD request to prevent rate limiting\nfunc GetDigest(url string, token string) (string, error) {\n\ttr := &http.Transport{\n\t\tProxy: http.ProxyFromEnvironment,\n\t\tDialContext: (&net.Dialer{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tKeepAlive: 30 * time.Second,\n\t\t}).DialContext,\n\t\tForceAttemptHTTP2:     true,\n\t\tMaxIdleConns:          100,\n\t\tIdleConnTimeout:       90 * time.Second,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t\tTLSClientConfig:       &tls.Config{InsecureSkipVerify: true},\n\t}\n\tclient := &http.Client{Transport: tr}\n\n\treq, _ := http.NewRequest(\"HEAD\", url, nil)\n\treq.Header.Set(\"User-Agent\", meta.UserAgent)\n\n\tif token == \"\" {\n\t\treturn \"\", errors.New(\"could not fetch token\")\n\t}\n\n\t// CREDENTIAL: Uncomment to log the request token\n\t// logrus.WithField(\"token\", token).Trace(\"Setting request token\")\n\n\treq.Header.Add(\"Authorization\", token)\n\treq.Header.Add(\"Accept\", \"application/vnd.docker.distribution.manifest.v2+json\")\n\treq.Header.Add(\"Accept\", \"application/vnd.docker.distribution.manifest.list.v2+json\")\n\treq.Header.Add(\"Accept\", \"application/vnd.docker.distribution.manifest.v1+json\")\n\treq.Header.Add(\"Accept\", \"application/vnd.oci.image.index.v1+json\")\n\n\tlogrus.WithField(\"url\", url).Debug(\"Doing a HEAD request to fetch a digest\")\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != 200 {\n\t\twwwAuthHeader := res.Header.Get(\"www-authenticate\")\n\t\tif wwwAuthHeader == \"\" {\n\t\t\twwwAuthHeader = \"not present\"\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"registry responded to head request with %q, auth: %q\", res.Status, wwwAuthHeader)\n\t}\n\treturn res.Header.Get(ContentDigestHeader), nil\n}\n"
  },
  {
    "path": "pkg/registry/digest/digest_test.go",
    "content": "package digest_test\n\nimport (\n\t\"fmt\"\n\t\"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t\"github.com/containrrr/watchtower/pkg/registry/digest\"\n\twtTypes \"github.com/containrrr/watchtower/pkg/types\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\t\"github.com/onsi/gomega/ghttp\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestDigest(t *testing.T) {\n\n\tRegisterFailHandler(Fail)\n\tRunSpecs(GinkgoT(), \"Digest Suite\")\n}\n\nvar (\n\tDockerHubCredentials = &wtTypes.RegistryCredentials{\n\t\tUsername: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_DH_USERNAME\"),\n\t\tPassword: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_DH_PASSWORD\"),\n\t}\n\tGHCRCredentials = &wtTypes.RegistryCredentials{\n\t\tUsername: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_GH_USERNAME\"),\n\t\tPassword: os.Getenv(\"CI_INTEGRATION_TEST_REGISTRY_GH_PASSWORD\"),\n\t}\n)\n\nfunc SkipIfCredentialsEmpty(credentials *wtTypes.RegistryCredentials, fn func()) func() {\n\tif credentials.Username == \"\" {\n\t\treturn func() {\n\t\t\tSkip(\"Username missing. Skipping integration test\")\n\t\t}\n\t} else if credentials.Password == \"\" {\n\t\treturn func() {\n\t\t\tSkip(\"Password missing. Skipping integration test\")\n\t\t}\n\t} else {\n\t\treturn fn\n\t}\n}\n\nvar _ = Describe(\"Digests\", func() {\n\tmockId := \"mock-id\"\n\tmockName := \"mock-container\"\n\tmockImage := \"ghcr.io/k6io/operator:latest\"\n\tmockCreated := time.Now()\n\tmockDigest := \"ghcr.io/k6io/operator@sha256:d68e1e532088964195ad3a0a71526bc2f11a78de0def85629beb75e2265f0547\"\n\n\tmockContainer := mocks.CreateMockContainerWithDigest(\n\t\tmockId,\n\t\tmockName,\n\t\tmockImage,\n\t\tmockCreated,\n\t\tmockDigest)\n\n\tmockContainerNoImage := mocks.CreateMockContainerWithImageInfoP(mockId, mockName, mockImage, mockCreated, nil)\n\n\tWhen(\"a digest comparison is done\", func() {\n\t\tIt(\"should return true if digests match\",\n\t\t\tSkipIfCredentialsEmpty(GHCRCredentials, func() {\n\t\t\t\tcreds := fmt.Sprintf(\"%s:%s\", GHCRCredentials.Username, GHCRCredentials.Password)\n\t\t\t\tmatches, err := digest.CompareDigest(mockContainer, creds)\n\t\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\t\tExpect(matches).To(Equal(true))\n\t\t\t}),\n\t\t)\n\n\t\tIt(\"should return false if digests differ\", func() {\n\n\t\t})\n\t\tIt(\"should return an error if the registry isn't available\", func() {\n\n\t\t})\n\t\tIt(\"should return an error when container contains no image info\", func() {\n\t\t\tmatches, err := digest.CompareDigest(mockContainerNoImage, `user:pass`)\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(matches).To(Equal(false))\n\t\t})\n\t})\n\tWhen(\"using different registries\", func() {\n\t\tIt(\"should work with DockerHub\",\n\t\t\tSkipIfCredentialsEmpty(DockerHubCredentials, func() {\n\t\t\t\tfmt.Println(DockerHubCredentials != nil) // to avoid crying linters\n\t\t\t}),\n\t\t)\n\t\tIt(\"should work with GitHub Container Registry\",\n\t\t\tSkipIfCredentialsEmpty(GHCRCredentials, func() {\n\t\t\t\tfmt.Println(GHCRCredentials != nil) // to avoid crying linters\n\t\t\t}),\n\t\t)\n\t})\n\tWhen(\"sending a HEAD request\", func() {\n\t\tvar server *ghttp.Server\n\t\tBeforeEach(func() {\n\t\t\tserver = ghttp.NewServer()\n\t\t})\n\t\tAfterEach(func() {\n\t\t\tserver.Close()\n\t\t})\n\t\tIt(\"should use a custom user-agent\", func() {\n\t\t\tserver.AppendHandlers(\n\t\t\t\tghttp.CombineHandlers(\n\t\t\t\t\tghttp.VerifyHeader(http.Header{\n\t\t\t\t\t\t\"User-Agent\": []string{\"Watchtower/v0.0.0-unknown\"},\n\t\t\t\t\t}),\n\t\t\t\t\tghttp.RespondWith(http.StatusOK, \"\", http.Header{\n\t\t\t\t\t\tdigest.ContentDigestHeader: []string{\n\t\t\t\t\t\t\tmockDigest,\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t)\n\t\t\tdig, err := digest.GetDigest(server.URL(), \"token\")\n\t\t\tExpect(server.ReceivedRequests()).Should(HaveLen(1))\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(dig).To(Equal(mockDigest))\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "pkg/registry/helpers/helpers.go",
    "content": "package helpers\n\nimport (\n\t\"github.com/distribution/reference\"\n)\n\n// domains for Docker Hub, the default registry\nconst (\n\tDefaultRegistryDomain       = \"docker.io\"\n\tDefaultRegistryHost         = \"index.docker.io\"\n\tLegacyDefaultRegistryDomain = \"index.docker.io\"\n)\n\n// GetRegistryAddress parses an image name\n// and returns the address of the specified registry\nfunc GetRegistryAddress(imageRef string) (string, error) {\n\tnormalizedRef, err := reference.ParseNormalizedNamed(imageRef)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\taddress := reference.Domain(normalizedRef)\n\n\tif address == DefaultRegistryDomain {\n\t\taddress = DefaultRegistryHost\n\t}\n\treturn address, nil\n}\n"
  },
  {
    "path": "pkg/registry/helpers/helpers_test.go",
    "content": "package helpers\n\nimport (\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestHelpers(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"Helper Suite\")\n}\n\nvar _ = Describe(\"the helpers\", func() {\n\tDescribe(\"GetRegistryAddress\", func() {\n\t\tIt(\"should return error if passed empty string\", func() {\n\t\t\t_, err := GetRegistryAddress(\"\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\t\tIt(\"should return index.docker.io for image refs with no explicit registry\", func() {\n\t\t\tExpect(GetRegistryAddress(\"watchtower\")).To(Equal(\"index.docker.io\"))\n\t\t\tExpect(GetRegistryAddress(\"containrrr/watchtower\")).To(Equal(\"index.docker.io\"))\n\t\t})\n\t\tIt(\"should return index.docker.io for image refs with docker.io domain\", func() {\n\t\t\tExpect(GetRegistryAddress(\"docker.io/watchtower\")).To(Equal(\"index.docker.io\"))\n\t\t\tExpect(GetRegistryAddress(\"docker.io/containrrr/watchtower\")).To(Equal(\"index.docker.io\"))\n\t\t})\n\t\tIt(\"should return the host if passed an image name containing a local host\", func() {\n\t\t\tExpect(GetRegistryAddress(\"henk:80/watchtower\")).To(Equal(\"henk:80\"))\n\t\t\tExpect(GetRegistryAddress(\"localhost/watchtower\")).To(Equal(\"localhost\"))\n\t\t})\n\t\tIt(\"should return the server address if passed a fully qualified image name\", func() {\n\t\t\tExpect(GetRegistryAddress(\"github.com/containrrr/config\")).To(Equal(\"github.com\"))\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "pkg/registry/manifest/manifest.go",
    "content": "package manifest\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\turl2 \"net/url\"\n\n\t\"github.com/containrrr/watchtower/pkg/registry/helpers\"\n\t\"github.com/containrrr/watchtower/pkg/types\"\n\tref \"github.com/distribution/reference\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// BuildManifestURL from raw image data\nfunc BuildManifestURL(container types.Container) (string, error) {\n\tnormalizedRef, err := ref.ParseDockerRef(container.ImageName())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tnormalizedTaggedRef, isTagged := normalizedRef.(ref.NamedTagged)\n\tif !isTagged {\n\t\treturn \"\", errors.New(\"Parsed container image ref has no tag: \" + normalizedRef.String())\n\t}\n\n\thost, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name())\n\timg, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag()\n\n\tlogrus.WithFields(logrus.Fields{\n\t\t\"image\":      img,\n\t\t\"tag\":        tag,\n\t\t\"normalized\": normalizedTaggedRef.Name(),\n\t\t\"host\":       host,\n\t}).Debug(\"Parsing image ref\")\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl := url2.URL{\n\t\tScheme: \"https\",\n\t\tHost:   host,\n\t\tPath:   fmt.Sprintf(\"/v2/%s/manifests/%s\", img, tag),\n\t}\n\treturn url.String(), nil\n}\n"
  },
  {
    "path": "pkg/registry/manifest/manifest_test.go",
    "content": "package manifest_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/internal/actions/mocks\"\n\t\"github.com/containrrr/watchtower/pkg/registry/manifest\"\n\tapiTypes \"github.com/docker/docker/api/types\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestManifest(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tRunSpecs(t, \"Manifest Suite\")\n}\n\nvar _ = Describe(\"the manifest module\", func() {\n\tDescribe(\"BuildManifestURL\", func() {\n\t\tIt(\"should return a valid url given a fully qualified image\", func() {\n\t\t\timageRef := \"ghcr.io/containrrr/watchtower:mytag\"\n\t\t\texpected := \"https://ghcr.io/v2/containrrr/watchtower/manifests/mytag\"\n\n\t\t\tURL, err := buildMockContainerManifestURL(imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(URL).To(Equal(expected))\n\t\t})\n\t\tIt(\"should assume Docker Hub for image refs with no explicit registry\", func() {\n\t\t\timageRef := \"containrrr/watchtower:latest\"\n\t\t\texpected := \"https://index.docker.io/v2/containrrr/watchtower/manifests/latest\"\n\n\t\t\tURL, err := buildMockContainerManifestURL(imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(URL).To(Equal(expected))\n\t\t})\n\t\tIt(\"should assume latest for image refs with no explicit tag\", func() {\n\t\t\timageRef := \"containrrr/watchtower\"\n\t\t\texpected := \"https://index.docker.io/v2/containrrr/watchtower/manifests/latest\"\n\n\t\t\tURL, err := buildMockContainerManifestURL(imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(URL).To(Equal(expected))\n\t\t})\n\t\tIt(\"should not prepend library/ for single-part container names in registries other than Docker Hub\", func() {\n\t\t\timageRef := \"docker-registry.domain/imagename:latest\"\n\t\t\texpected := \"https://docker-registry.domain/v2/imagename/manifests/latest\"\n\n\t\t\tURL, err := buildMockContainerManifestURL(imageRef)\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t\tExpect(URL).To(Equal(expected))\n\t\t})\n\t\tIt(\"should throw an error on pinned images\", func() {\n\t\t\timageRef := \"docker-registry.domain/imagename@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7\"\n\t\t\tURL, err := buildMockContainerManifestURL(imageRef)\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t\tExpect(URL).To(BeEmpty())\n\t\t})\n\t})\n})\n\nfunc buildMockContainerManifestURL(imageRef string) (string, error) {\n\timageInfo := apiTypes.ImageInspect{\n\t\tRepoTags: []string{\n\t\t\timageRef,\n\t\t},\n\t}\n\tmockID := \"mock-id\"\n\tmockName := \"mock-container\"\n\tmockCreated := time.Now()\n\tmock := mocks.CreateMockContainerWithImageInfo(mockID, mockName, imageRef, mockCreated, imageInfo)\n\n\treturn manifest.BuildManifestURL(mock)\n}\n"
  },
  {
    "path": "pkg/registry/registry.go",
    "content": "package registry\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/registry/helpers\"\n\twatchtowerTypes \"github.com/containrrr/watchtower/pkg/types\"\n\tref \"github.com/distribution/reference\"\n\t\"github.com/docker/docker/api/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// GetPullOptions creates a struct with all options needed for pulling images from a registry\nfunc GetPullOptions(imageName string) (types.ImagePullOptions, error) {\n\tauth, err := EncodedAuth(imageName)\n\tlog.Debugf(\"Got image name: %s\", imageName)\n\tif err != nil {\n\t\treturn types.ImagePullOptions{}, err\n\t}\n\n\tif auth == \"\" {\n\t\treturn types.ImagePullOptions{}, nil\n\t}\n\n\t// CREDENTIAL: Uncomment to log docker config auth\n\t// log.Tracef(\"Got auth value: %s\", auth)\n\n\treturn types.ImagePullOptions{\n\t\tRegistryAuth:  auth,\n\t\tPrivilegeFunc: DefaultAuthHandler,\n\t}, nil\n}\n\n// DefaultAuthHandler will be invoked if an AuthConfig is rejected\n// It could be used to return a new value for the \"X-Registry-Auth\" authentication header,\n// but there's no point trying again with the same value as used in AuthConfig\nfunc DefaultAuthHandler() (string, error) {\n\tlog.Debug(\"Authentication request was rejected. Trying again without authentication\")\n\treturn \"\", nil\n}\n\n// WarnOnAPIConsumption will return true if the registry is known-expected\n// to respond well to HTTP HEAD in checking the container digest -- or if there\n// are problems parsing the container hostname.\n// Will return false if behavior for container is unknown.\nfunc WarnOnAPIConsumption(container watchtowerTypes.Container) bool {\n\n\tnormalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())\n\tif err != nil {\n\t\treturn true\n\t}\n\n\tcontainerHost, err := helpers.GetRegistryAddress(normalizedRef.Name())\n\tif err != nil {\n\t\treturn true\n\t}\n\n\tif containerHost == helpers.DefaultRegistryHost || containerHost == \"ghcr.io\" {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/registry/registry_suite_test.go",
    "content": "package registry_test\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"testing\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nfunc TestRegistry(t *testing.T) {\n\tRegisterFailHandler(Fail)\n\tlogrus.SetOutput(GinkgoWriter)\n\tRunSpecs(t, \"Registry Suite\")\n}\n"
  },
  {
    "path": "pkg/registry/registry_test.go",
    "content": "package registry_test\n\nimport (\n\t\"github.com/containrrr/watchtower/internal/actions/mocks\"\n\tunit \"github.com/containrrr/watchtower/pkg/registry\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"time\"\n)\n\nvar _ = Describe(\"Registry\", func() {\n\tDescribe(\"WarnOnAPIConsumption\", func() {\n\t\tWhen(\"Given a container with an image from ghcr.io\", func() {\n\t\t\tIt(\"should want to warn\", func() {\n\t\t\t\tExpect(testContainerWithImage(\"ghcr.io/containrrr/watchtower\")).To(BeTrue())\n\t\t\t})\n\t\t})\n\t\tWhen(\"Given a container with an image implicitly from dockerhub\", func() {\n\t\t\tIt(\"should want to warn\", func() {\n\t\t\t\tExpect(testContainerWithImage(\"docker:latest\")).To(BeTrue())\n\t\t\t})\n\t\t})\n\t\tWhen(\"Given a container with an image explicitly from dockerhub\", func() {\n\t\t\tIt(\"should want to warn\", func() {\n\t\t\t\tExpect(testContainerWithImage(\"index.docker.io/docker:latest\")).To(BeTrue())\n\t\t\t\tExpect(testContainerWithImage(\"docker.io/docker:latest\")).To(BeTrue())\n\t\t\t})\n\t\t})\n\t\tWhen(\"Given a container with an image from some other registry\", func() {\n\t\t\tIt(\"should not want to warn\", func() {\n\t\t\t\tExpect(testContainerWithImage(\"docker.fsf.org/docker:latest\")).To(BeFalse())\n\t\t\t\tExpect(testContainerWithImage(\"altavista.com/docker:latest\")).To(BeFalse())\n\t\t\t\tExpect(testContainerWithImage(\"gitlab.com/docker:latest\")).To(BeFalse())\n\t\t\t})\n\t\t})\n\t})\n})\n\nfunc testContainerWithImage(imageName string) bool {\n\tcontainer := mocks.CreateMockContainer(\"\", \"\", imageName, time.Now())\n\treturn unit.WarnOnAPIConsumption(container)\n}\n"
  },
  {
    "path": "pkg/registry/trust.go",
    "content": "package registry\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/containrrr/watchtower/pkg/registry/helpers\"\n\tcliconfig \"github.com/docker/cli/cli/config\"\n\t\"github.com/docker/cli/cli/config/configfile\"\n\t\"github.com/docker/cli/cli/config/credentials\"\n\t\"github.com/docker/cli/cli/config/types\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// EncodedAuth returns an encoded auth config for the given registry\n// loaded from environment variables or docker config\n// as available in that order\nfunc EncodedAuth(ref string) (string, error) {\n\tauth, err := EncodedEnvAuth()\n\tif err != nil {\n\t\tauth, err = EncodedConfigAuth(ref)\n\t}\n\treturn auth, err\n}\n\n// EncodedEnvAuth returns an encoded auth config for the given registry\n// loaded from environment variables\n// Returns an error if authentication environment variables have not been set\nfunc EncodedEnvAuth() (string, error) {\n\tusername := os.Getenv(\"REPO_USER\")\n\tpassword := os.Getenv(\"REPO_PASS\")\n\tif username != \"\" && password != \"\" {\n\t\tauth := types.AuthConfig{\n\t\t\tUsername: username,\n\t\t\tPassword: password,\n\t\t}\n    \n\t\tlog.Debugf(\"Loaded auth credentials for registry user %s from environment\", auth.Username)\n\t\t// CREDENTIAL: Uncomment to log REPO_PASS environment variable\n\t\t// log.Tracef(\"Using auth password %s\", auth.Password)\n\n\t\treturn EncodeAuth(auth)\n\t}\n\treturn \"\", errors.New(\"registry auth environment variables (REPO_USER, REPO_PASS) not set\")\n}\n\n// EncodedConfigAuth returns an encoded auth config for the given registry\n// loaded from the docker config\n// Returns an empty string if credentials cannot be found for the referenced server\n// The docker config must be mounted on the container\nfunc EncodedConfigAuth(imageRef string) (string, error) {\n\tserver, err := helpers.GetRegistryAddress(imageRef)\n\tif err != nil {\n\t\tlog.Errorf(\"Could not get registry from image ref %s\", imageRef)\n\t\treturn \"\", err\n\t}\n\n\tconfigDir := os.Getenv(\"DOCKER_CONFIG\")\n\tif configDir == \"\" {\n\t\tconfigDir = \"/\"\n\t}\n\tconfigFile, err := cliconfig.Load(configDir)\n\tif err != nil {\n\t\tlog.Errorf(\"Unable to find default config file: %s\", err)\n\t\treturn \"\", err\n\t}\n\tcredStore := CredentialsStore(*configFile)\n\tauth, _ := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore\n\n\tif auth == (types.AuthConfig{}) {\n\t\tlog.WithField(\"config_file\", configFile.Filename).Debugf(\"No credentials for %s found\", server)\n\t\treturn \"\", nil\n\t}\n\tlog.Debugf(\"Loaded auth credentials for user %s, on registry %s, from file %s\", auth.Username, server, configFile.Filename)\n\t// CREDENTIAL: Uncomment to log docker config password\n\t// log.Tracef(\"Using auth password %s\", auth.Password)\n\treturn EncodeAuth(auth)\n}\n\n// CredentialsStore returns a new credentials store based\n// on the settings provided in the configuration file.\nfunc CredentialsStore(configFile configfile.ConfigFile) credentials.Store {\n\tif configFile.CredentialsStore != \"\" {\n\t\treturn credentials.NewNativeStore(&configFile, configFile.CredentialsStore)\n\t}\n\treturn credentials.NewFileStore(&configFile)\n}\n\n// EncodeAuth Base64 encode an AuthConfig struct for transmission over HTTP\nfunc EncodeAuth(authConfig types.AuthConfig) (string, error) {\n\tbuf, err := json.Marshal(authConfig)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.URLEncoding.EncodeToString(buf), nil\n}\n"
  },
  {
    "path": "pkg/registry/trust_test.go",
    "content": "package registry\n\nimport (\n\t\"os\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n)\n\nvar _ = Describe(\"Registry credential helpers\", func() {\n\tDescribe(\"EncodedAuth\", func() {\n\t\tIt(\"should return repo credentials from env when set\", func() {\n\t\t\tvar err error\n\t\t\texpected := \"eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=\"\n\n\t\t\terr = os.Setenv(\"REPO_USER\", \"containrrr-user\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\terr = os.Setenv(\"REPO_PASS\", \"containrrr-pass\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\tconfig, err := EncodedEnvAuth()\n\t\t\tExpect(config).To(Equal(expected))\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\t\t})\n\t})\n\n\tDescribe(\"EncodedEnvAuth\", func() {\n\t\tIt(\"should return an error if repo envs are unset\", func() {\n\t\t\t_ = os.Unsetenv(\"REPO_USER\")\n\t\t\t_ = os.Unsetenv(\"REPO_PASS\")\n\n\t\t\t_, err := EncodedEnvAuth()\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\t})\n\n\tDescribe(\"EncodedConfigAuth\", func() {\n\t\tIt(\"should return an error if file is not present\", func() {\n\t\t\tvar err error\n\n\t\t\terr = os.Setenv(\"DOCKER_CONFIG\", \"/dev/null/should-fail\")\n\t\t\tExpect(err).NotTo(HaveOccurred())\n\n\t\t\t_, err = EncodedConfigAuth(\"\")\n\t\t\tExpect(err).To(HaveOccurred())\n\t\t})\n\t})\n})\n"
  },
  {
    "path": "pkg/session/container_status.go",
    "content": "package session\n\nimport wt \"github.com/containrrr/watchtower/pkg/types\"\n\n// State indicates what the current state is of the container\ntype State int\n\n// State enum values\nconst (\n\t// UnknownState is only used to represent an uninitialized State value\n\tUnknownState State = iota\n\tSkippedState\n\tScannedState\n\tUpdatedState\n\tFailedState\n\tFreshState\n\tStaleState\n)\n\n// ContainerStatus contains the container state during a session\ntype ContainerStatus struct {\n\tcontainerID   wt.ContainerID\n\toldImage      wt.ImageID\n\tnewImage      wt.ImageID\n\tcontainerName string\n\timageName     string\n\terror\n\tstate State\n}\n\n// ID returns the container ID\nfunc (u *ContainerStatus) ID() wt.ContainerID {\n\treturn u.containerID\n}\n\n// Name returns the container name\nfunc (u *ContainerStatus) Name() string {\n\treturn u.containerName\n}\n\n// CurrentImageID returns the image ID that the container used when the session started\nfunc (u *ContainerStatus) CurrentImageID() wt.ImageID {\n\treturn u.oldImage\n}\n\n// LatestImageID returns the newest image ID found during the session\nfunc (u *ContainerStatus) LatestImageID() wt.ImageID {\n\treturn u.newImage\n}\n\n// ImageName returns the name:tag that the container uses\nfunc (u *ContainerStatus) ImageName() string {\n\treturn u.imageName\n}\n\n// Error returns the error (if any) that was encountered for the container during a session\nfunc (u *ContainerStatus) Error() string {\n\tif u.error == nil {\n\t\treturn \"\"\n\t}\n\treturn u.error.Error()\n}\n\n// State returns the current State that the container is in\nfunc (u *ContainerStatus) State() string {\n\tswitch u.state {\n\tcase SkippedState:\n\t\treturn \"Skipped\"\n\tcase ScannedState:\n\t\treturn \"Scanned\"\n\tcase UpdatedState:\n\t\treturn \"Updated\"\n\tcase FailedState:\n\t\treturn \"Failed\"\n\tcase FreshState:\n\t\treturn \"Fresh\"\n\tcase StaleState:\n\t\treturn \"Stale\"\n\tdefault:\n\t\treturn \"Unknown\"\n\t}\n}\n"
  },
  {
    "path": "pkg/session/progress.go",
    "content": "package session\n\nimport (\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// Progress contains the current session container status\ntype Progress map[types.ContainerID]*ContainerStatus\n\n// UpdateFromContainer sets various status fields from their corresponding container equivalents\nfunc UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus {\n\treturn &ContainerStatus{\n\t\tcontainerID:   cont.ID(),\n\t\tcontainerName: cont.Name(),\n\t\timageName:     cont.ImageName(),\n\t\toldImage:      cont.SafeImageID(),\n\t\tnewImage:      newImage,\n\t\tstate:         state,\n\t}\n}\n\n// AddSkipped adds a container to the Progress with the state set as skipped\nfunc (m Progress) AddSkipped(cont types.Container, err error) {\n\tupdate := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState)\n\tupdate.error = err\n\tm.Add(update)\n}\n\n// AddScanned adds a container to the Progress with the state set as scanned\nfunc (m Progress) AddScanned(cont types.Container, newImage types.ImageID) {\n\tm.Add(UpdateFromContainer(cont, newImage, ScannedState))\n}\n\n// UpdateFailed updates the containers passed, setting their state as failed with the supplied error\nfunc (m Progress) UpdateFailed(failures map[types.ContainerID]error) {\n\tfor id, err := range failures {\n\t\tupdate := m[id]\n\t\tupdate.error = err\n\t\tupdate.state = FailedState\n\t}\n}\n\n// Add a container to the map using container ID as the key\nfunc (m Progress) Add(update *ContainerStatus) {\n\tm[update.containerID] = update\n}\n\n// MarkForUpdate marks the container identified by containerID for update\nfunc (m Progress) MarkForUpdate(containerID types.ContainerID) {\n\tm[containerID].state = UpdatedState\n}\n\n// Report creates a new Report from a Progress instance\nfunc (m Progress) Report() types.Report {\n\treturn NewReport(m)\n}\n"
  },
  {
    "path": "pkg/session/report.go",
    "content": "package session\n\nimport (\n\t\"sort\"\n\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\ntype report struct {\n\tscanned []types.ContainerReport\n\tupdated []types.ContainerReport\n\tfailed  []types.ContainerReport\n\tskipped []types.ContainerReport\n\tstale   []types.ContainerReport\n\tfresh   []types.ContainerReport\n}\n\nfunc (r *report) Scanned() []types.ContainerReport {\n\treturn r.scanned\n}\nfunc (r *report) Updated() []types.ContainerReport {\n\treturn r.updated\n}\nfunc (r *report) Failed() []types.ContainerReport {\n\treturn r.failed\n}\nfunc (r *report) Skipped() []types.ContainerReport {\n\treturn r.skipped\n}\nfunc (r *report) Stale() []types.ContainerReport {\n\treturn r.stale\n}\nfunc (r *report) Fresh() []types.ContainerReport {\n\treturn r.fresh\n}\nfunc (r *report) All() []types.ContainerReport {\n\tallLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)\n\tall := make([]types.ContainerReport, 0, allLen)\n\n\tpresentIds := map[types.ContainerID][]string{}\n\n\tappendUnique := func(reports []types.ContainerReport) {\n\t\tfor _, cr := range reports {\n\t\t\tif _, found := presentIds[cr.ID()]; found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tall = append(all, cr)\n\t\t\tpresentIds[cr.ID()] = nil\n\t\t}\n\t}\n\n\tappendUnique(r.updated)\n\tappendUnique(r.failed)\n\tappendUnique(r.skipped)\n\tappendUnique(r.stale)\n\tappendUnique(r.fresh)\n\tappendUnique(r.scanned)\n\n\tsort.Sort(sortableContainers(all))\n\n\treturn all\n}\n\n// NewReport creates a types.Report from the supplied Progress\nfunc NewReport(progress Progress) types.Report {\n\treport := &report{\n\t\tscanned: []types.ContainerReport{},\n\t\tupdated: []types.ContainerReport{},\n\t\tfailed:  []types.ContainerReport{},\n\t\tskipped: []types.ContainerReport{},\n\t\tstale:   []types.ContainerReport{},\n\t\tfresh:   []types.ContainerReport{},\n\t}\n\n\tfor _, update := range progress {\n\t\tif update.state == SkippedState {\n\t\t\treport.skipped = append(report.skipped, update)\n\t\t\tcontinue\n\t\t}\n\n\t\treport.scanned = append(report.scanned, update)\n\t\tif update.newImage == update.oldImage {\n\t\t\tupdate.state = FreshState\n\t\t\treport.fresh = append(report.fresh, update)\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch update.state {\n\t\tcase UpdatedState:\n\t\t\treport.updated = append(report.updated, update)\n\t\tcase FailedState:\n\t\t\treport.failed = append(report.failed, update)\n\t\tdefault:\n\t\t\tupdate.state = StaleState\n\t\t\treport.stale = append(report.stale, update)\n\t\t}\n\t}\n\n\tsort.Sort(sortableContainers(report.scanned))\n\tsort.Sort(sortableContainers(report.updated))\n\tsort.Sort(sortableContainers(report.failed))\n\tsort.Sort(sortableContainers(report.skipped))\n\tsort.Sort(sortableContainers(report.stale))\n\tsort.Sort(sortableContainers(report.fresh))\n\n\treturn report\n}\n\ntype sortableContainers []types.ContainerReport\n\n// Len implements sort.Interface.Len\nfunc (s sortableContainers) Len() int { return len(s) }\n\n// Less implements sort.Interface.Less\nfunc (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }\n\n// Swap implements sort.Interface.Swap\nfunc (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\n"
  },
  {
    "path": "pkg/sorter/sort.go",
    "content": "package sorter\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/containrrr/watchtower/pkg/types\"\n)\n\n// ByCreated allows a list of Container structs to be sorted by the container's\n// created date.\ntype ByCreated []types.Container\n\nfunc (c ByCreated) Len() int      { return len(c) }\nfunc (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }\n\n// Less will compare two elements (identified by index) in the Container\n// list by created-date.\nfunc (c ByCreated) Less(i, j int) bool {\n\tt1, err := time.Parse(time.RFC3339Nano, c[i].ContainerInfo().Created)\n\tif err != nil {\n\t\tt1 = time.Now()\n\t}\n\n\tt2, _ := time.Parse(time.RFC3339Nano, c[j].ContainerInfo().Created)\n\tif err != nil {\n\t\tt1 = time.Now()\n\t}\n\n\treturn t1.Before(t2)\n}\n\n// SortByDependencies will sort the list of containers taking into account any\n// links between containers. Container with no outgoing links will be sorted to\n// the front of the list while containers with links will be sorted after all\n// of their dependencies. This sort order ensures that linked containers can\n// be started in the correct order.\nfunc SortByDependencies(containers []types.Container) ([]types.Container, error) {\n\tsorter := dependencySorter{}\n\treturn sorter.Sort(containers)\n}\n\ntype dependencySorter struct {\n\tunvisited []types.Container\n\tmarked    map[string]bool\n\tsorted    []types.Container\n}\n\nfunc (ds *dependencySorter) Sort(containers []types.Container) ([]types.Container, error) {\n\tds.unvisited = containers\n\tds.marked = map[string]bool{}\n\n\tfor len(ds.unvisited) > 0 {\n\t\tif err := ds.visit(ds.unvisited[0]); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn ds.sorted, nil\n}\n\nfunc (ds *dependencySorter) visit(c types.Container) error {\n\n\tif _, ok := ds.marked[c.Name()]; ok {\n\t\treturn fmt.Errorf(\"circular reference to %s\", c.Name())\n\t}\n\n\t// Mark any visited node so that circular references can be detected\n\tds.marked[c.Name()] = true\n\tdefer delete(ds.marked, c.Name())\n\n\t// Recursively visit links\n\tfor _, linkName := range c.Links() {\n\t\tif linkedContainer := ds.findUnvisited(linkName); linkedContainer != nil {\n\t\t\tif err := ds.visit(*linkedContainer); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Move container from unvisited to sorted\n\tds.removeUnvisited(c)\n\tds.sorted = append(ds.sorted, c)\n\n\treturn nil\n}\n\nfunc (ds *dependencySorter) findUnvisited(name string) *types.Container {\n\tfor _, c := range ds.unvisited {\n\t\tif c.Name() == name {\n\t\t\treturn &c\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (ds *dependencySorter) removeUnvisited(c types.Container) {\n\tvar idx int\n\tfor i := range ds.unvisited {\n\t\tif ds.unvisited[i].Name() == c.Name() {\n\t\t\tidx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tds.unvisited = append(ds.unvisited[0:idx], ds.unvisited[idx+1:]...)\n}\n"
  },
  {
    "path": "pkg/types/container.go",
    "content": "package types\n\nimport (\n\t\"strings\"\n\n\t\"github.com/docker/docker/api/types\"\n\tdc \"github.com/docker/docker/api/types/container\"\n)\n\n// ImageID is a hash string representing a container image\ntype ImageID string\n\n// ContainerID is a hash string representing a container instance\ntype ContainerID string\n\n// ShortID returns the 12-character (hex) short version of an image ID hash, removing any \"sha256:\" prefix if present\nfunc (id ImageID) ShortID() (short string) {\n\treturn shortID(string(id))\n}\n\n// ShortID returns the 12-character (hex) short version of a container ID hash, removing any \"sha256:\" prefix if present\nfunc (id ContainerID) ShortID() (short string) {\n\treturn shortID(string(id))\n}\n\nfunc shortID(longID string) string {\n\tprefixSep := strings.IndexRune(longID, ':')\n\toffset := 0\n\tlength := 12\n\tif prefixSep >= 0 {\n\t\tif longID[0:prefixSep] == \"sha256\" {\n\t\t\toffset = prefixSep + 1\n\t\t} else {\n\t\t\tlength += prefixSep + 1\n\t\t}\n\t}\n\n\tif len(longID) >= offset+length {\n\t\treturn longID[offset : offset+length]\n\t}\n\n\treturn longID\n}\n\n// Container is a docker container running an image\ntype Container interface {\n\tContainerInfo() *types.ContainerJSON\n\tID() ContainerID\n\tIsRunning() bool\n\tName() string\n\tImageID() ImageID\n\tSafeImageID() ImageID\n\tImageName() string\n\tEnabled() (bool, bool)\n\tIsMonitorOnly(UpdateParams) bool\n\tScope() (string, bool)\n\tLinks() []string\n\tToRestart() bool\n\tIsWatchtower() bool\n\tStopSignal() string\n\tHasImageInfo() bool\n\tImageInfo() *types.ImageInspect\n\tGetLifecyclePreCheckCommand() string\n\tGetLifecyclePostCheckCommand() string\n\tGetLifecyclePreUpdateCommand() string\n\tGetLifecyclePostUpdateCommand() string\n\tVerifyConfiguration() error\n\tSetStale(bool)\n\tIsStale() bool\n\tIsNoPull(UpdateParams) bool\n\tSetLinkedToRestarting(bool)\n\tIsLinkedToRestarting() bool\n\tPreUpdateTimeout() int\n\tPostUpdateTimeout() int\n\tIsRestarting() bool\n\tGetCreateConfig() *dc.Config\n\tGetCreateHostConfig() *dc.HostConfig\n}\n"
  },
  {
    "path": "pkg/types/convertible_notifier.go",
    "content": "package types\n\nimport (\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// ConvertibleNotifier is a notifier capable of creating a shoutrrr URL\ntype ConvertibleNotifier interface {\n\tGetURL(c *cobra.Command) (string, error)\n}\n\n// DelayNotifier is a notifier that might need to be delayed before sending notifications\ntype DelayNotifier interface {\n\tGetDelay() time.Duration\n}\n"
  },
  {
    "path": "pkg/types/filter.go",
    "content": "package types\n\n// A Filter is a prototype for a function that can be used to filter the\n// results from a call to the ListContainers() method on the Client.\ntype Filter func(FilterableContainer) bool\n"
  },
  {
    "path": "pkg/types/filterable_container.go",
    "content": "package types\n\n// A FilterableContainer is the interface which is used to filter\n// containers.\ntype FilterableContainer interface {\n\tName() string\n\tIsWatchtower() bool\n\tEnabled() (bool, bool)\n\tScope() (string, bool)\n\tImageName() string\n}\n"
  },
  {
    "path": "pkg/types/notifier.go",
    "content": "package types\n\n// Notifier is the interface that all notification services have in common\ntype Notifier interface {\n\tStartNotification()\n\tSendNotification(Report)\n\tAddLogHook()\n\tGetNames() []string\n\tGetURLs() []string\n\tClose()\n}\n"
  },
  {
    "path": "pkg/types/registry_credentials.go",
    "content": "package types\n\n// RegistryCredentials is a credential pair used for basic auth\ntype RegistryCredentials struct {\n\tUsername string\n\tPassword string // usually a token rather than an actual password\n}\n"
  },
  {
    "path": "pkg/types/report.go",
    "content": "package types\n\n// Report contains reports for all the containers processed during a session\ntype Report interface {\n\tScanned() []ContainerReport\n\tUpdated() []ContainerReport\n\tFailed() []ContainerReport\n\tSkipped() []ContainerReport\n\tStale() []ContainerReport\n\tFresh() []ContainerReport\n\tAll() []ContainerReport\n}\n\n// ContainerReport represents a container that was included in watchtower session\ntype ContainerReport interface {\n\tID() ContainerID\n\tName() string\n\tCurrentImageID() ImageID\n\tLatestImageID() ImageID\n\tImageName() string\n\tError() string\n\tState() string\n}\n"
  },
  {
    "path": "pkg/types/token_response.go",
    "content": "package types\n\n// TokenResponse is returned by the registry on successful authentication\ntype TokenResponse struct {\n\tToken string `json:\"token\"`\n}\n"
  },
  {
    "path": "pkg/types/update_params.go",
    "content": "package types\n\nimport (\n\t\"time\"\n)\n\n// UpdateParams contains all different options available to alter the behavior of the Update func\ntype UpdateParams struct {\n\tFilter          Filter\n\tCleanup         bool\n\tNoRestart       bool\n\tTimeout         time.Duration\n\tMonitorOnly     bool\n\tNoPull\t\t\tbool\n\tLifecycleHooks  bool\n\tRollingRestart  bool\n\tLabelPrecedence bool\n}\n"
  },
  {
    "path": "prometheus/prometheus.yml",
    "content": "scrape_configs:\n  - job_name: watchtower\n    scrape_interval: 5s\n    metrics_path: /v1/metrics\n    bearer_token: demotoken\n    static_configs:\n      - targets:\n        - 'watchtower:8080'\n    \n"
  },
  {
    "path": "scripts/build-tplprev.sh",
    "content": "#!/bin/bash\n\ncd $(git rev-parse --show-toplevel)\n\ncp \"$(go env GOROOT)/misc/wasm/wasm_exec.js\" ./docs/assets/\n\nGOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev"
  },
  {
    "path": "scripts/codecov.sh",
    "content": "#!/usr/bin/env bash\n\ngo test -v -coverprofile coverage.out -covermode atomic ./...\n\n# Requires CODECOV_TOKEN to be set\nbash <(curl -s https://codecov.io/bash)"
  },
  {
    "path": "scripts/contnet-tests.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nfunction exit_env_err() {\n  >&2 echo \"Required environment variable not set: $1\"\n  exit 1\n}\n\nif [ -z \"$VPN_SERVICE_PROVIDER\" ]; then exit_env_err \"VPN_SERVICE_PROVIDER\"; fi\nif [ -z \"$OPENVPN_USER\" ]; then exit_env_err \"OPENVPN_USER\"; fi\nif [ -z \"$OPENVPN_PASSWORD\" ]; then exit_env_err \"OPENVPN_PASSWORD\"; fi\n# if [ -z \"$SERVER_COUNTRIES\" ]; then exit_env_err \"SERVER_COUNTRIES\"; fi\n\n\nexport SERVER_COUNTRIES=${SERVER_COUNTRIES:\"Sweden\"}\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nCOMPOSE_FILE=\"$REPO_ROOT/dockerfiles/container-networking/docker-compose.yml\"\nDEFAULT_WATCHTOWER=\"$REPO_ROOT/watchtower\"\nWATCHTOWER=\"$*\"\nWATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}\necho \"repo root path is $REPO_ROOT\"\necho \"watchtower path is $WATCHTOWER\"\necho \"compose file path is $COMPOSE_FILE\"\n\necho; echo \"=== Forcing network container producer update...\"\n\necho \"Pull previous version of gluetun...\"\ndocker pull qmcgaw/gluetun:v3.34.3\necho \"Fake new version of gluetun by retagging v3.34.4 as v3.35.0...\"\ndocker tag qmcgaw/gluetun:v3.34.3  qmcgaw/gluetun:v3.35.0\n\necho; echo \"=== Creating containers...\"\n\ndocker compose -p \"wt-contnet\" -f \"$COMPOSE_FILE\" up -d\n\necho; echo \"=== Running watchtower\"\n$WATCHTOWER --run-once\n\necho; echo \"=== Removing containers...\"\n\ndocker compose -p \"wt-contnet\" -f \"$COMPOSE_FILE\" down\n"
  },
  {
    "path": "scripts/dependency-test.sh",
    "content": "#!/usr/bin/env bash\n\n# Simulates a container that will always be updated, checking whether it shuts down it's dependencies correctly.\n# Note that this test does not verify the results in any way\n\nset -e\nSCRIPT_ROOT=$(dirname \"$(readlink -m \"$(type -p \"$0\")\")\")\nsource \"$SCRIPT_ROOT/docker-util.sh\"\n\nDepArgs=\"\"\nif [ -z \"$1\" ] || [ \"$1\" == \"depends-on\" ]; then\n  DepArgs=\"--label com.centurylinklabs.watchtower.depends-on=parent\"\nelif [ \"$1\" == \"linked\" ]; then\n  DepArgs=\"--link parent\"\nelse\n  DepArgs=$1\nfi\n\nWatchArgs=\"${*:2}\"\nif [ -z \"$WatchArgs\" ]; then\n  WatchArgs=\"--debug\"\nfi\n\ntry-remove-container parent\ntry-remove-container depending\n\nREPO=$(registry-host)\n\ncreate-dummy-image deptest/parent\ncreate-dummy-image deptest/depending\n\necho \"\"\n\necho -en \"Starting \\e[94mparent\\e[0m container... \"\nCmdParent=\"docker run -d -p 9090 --name parent $REPO/deptest/parent\"\n$CmdParent\nPARENT_REV_BEFORE=$(query-rev parent)\nPARENT_START_BEFORE=$(container-started parent)\necho -e \"Rev: \\e[92m$PARENT_REV_BEFORE\\e[0m\"\necho -e \"Started: \\e[96m$PARENT_START_BEFORE\\e[0m\"\necho -e \"Command: \\e[37m$CmdParent\\e[0m\"\n\necho \"\"\n\necho -en \"Starting \\e[94mdepending\\e[0m container... \"\nCmdDepend=\"docker run -d -p 9090 --name depending $DepArgs $REPO/deptest/depending\"\n$CmdDepend\nDEPEND_REV_BEFORE=$(query-rev depending)\nDEPEND_START_BEFORE=$(container-started depending)\necho -e \"Rev: \\e[92m$DEPEND_REV_BEFORE\\e[0m\"\necho -e \"Started: \\e[96m$DEPEND_START_BEFORE\\e[0m\"\necho -e \"Command: \\e[37m$CmdDepend\\e[0m\"\n\necho -e \"\"\n\ncreate-dummy-image deptest/parent\n\necho -e \"\\nRunning watchtower...\"\n\nif [ -z \"$WATCHTOWER_TAG\" ]; then\n  ## Windows support:\n  #export DOCKER_HOST=tcp://localhost:2375\n  #export CLICOLOR=1\n  go run . --run-once $WatchArgs\nelse\n  docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower:\"$WATCHTOWER_TAG\" --run-once $WatchArgs\nfi\n\necho -e \"\\nSession results:\"\n\nPARENT_REV_AFTER=$(query-rev parent)\nPARENT_START_AFTER=$(container-started parent)\necho -en \"  Parent image: \\e[95m$PARENT_REV_BEFORE\\e[0m => \\e[94m$PARENT_REV_AFTER\\e[0m \"\nif [ \"$PARENT_REV_AFTER\" == \"$PARENT_REV_BEFORE\" ]; then\n  echo -e \"(\\e[91mSame\\e[0m)\"\nelse\n  echo -e \"(\\e[92mUpdated\\e[0m)\"\nfi\necho -en \"  Parent container: \\e[95m$PARENT_START_BEFORE\\e[0m => \\e[94m$PARENT_START_AFTER\\e[0m \"\nif [ \"$PARENT_START_AFTER\" == \"$PARENT_START_BEFORE\" ]; then\n  echo -e \"(\\e[91mSame\\e[0m)\"\nelse\n  echo -e \"(\\e[92mRestarted\\e[0m)\"\nfi\n\necho \"\"\n\nDEPEND_REV_AFTER=$(query-rev depending)\nDEPEND_START_AFTER=$(container-started depending)\necho -en \"  Depend image: \\e[95m$DEPEND_REV_BEFORE\\e[0m => \\e[94m$DEPEND_REV_AFTER\\e[0m \"\nif [ \"$DEPEND_REV_BEFORE\" == \"$DEPEND_REV_AFTER\" ]; then\n  echo -e \"(\\e[92mSame\\e[0m)\"\nelse\n  echo -e \"(\\e[91mUpdated\\e[0m)\"\nfi\necho -en \"  Depend container: \\e[95m$DEPEND_START_BEFORE\\e[0m => \\e[94m$DEPEND_START_AFTER\\e[0m \"\nif [ \"$DEPEND_START_BEFORE\" == \"$DEPEND_START_AFTER\" ]; then\n  echo -e \"(\\e[91mSame\\e[0m)\"\nelse\n  echo -e \"(\\e[92mRestarted\\e[0m)\"\nfi\n\necho \"\""
  },
  {
    "path": "scripts/docker-util.sh",
    "content": "#!/usr/bin/env bash\n# This file is meant to be sourced into other scripts and contain some utility functions for docker e2e testing\n\n\nCONTAINER_PREFIX=${CONTAINER_PREFIX:-du}\n\nfunction get-port() {\n    Container=$1\n    Port=$2\n\n    if [ -z \"$Container\" ];  then\n      echo \"CONTAINER missing\" 1>&2\n      return 1\n    fi\n\n    if [ -z \"$Port\" ];  then\n      echo \"PORT missing\" 1>&2\n      return 1\n    fi\n\n    Query=\".[].NetworkSettings.Ports[\\\"$Port/tcp\\\"] | .[0].HostPort\"\n    docker container inspect \"$Container\" | jq -r \"$Query\"\n}\n\nfunction start-registry() {\n  local Name=\"$CONTAINER_PREFIX-registry\"\n  echo -en \"Starting \\e[94m$Name\\e[0m container... \"\n  local Port=\"${1:-5000}\"\n  docker run -d -p 5000:\"$Port\" --restart=unless-stopped --name \"$Name\" registry:2\n}\n\nfunction stop-registry() {\n  try-remove-container \"$CONTAINER_PREFIX-registry\"\n}\n\nfunction registry-host() {\n  echo \"localhost:$(get-port \"$CONTAINER_PREFIX\"-registry 5000)\"\n}\n\nfunction try-remove-container() {\n  echo -en \"Looking for container \\e[95m$1\\e[0m... \"\n  local Found\n  Found=$(container-id \"$1\")\n  if [ -n \"$Found\" ]; then\n    echo \"$Found\"\n    echo -n \"  Stopping... \"\n    docker stop \"$1\"\n    echo -n \"  Removing... \"\n    docker rm \"$1\"\n  else\n    echo \"Not found\"\n  fi\n}\n\nfunction create-dummy-image() {\n    if [ -z \"$1\" ];  then\n      echo \"TAG missing\"\n      return 1\n    fi\n    local Tag=\"$1\"\n    local Repo\n    Repo=\"$(registry-host)\"\n    local Revision=${2:-$((\"$(date +%s)\" - \"$(date --date='2021-10-21' +%s)\"))}\n\n    echo -e \"Creating new image \\e[95m$Tag\\e[0m revision: \\e[94m$Revision\\e[0m\"\n\n    local BuildDir=\"/tmp/docker-dummy-$Tag-$Revision\"\n\n    mkdir -p \"$BuildDir\"\n\n    cat > \"$BuildDir/Dockerfile\" << END\nFROM alpine\n\nRUN echo \"Tag: $Tag\"\nRUN echo \"Revision: $Revision\"\nENTRYPOINT [\"nc\", \"-lk\", \"-v\", \"-l\", \"-p\", \"9090\", \"-e\", \"echo\", \"-e\", \"HTTP/1.1 200 OK\\n\\n$Tag $Revision\"]\nEND\n\n   docker build -t \"$Repo/$Tag:latest\" -t \"$Repo/$Tag:r$Revision\" \"$BuildDir\"\n\n   echo -e \"Pushing images...\\e[93m\"\n   docker push -q \"$Repo/$Tag:latest\"\n   docker push -q \"$Repo/$Tag:r$Revision\"\n   echo -en \"\\e[0m\"\n\n   rm -r \"$BuildDir\"\n}\n\nfunction query-rev() {\n  local Name=$1\n  if [ -z \"$Name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n  curl -s \"localhost:$(get-port \"$Name\" 9090)\"\n}\n\nfunction latest-image-rev() {\n  local Tag=$1\n  if [ -z \"$Tag\" ];  then\n    echo \"TAG missing\"\n    return 1\n  fi\n  local ID\n  ID=$(docker image ls \"$(registry-host)\"/\"$Tag\":latest -q)\n  docker image inspect \"$ID\" | jq -r '.[].RepoTags | .[]' | grep  -v latest\n}\n\nfunction container-id() {\n  local Name=$1\n  if [ -z \"$Name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n  docker container ls -f name=\"$Name\" -q\n}\n\nfunction container-started() {\n  local Name=$1\n  if [ -z \"$Name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n  docker container inspect \"$Name\" | jq -r .[].State.StartedAt\n}\n\n\nfunction container-exists() {\n  local Name=$1\n  if [ -z \"$Name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n  \n  docker container inspect \"$Name\" 1> /dev/null 2> /dev/null\n}\n\nfunction registry-exists() {\n  container-exists \"$CONTAINER_PREFIX-registry\"\n}\n\nfunction create-container() {\n  local container_name=$1\n    if [ -z \"$container_name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n  local image_name=\"${2:-$container_name}\"\n\n  echo -en \"Creating \\e[94m$container_name\\e[0m container... \"\n  local result\n  result=$(docker run -d --name \"$container_name\" \"$(registry-host)/$image_name\" 2>&1)\n  if [ \"${#result}\" -eq 64 ]; then\n    echo -e \"\\e[92m${result:0:12}\\e[0m\"\n    return 0\n  else\n    echo -e \"\\e[91mFailed!\\n\\e[97m$result\\e[0m\"\n    return 1\n  fi\n}\n\nfunction remove-images() {\n  local image_name=$1\n  if [ -z \"$image_name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n\n  local images\n  mapfile -t images < <(docker images -q \"$image_name\" | uniq)\n  if [ -n \"${images[*]}\" ]; then\n    docker image rm \"${images[@]}\"\n  else\n    echo \"No images matched \\\"$image_name\\\"\"\n  fi\n}\n\nfunction remove-repo-images() {\n  local image_name=$1\n  if [ -z \"$image_name\" ];  then\n    echo \"NAME missing\"\n    return 1\n  fi\n\n  remove-images \"$(registry-host)/images/$image_name\"\n}"
  },
  {
    "path": "scripts/du-cli.sh",
    "content": "#!/usr/bin/env bash\n\nSCRIPT_ROOT=$(dirname \"$(readlink -m \"$(type -p \"$0\")\")\")\nsource \"$SCRIPT_ROOT/docker-util.sh\"\n\ncase $1 in\n  registry | reg | r)\n    case $2 in\n      start)\n        start-registry\n        ;;\n      stop)\n        stop-registry\n        ;;\n      host)\n        registry-host\n        ;;\n      *)\n        echo \"Unknown registry action \\\"$2\\\"\"\n        ;;\n    esac\n    ;;\n  image | img | i)\n    case $2 in\n      rev)\n        create-dummy-image \"${@:3:2}\"\n        ;;\n      latest)\n        latest-image-rev \"$3\"\n        ;;\n      rm)\n        remove-repo-images \"$3\"\n        ;;\n      *)\n        echo \"Unknown image action \\\"$2\\\"\"\n        ;;\n    esac\n    ;;\n  container | cnt | c)\n    case $2 in\n      query)\n        query-rev \"$3\"\n        ;;\n      rm)\n        try-remove-container \"$3\"\n        ;;\n      id)\n        container-id \"$3\"\n        ;;\n      started)\n        container-started \"$3\"\n        ;;\n      create)\n        create-container \"${@:3:2}\"\n        ;;\n      create-stale)\n        if [ -z \"$3\" ]; then\n          echo \"NAME missing\"\n          exit 1\n        fi\n        if ! registry-exists; then\n          echo \"Registry container missing! Creating...\"\n          start-registry || exit 1\n        fi\n        image_name=\"images/$3\"\n        container_name=$3\n        $0 image rev \"$image_name\" || exit 1\n        $0 container create \"$container_name\" \"$image_name\" || exit 1\n        $0 image rev \"$image_name\" || exit 1\n        ;;\n      *)\n        echo \"Unknown container action \\\"$2\\\"\"\n        ;;\n    esac\n    ;;\n  *)\n    echo \"Unknown keyword \\\"$1\\\"\"\n    ;;\nesac"
  },
  {
    "path": "scripts/lifecycle-tests.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nIMAGE=server\nCONTAINER=server\nLINKED_IMAGE=linked\nLINKED_CONTAINER=linked\nWATCHTOWER_INTERVAL=2\n\nfunction remove_container {\n\tdocker kill $1 >> /dev/null || true && docker rm -v $1 >> /dev/null || true\n}\n\nfunction cleanup {\n  # Do cleanup on exit or error\n  echo \"Final cleanup\"\n  sleep 2\n  remove_container $CONTAINER\n  remove_container $LINKED_CONTAINER\n  pkill -9 -f watchtower >> /dev/null || true\n}\ntrap cleanup EXIT\n\nDEFAULT_WATCHTOWER=\"$(dirname \"${BASH_SOURCE[0]}\")/../watchtower\"\nWATCHTOWER=$1\nWATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}\necho \"watchtower path is $WATCHTOWER\"\n\n##################################################################################\n##### PREPARATION ################################################################\n##################################################################################\n\n#  Create Dockerfile template\nDOCKERFILE=$(cat << EOF\nFROM node:alpine\n\nLABEL com.centurylinklabs.watchtower.lifecycle.pre-update=\"cat /opt/test/value.txt\"\nLABEL com.centurylinklabs.watchtower.lifecycle.post-update=\"echo image > /opt/test/value.txt\"\n\nENV IMAGE_TIMESTAMP=TIMESTAMP\n\nWORKDIR /opt/test\nENTRYPOINT [\"/usr/local/bin/node\", \"/opt/test/server.js\"]\n\nEXPOSE 8888\n\nRUN mkdir -p /opt/test && echo \"default\" > /opt/test/value.txt\nCOPY server.js /opt/test/server.js\nEOF\n)\n\n# Create temporary directory to build docker image\nTMP_DIR=\"/tmp/watchtower-commands-test\"\nmkdir -p $TMP_DIR\n\n# Create simple http server\ncat > $TMP_DIR/server.js << EOF\nconst http = require(\"http\");\nconst fs = require(\"fs\");\n\nhttp.createServer(function(request, response) {\n\tconst fileContent = fs.readFileSync(\"/opt/test/value.txt\");\n\tresponse.writeHead(200, {\"Content-Type\": \"text/plain\"});\n\tresponse.write(fileContent);\n\tresponse.end();\n}).listen(8888, () => { console.log('server is listening on 8888'); });\nEOF\n\nfunction builddocker {\n\tTIMESTAMP=$(date +%s)\n\techo \"Building image $TIMESTAMP\"\n\techo \"${DOCKERFILE/TIMESTAMP/$TIMESTAMP}\" > $TMP_DIR/Dockerfile\n\tdocker build $TMP_DIR -t $IMAGE >> /dev/null\n}\n\n# Start watchtower\necho \"Starting watchtower\"\n$WATCHTOWER -i $WATCHTOWER_INTERVAL --no-pull --stop-timeout 2s --enable-lifecycle-hooks $CONTAINER $LINKED_CONTAINER &\nsleep 3\n\necho \"#################################################################\"\necho \"##### TEST CASE 1: Execute commands from base image\"\necho \"#################################################################\"\n\n# Build base image\nbuilddocker\n\n# Run container\ndocker run -d -p 0.0.0.0:8888:8888 --name $CONTAINER $IMAGE:latest >> /dev/null\nsleep 1\necho \"Container $CONTAINER is running\"\n\n# Test default value\nRESP=$(curl -s http://localhost:8888)\nif [ $RESP != \"default\" ]; then\n\techo \"Default value of container response is invalid\" 1>&2\n\texit 1\nfi\n\n# Build updated image to trigger watchtower update\nbuilddocker\n\nWAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))\necho \"Wait for $WAIT_AMOUNT seconds\"\nsleep $WAIT_AMOUNT\n\n# Test value after post-update-command\nRESP=$(curl -s http://localhost:8888)\nif [[ $RESP != \"image\" ]]; then\n\techo \"Value of container response is invalid. Expected: image. Actual: $RESP\"\n\texit 1\nfi\n\nremove_container $CONTAINER\n\necho \"#################################################################\"\necho \"##### TEST CASE 2: Execute commands from container and base image\"\necho \"#################################################################\"\n\n# Build base image\nbuilddocker\n\n# Run container\ndocker run -d -p 0.0.0.0:8888:8888 \\\n\t--label=com.centurylinklabs.watchtower.lifecycle.post-update=\"echo container > /opt/test/value.txt\" \\\n\t--name $CONTAINER $IMAGE:latest >> /dev/null\nsleep 1\necho \"Container $CONTAINER is running\"\n\n# Test default value\nRESP=$(curl -s http://localhost:8888)\nif [ $RESP != \"default\" ]; then\n\techo \"Default value of container response is invalid\" 1>&2\n\texit 1\nfi\n\n# Build updated image to trigger watchtower update\nbuilddocker\n\nWAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))\necho \"Wait for $WAIT_AMOUNT seconds\"\nsleep $WAIT_AMOUNT\n\n# Test value after post-update-command\nRESP=$(curl -s http://localhost:8888)\nif [[ $RESP != \"container\" ]]; then\n\techo \"Value of container response is invalid. Expected: container. Actual: $RESP\"\n\texit 1\nfi\n\nremove_container $CONTAINER\n\necho \"#################################################################\"\necho \"##### TEST CASE 3: Execute commands with a linked container\"\necho \"#################################################################\"\n\n# Tag the current image to keep a version for the linked container\ndocker tag $IMAGE:latest $LINKED_IMAGE:latest\n\n# Build base image\nbuilddocker\n\n# Run container\ndocker run -d -p 0.0.0.0:8888:8888 \\\n\t--label=com.centurylinklabs.watchtower.lifecycle.post-update=\"echo container > /opt/test/value.txt\" \\\n\t--name $CONTAINER $IMAGE:latest >> /dev/null\ndocker run -d -p 0.0.0.0:8989:8888 \\\n\t--label=com.centurylinklabs.watchtower.lifecycle.post-update=\"echo container > /opt/test/value.txt\" \\\n\t--link $CONTAINER \\\n\t--name $LINKED_CONTAINER $LINKED_IMAGE:latest >> /dev/null\nsleep 1\necho \"Container $CONTAINER and $LINKED_CONTAINER are running\"\n\n# Test default value\nRESP=$(curl -s http://localhost:8888)\nif [ $RESP != \"default\" ]; then\n\techo \"Default value of container response is invalid\" 1>&2\n\texit 1\nfi\n\n# Test default value for linked container\nRESP=$(curl -s http://localhost:8989)\nif [ $RESP != \"default\" ]; then\n\techo \"Default value of linked container response is invalid\" 1>&2\n\texit 1\nfi\n\n# Build updated image to trigger watchtower update\nbuilddocker\n\nWAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))\necho \"Wait for $WAIT_AMOUNT seconds\"\nsleep $WAIT_AMOUNT\n\n# Test value after post-update-command\nRESP=$(curl -s http://localhost:8888)\nif [[ $RESP != \"container\" ]]; then\n\techo \"Value of container response is invalid. Expected: container. Actual: $RESP\"\n\texit 1\nfi\n\n# Test that linked container did not execute pre/post-update-command\nRESP=$(curl -s http://localhost:8989)\nif [[ $RESP != \"default\" ]]; then\n\techo \"Value of linked container response is invalid. Expected: default. Actual: $RESP\"\n\texit 1\nfi"
  },
  {
    "path": "tplprev/main.go",
    "content": "//go:build !wasm\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/containrrr/watchtower/internal/meta\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/preview\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/preview/data\"\n)\n\nfunc main() {\n\tfmt.Fprintf(os.Stderr, \"watchtower/tplprev %v\\n\\n\", meta.Version)\n\n\tvar states string\n\tvar entries string\n\n\tflag.StringVar(&states, \"states\", \"cccuuueeekkktttfff\", \"sCanned, Updated, failEd, sKipped, sTale, Fresh\")\n\tflag.StringVar(&entries, \"entries\", \"ewwiiidddd\", \"Fatal,Error,Warn,Info,Debug,Trace\")\n\n\tflag.Parse()\n\n\tif len(flag.Args()) < 1 {\n\t\tfmt.Fprintln(os.Stderr, \"Missing required argument TEMPLATE\")\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tinput, err := os.ReadFile(flag.Arg(0))\n\tif err != nil {\n\n\t\tfmt.Fprintf(os.Stderr, \"Failed to read template file %q: %v\\n\", flag.Arg(0), err)\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tresult, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to read template file %q: %v\\n\", flag.Arg(0), err)\n\t\tos.Exit(1)\n\t\treturn\n\t}\n\n\tfmt.Println(result)\n}\n"
  },
  {
    "path": "tplprev/main_wasm.go",
    "content": "//go:build wasm\n\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/containrrr/watchtower/internal/meta\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/preview\"\n\t\"github.com/containrrr/watchtower/pkg/notifications/preview/data\"\n\n\t\"syscall/js\"\n)\n\nfunc main() {\n\tfmt.Println(\"watchtower/tplprev v\" + meta.Version)\n\n\tjs.Global().Set(\"WATCHTOWER\", js.ValueOf(map[string]any{\n\t\t\"tplprev\": js.FuncOf(jsTplPrev),\n\t}))\n\t<-make(chan bool)\n\n}\n\nfunc jsTplPrev(this js.Value, args []js.Value) any {\n\n\tif len(args) < 3 {\n\t\treturn \"Requires 3 arguments passed\"\n\t}\n\n\tinput := args[0].String()\n\n\tstatesArg := args[1]\n\tvar states []data.State\n\n\tif statesArg.Type() == js.TypeString {\n\t\tstates = data.StatesFromString(statesArg.String())\n\t} else {\n\t\tfor i := 0; i < statesArg.Length(); i++ {\n\t\t\tstate := data.State(statesArg.Index(i).String())\n\t\t\tstates = append(states, state)\n\t\t}\n\t}\n\n\tlevelsArg := args[2]\n\tvar levels []data.LogLevel\n\n\tif levelsArg.Type() == js.TypeString {\n\t\tlevels = data.LevelsFromString(statesArg.String())\n\t} else {\n\t\tfor i := 0; i < levelsArg.Length(); i++ {\n\t\t\tlevel := data.LogLevel(levelsArg.Index(i).String())\n\t\t\tlevels = append(levels, level)\n\t\t}\n\t}\n\n\tresult, err := preview.Render(input, states, levels)\n\tif err != nil {\n\t\treturn \"Error: \" + err.Error()\n\t}\n\treturn result\n}\n"
  }
]