3 Commits
0.1.1 ... 0.1.3

Author SHA1 Message Date
35f88303fa Improved hooks and scripts
* Invoke hooks before and after updating
* Scripts can now be run --before and --after
2022-03-19 23:22:08 +01:00
e77e61d0b0 Bugfixes and improvements
* Implemented the --image option
* Implemented the --write-state option to go with --check
* Fixed bug in Lockfile class
2022-03-11 01:55:35 +01:00
66abd9d3c5 Updated readme 2022-03-10 02:32:47 +01:00
5 changed files with 148 additions and 32 deletions

View File

@ -1,21 +1,46 @@
# Fresh: Keeping your docker stacks up to date # Fresh: Keeping your docker stacks up to date
Fresh was written to scratch an itch. It works by querying the respective repositories Fresh was written to scratch an itch. It works by querying the respective repositories
returning for the various manifests in order to determine if the images have been changed. If so
it can do a combination of things:
- Set the exitcode to indicate the freshness, `0` means up-to-date and `1` outdated.
- Pull the updated images with docker-compose, and optionally recreate the containers
with `docker-compose up`.
- Run a script before and after updating, f.ex. to enable maintenance mode or update
permission on volumes.
- Notify webhooks when updating. Currently only Slack and Mattermost are supported.
Fresh is designed to be invoked using cron or systemd timers, and as such provides
a light-weight easy-to-use alternative to more complex toolkits.
## Building ## Building
Build using NoccyLabs Pharlite. Build using NoccyLabs Pharlite:
$ tools/pharlite
## Installing
Download the latest version (or build it yourself) and move it into `/usr/bin`.
Go grab it at [https://dev.noccylabs.info/noccy/fresh/releases](https://dev.noccylabs.info/noccy/fresh/releases)
## Usage ## Usage
To check for updates, pull updated images and recreate any containers defined in the To check for updates, pull updated images and recreate any containers defined in the
`docker-compose.yml` in the current directory: `docker-compose.yml` in the current directory:
``` $ fresh.phar
$ fresh.phar
``` Specify a directory to chdir into; very useful with cron:
$ fresh.phar --dir /srv/docker/mystack
To invoke scripts or webhooks:
$ fresh.phar --before scripts/sitedown.sh --after scripts/siteup.sh \
--slack https://my.slack.or/mattermost/webhook
For all available options, use the `--help` flag. For all available options, use the `--help` flag.
@ -25,15 +50,17 @@ For all available options, use the `--help` flag.
`--check` flag. `--check` flag.
- Only checks authenticated registries for new versions. But if you are using - Only checks authenticated registries for new versions. But if you are using
this you probably aren't using DockerHub anyway. this you probably aren't using DockerHub anyway.
- ~Using the `-q` or `--quiet` flags doesn't mute the output from `docker-compose`.~
- ~Using `--check` still updates the state, need a `--no-state` flag or so to ~
~prevent updating the state.~
## FAQ ## FAQ
- **How does Fresh remember the last seen hash?** The container hashes are stored - **How does Fresh remember the last seen hash?** The container hashes are stored
in the same directory as the `docker-compose.yml` file in a file named `fresh.yml`. in the same directory as the `docker-compose.yml` file in a file named `fresh.yml`.
Remove this file to force trigger an update. Remove this file to force trigger an update.
- **What are these hashes?** Fresh grabs the manifest for the image from the registry
and proceeds to hash a concatenation of all the various build layer hashes. This
should mean if the image is new but the layers are the same nothing will be updated.
- **How do I notify a Mattermost webook?** Mattermost webhooks are compatible with
Slack webhooks, so simply use the `--slack` flag.
## Changes ## Changes
@ -45,3 +72,14 @@ For all available options, use the `--help` flag.
- Disabled automatic flushing of the state to disk; --check will no longer update - Disabled automatic flushing of the state to disk; --check will no longer update
the state file, but --pull and default update will. the state file, but --pull and default update will.
**0.1.2**
- Fixed a bug in lockfile class preventing release of stale lockfile.
- The `--image` option finally works.
- Added a `--write-state`/`-w` option to write updated hashes to the state file.
- Implemented `--config` and `--config-type`options.
**0.1.3**
- Added a `--before` script hook, to complement the `--after` hook.
- Hooks now invoked both before and after deploy.

View File

@ -7,6 +7,7 @@ if (file_exists(__DIR__."/../src/version.php")) {
require_once __DIR__."/../src/version.php"; require_once __DIR__."/../src/version.php";
} else { } else {
define("APP_VERSION", "DEV"); define("APP_VERSION", "DEV");
define("BUILD_DATE", "src");
} }
$r = new NoccyLabs\FreshDocker\Refresher(); $r = new NoccyLabs\FreshDocker\Refresher();

