Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
35f88303fa | |||
e77e61d0b0 | |||
66abd9d3c5 |
54
README.md
54
README.md
@ -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.
|
||||||
|
@ -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();
|
||||||
|
21
src/Configuration/SingleImageConfiguration.php
Normal file
21
src/Configuration/SingleImageConfiguration.php
Normal 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 ];
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user