View File

@ -0,0 +1,21 @@
<?php
namespace NoccyLabs\FreshDocker\Configuration;
use Symfony\Component\Yaml\Yaml;
class SingleImageConfiguration
{
private $image;
public function __construct(string $image)
{
$this->image = $image;
}
public function getChecks(): array
{
return [ $this->image ];
}
}

View File

@ -5,6 +5,7 @@ namespace NoccyLabs\FreshDocker;
use NoccyLabs\FreshDocker\State\Log; use NoccyLabs\FreshDocker\State\Log;
use NoccyLabs\FreshDocker\Configuration\ComposeConfiguration; use NoccyLabs\FreshDocker\Configuration\ComposeConfiguration;
use NoccyLabs\FreshDocker\Configuration\LocalConfiguration; use NoccyLabs\FreshDocker\Configuration\LocalConfiguration;
use NoccyLabs\FreshDocker\Configuration\SingleImageConfiguration;
use NoccyLabs\FreshDocker\Credentials\BasicCredentialsLoader; use NoccyLabs\FreshDocker\Credentials\BasicCredentialsLoader;
use NoccyLabs\FreshDocker\Credentials\CredentialsLoaderInterface; use NoccyLabs\FreshDocker\Credentials\CredentialsLoaderInterface;
use NoccyLabs\FreshDocker\Hooks\SlackHook; use NoccyLabs\FreshDocker\Hooks\SlackHook;
@ -25,14 +26,16 @@ class Refresher
'pull' => [ null, 'pull', "Only pull if updated, don't up" ], 'pull' => [ null, 'pull', "Only pull if updated, don't up" ],
'check' => [ null, 'check', "Only check for updates, set exit code" ], 'check' => [ null, 'check', "Only check for updates, set exit code" ],
'prune' => [ null, 'prune', "Prune dangling images after pull and up" ], 'prune' => [ null, 'prune', "Prune dangling images after pull and up" ],
'write-state' => [ 'w', 'write-state', "Always write updated state (only useful with --check)", false ],
], ],
'Hooks' => [ 'Hooks' => [
'slack' => [ null, 'slack:', "Notify a slack webhook when updating", "FRESH_SLACK" ], 'slack' => [ null, 'slack:', "Notify a slack webhook when updating", "FRESH_SLACK" ],
'script' => [ null, 'after:', "Invoke script after updating", "FRESH_AFTER" ], 'script-after' => [ null, 'after:', "Invoke script after updating", "FRESH_AFTER" ],
'script-before' => [ null, 'before:', "Invoke script before updating", "FRESH_BEFORE" ],
], ],
'Config' => [ 'Config' => [
'config' => [ 'c:', 'config:', "Use custom configuration file", "FRESH_CONFIG" ], 'config' => [ 'c:', 'config:', "Use custom configuration file", "FRESH_CONFIG" ],
'type' => [ 'C:', 'config-type:', "Configuration type (auto, fresh, compose)", "FRESH_CONFIG_TYPE", "auto" ], 'config-type' => [ 'C:', 'config-type:', "Configuration type (auto, fresh, compose)", "FRESH_CONFIG_TYPE", "auto" ],
'credentials' => [ null, 'credentials:', "Set credentials loader type (auto or basic)", "FRESH_CREDENTIALS", "auto" ], 'credentials' => [ null, 'credentials:', "Set credentials loader type (auto or basic)", "FRESH_CREDENTIALS", "auto" ],
] ]
]; ];
@ -111,8 +114,9 @@ class Refresher
public function printUsage() public function printUsage()
{ {
$tty = posix_isatty(STDOUT);
printf("fresh.phar v%s - (c) 2022, NoccyLabs / GPL v3 or later\n", APP_VERSION); printf("fresh.phar v%s (%s) - (c) 2022, NoccyLabs / GPL v3 or later\n", APP_VERSION, BUILD_DATE);
printf("Check for updates to docker images or compose stacks.\n\n"); printf("Check for updates to docker images or compose stacks.\n\n");
printf("Usage:\n %s [options]\n\n", basename($GLOBALS['argv'][0])); printf("Usage:\n %s [options]\n\n", basename($GLOBALS['argv'][0]));
printf("Options:\n"); printf("Options:\n");
@ -129,10 +133,10 @@ class Refresher
if ($s&&$l) $optkey .= ","; if ($s&&$l) $optkey .= ",";
if ($l) $optkey .= "--" . $l; if ($l) $optkey .= "--" . $l;
if ($h) $optkey .= " VALUE"; if ($h) $optkey .= " VALUE";
printf(" %-25s %s", $optkey, $opt[2]); $out = sprintf(" %-25s %s", $optkey, $opt[2]);
if ($d) printf(" (default: %s)", $d); if ($d) $out.= sprintf(" (default: %s)", $d);
if ($e) printf(" [\$%s]", $e); if ($e) $out.= sprintf(" [\$%s]", $e);
echo "\n"; echo $out . "\n";
} }
} }
printf("\nNote:\n"); printf("\nNote:\n");
@ -163,13 +167,20 @@ class Refresher
// If called with --check, only return exit status // If called with --check, only return exit status
if ($this->options['check']) { if ($this->options['check']) {
if (($updated !== null) && $this->options['write-state']) {
// Flush state if called with -w/--write-state
$this->log->append("Flushing state...");
$this->state->flush();
}
exit(($updated === null) ? 0 : 1); exit(($updated === null) ? 0 : 1);
} }
if ($updated !== null) { if ($updated !== null) {
$this->callHooks($updated); $this->callHooks($updated, 'before');
$this->doUpdate($updated); $this->callScript('before');
$this->callScript(); $this->doUpdate();
$this->callHooks($updated, 'after');
$this->callScript('after');
} }
exit(($updated === null) ? 0 : 1); exit(($updated === null) ? 0 : 1);
@ -227,13 +238,49 @@ class Refresher
*/ */
private function setupConfiguration() private function setupConfiguration()
{ {
if ($this->options['image']) {
if (file_exists($this->path."/docker-compose.yml")) { $this->config = new SingleImageConfiguration($this->options['image']);
$this->config = new ComposeConfiguration($this->path."/docker-compose.yml"); return;
} else {
fwrite(STDERR, "error: Couldn't find a supported configuration file\n");
exit(2);
} }
$configType = $this->options['config-type'];
$configFile = $this->options['config'] ? realpath($this->options['config']) : null;
switch ($configType) {
case 'auto':
if ($configFile) {
if (basename($configFile) == "docker-compose.yml") {
$this->log->append("Using configuration file ".$configFile);
$this->config = new ComposeConfiguration($configFile);
return;
}
} else {
if (file_exists($this->path."/docker-compose.yml")) {
$this->log->append("Using configuration file ".$this->path."/docker-compose.yml");
$this->config = new ComposeConfiguration($this->path."/docker-compose.yml");
return;
}
}
break;
case 'compose':
if ($configFile) {
if (basename($configFile) == "docker-compose.yml") {
$this->log->append("Using configuration file ".$configFile);
$this->config = new ComposeConfiguration($configFile);
return;
}
} else {
if (file_exists($this->path."/docker-compose.yml")) {
$this->log->append("Using configuration file ".$this->path."/docker-compose.yml");
$this->config = new ComposeConfiguration($this->path."/docker-compose.yml");
return;
}
}
break;
}
fwrite(STDERR, "error: Couldn't find a supported configuration file\n");
exit(2);
} }
/** /**
@ -318,13 +365,22 @@ class Refresher
} }
} }
private function callHooks(array $updated) private function callHooks(array $updated, string $event)
{ {
$images = []; switch ($event) {
foreach ($updated as $u) { case 'before':
$images[] = sprintf("%s/%s:%s", $u->ref->getRegistry(), $u->ref->getImage(), $u->ref->getTag()); $images = [];
foreach ($updated as $u) {
$images[] = sprintf("%s/%s:%s", $u->ref->getRegistry(), $u->ref->getImage(), $u->ref->getTag());
}
$msg = "Deploying updated containers:\n* ".join("\n* ", $images);
break;
case 'after':
$msg = "Deploy complete";
break;
default:
return;
} }
$msg = "Deploying updated containers:\n* ".join("\n* ", $images);
foreach ($this->hooks as $hook) { foreach ($this->hooks as $hook) {
$hook->sendMessage($msg, []); $hook->sendMessage($msg, []);
@ -332,9 +388,9 @@ class Refresher
} }
private function callScript() private function callScript(string $event)
{ {
$script = $this->options['script']; $script = $this->options["script-{$event}"] ?? null;
if ($script) { if ($script) {
$this->exec($script); $this->exec($script);
} }

View File

@ -17,7 +17,7 @@ class Lockfile
public function lock() public function lock()
{ {
if (file_exists($this->filename)) { if (file_exists($this->filename)) {
if (time() - filemtime($this->filename) < $this-$maxLock) { if (time() - filemtime($this->filename) < $this->maxLock) {
throw new \RuntimeException("Lockfile {$this->filename} already exists"); throw new \RuntimeException("Lockfile {$this->filename} already exists");
} }
} }