Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
Chris | 1a14f5f1d8 | |
Chris | 6c422eafe6 | |
Chris | 88f3b75383 |
|
@ -27,3 +27,17 @@
|
||||||
- Renamed the lock file from `fresh.lock` to `.fresh.lock`.
|
- Renamed the lock file from `fresh.lock` to `.fresh.lock`.
|
||||||
- Added option `--lockfile` to override lockfile file name.
|
- Added option `--lockfile` to override lockfile file name.
|
||||||
- Implemented the fresh configuration loader.
|
- Implemented the fresh configuration loader.
|
||||||
|
|
||||||
|
**0.1.5**
|
||||||
|
|
||||||
|
- Bugfix: The lockfile is no longer removed automatically, but only if it was
|
||||||
|
created by the current instance.
|
||||||
|
|
||||||
|
**0.1.6**
|
||||||
|
|
||||||
|
- Implemented self-updating (use -U or --self-update)
|
||||||
|
|
||||||
|
**0.1.7**
|
||||||
|
|
||||||
|
- Added `--only` option, to only pull for specific services
|
||||||
|
- Added `--updated` option to only pull updated images
|
|
@ -19,7 +19,8 @@ a light-weight easy-to-use alternative to more complex toolkits.
|
||||||
Fresh requires **PHP 8.0** or later.
|
Fresh requires **PHP 8.0** or later.
|
||||||
|
|
||||||
Download the latest version (or build it yourself) and move it into `/usr/bin`.
|
Download the latest version (or build it yourself) and move it into `/usr/bin`.
|
||||||
You can grab it at [https://dev.noccylabs.info/noccy/fresh/releases](https://dev.noccylabs.info/noccy/fresh/releases).
|
You can grab it at [https://dev.noccylabs.info/noccy/fresh/releases](https://dev.noccylabs.info/noccy/fresh/releases)
|
||||||
|
or [https://files.noccylabs.info/fresh](https://files.noccylabs.info/fresh)
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,16 @@ require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
if (file_exists(__DIR__."/../src/version.php")) {
|
if (file_exists(__DIR__."/../src/version.php")) {
|
||||||
require_once __DIR__."/../src/version.php";
|
require_once __DIR__."/../src/version.php";
|
||||||
|
if (PHAR::running()) {
|
||||||
|
require_once __DIR__."/../src/swup.php";
|
||||||
|
SWUP::register([
|
||||||
|
'url' => "https://files.noccylabs.info/.repo",
|
||||||
|
'channel' => 'beta',
|
||||||
|
'package' => 'fresh',
|
||||||
|
'version' => APP_VERSION,
|
||||||
|
'phar' => 'fresh.phar'
|
||||||
|
]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
define("APP_VERSION", "DEV");
|
define("APP_VERSION", "DEV");
|
||||||
define("BUILD_DATE", "src");
|
define("BUILD_DATE", "src");
|
||||||
|
|
|
@ -9,6 +9,8 @@ class ComposeConfiguration
|
||||||
|
|
||||||
private array $checks = [];
|
private array $checks = [];
|
||||||
|
|
||||||
|
private array $services = [];
|
||||||
|
|
||||||
public function __construct(string $configfile)
|
public function __construct(string $configfile)
|
||||||
{
|
{
|
||||||
$this->loadComposeFile($configfile);
|
$this->loadComposeFile($configfile);
|
||||||
|
@ -23,6 +25,10 @@ class ComposeConfiguration
|
||||||
if ($image && !in_array($image,$this->checks)) {
|
if ($image && !in_array($image,$this->checks)) {
|
||||||
$this->checks[] = $image;
|
$this->checks[] = $image;
|
||||||
}
|
}
|
||||||
|
if (!array_key_exists($image, $this->services)) {
|
||||||
|
$this->services[$image] = [];
|
||||||
|
}
|
||||||
|
$this->services[$image][] = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,4 +36,16 @@ class ComposeConfiguration
|
||||||
{
|
{
|
||||||
return $this->checks;
|
return $this->checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getServicesForImages(array $images)
|
||||||
|
{
|
||||||
|
$ret = [];
|
||||||
|
foreach ($images as $ref) {
|
||||||
|
$image = (string)$ref->ref;
|
||||||
|
if (array_key_exists($image, $this->services)) {
|
||||||
|
$ret = array_merge($ret, $this->services[$image]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_unique($ret);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -53,4 +53,9 @@ class ImageReference
|
||||||
{
|
{
|
||||||
return $this->tag;
|
return $this->tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return sprintf("%s/%s:%s", $this->registry, $this->image, $this->tag);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -21,6 +21,7 @@ namespace NoccyLabs\FreshDocker;
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
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;
|
||||||
|
@ -32,6 +33,7 @@ use NoccyLabs\FreshDocker\ImageReference;
|
||||||
use NoccyLabs\FreshDocker\Registry\RegistryV2Client;
|
use NoccyLabs\FreshDocker\Registry\RegistryV2Client;
|
||||||
use NoccyLabs\FreshDocker\State\PersistentState;
|
use NoccyLabs\FreshDocker\State\PersistentState;
|
||||||
use NoccyLabs\FreshDocker\State\Lockfile;
|
use NoccyLabs\FreshDocker\State\Lockfile;
|
||||||
|
use Phar;
|
||||||
|
|
||||||
class Refresher
|
class Refresher
|
||||||
{
|
{
|
||||||
|
@ -48,6 +50,8 @@ class Refresher
|
||||||
'path' => [ 'd:', 'dir:', "Change working directory", "FRESH_DIR" ],
|
'path' => [ 'd:', 'dir:', "Change working directory", "FRESH_DIR" ],
|
||||||
'image' => [ 'i:', 'image:', "Check a specific image instead of images from config", "FRESH_IMAGE" ],
|
'image' => [ 'i:', 'image:', "Check a specific image instead of images from config", "FRESH_IMAGE" ],
|
||||||
'pull' => [ null, 'pull', "Only pull if updated, don't up" ],
|
'pull' => [ null, 'pull', "Only pull if updated, don't up" ],
|
||||||
|
'only' => [ null, 'only:', "Comma-separated list of services to docker-compose pull" ],
|
||||||
|
'updated' => [ null, 'updated', "Only pass updated services to docker-compose pull" ],
|
||||||
'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)", null, false ],
|
'write-state' => [ 'w', 'write-state', "Always write updated state (only useful with --check)", null, false ],
|
||||||
|
@ -151,6 +155,13 @@ class Refresher
|
||||||
{
|
{
|
||||||
$tty = posix_isatty(STDOUT);
|
$tty = posix_isatty(STDOUT);
|
||||||
|
|
||||||
|
// add the "Self-Updating" options group if we are running in a phar
|
||||||
|
if (Phar::running()) {
|
||||||
|
self::$optionsMap['Self-Updating'] = [
|
||||||
|
'update' => [ 'U', 'self-update', "Update to the latest version" ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
printf("fresh.phar v%s (%s) - (c) 2022, NoccyLabs / GPL v3 or later\n", APP_VERSION, BUILD_DATE);
|
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]));
|
||||||
|
@ -198,6 +209,8 @@ class Refresher
|
||||||
$this->setupConfiguration();
|
$this->setupConfiguration();
|
||||||
$this->setupHooks();
|
$this->setupHooks();
|
||||||
|
|
||||||
|
$this->lockfile->lock();
|
||||||
|
|
||||||
$updated = $this->checkUpdates();
|
$updated = $this->checkUpdates();
|
||||||
|
|
||||||
// If called with --check, only return exit status
|
// If called with --check, only return exit status
|
||||||
|
@ -213,11 +226,11 @@ class Refresher
|
||||||
if ($updated !== null) {
|
if ($updated !== null) {
|
||||||
$this->callHooks($updated, 'before');
|
$this->callHooks($updated, 'before');
|
||||||
$this->callScript('before');
|
$this->callScript('before');
|
||||||
$this->doUpdate();
|
$this->doUpdate($updated);
|
||||||
$this->callHooks($updated, 'after');
|
$this->callHooks($updated, 'after');
|
||||||
$this->callScript('after');
|
$this->callScript('after');
|
||||||
}
|
}
|
||||||
|
$this->lockfile->release();
|
||||||
exit(($updated === null) ? 0 : 1);
|
exit(($updated === null) ? 0 : 1);
|
||||||
|
|
||||||
} catch (\Throwable $t) {
|
} catch (\Throwable $t) {
|
||||||
|
@ -226,6 +239,7 @@ class Refresher
|
||||||
if (!$this->options['verbose']) {
|
if (!$this->options['verbose']) {
|
||||||
fprintf(STDERR, $this->log->asString()."\n");
|
fprintf(STDERR, $this->log->asString()."\n");
|
||||||
}
|
}
|
||||||
|
$this->lockfile->release();
|
||||||
exit(2);
|
exit(2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -253,6 +267,8 @@ class Refresher
|
||||||
$lockfile = $this->options['lockfile'];
|
$lockfile = $this->options['lockfile'];
|
||||||
if (!str_starts_with($lockfile,'/')) $lockfile = $this->path . "/" . $lockfile;
|
if (!str_starts_with($lockfile,'/')) $lockfile = $this->path . "/" . $lockfile;
|
||||||
|
|
||||||
|
$this->log->append("Statefile: {$statefile}");
|
||||||
|
$this->log->append("Lockfile: {$lockfile}");
|
||||||
$this->state = new PersistentState($statefile); // $this->path . "/" . self::$StateFileName);
|
$this->state = new PersistentState($statefile); // $this->path . "/" . self::$StateFileName);
|
||||||
$this->lockfile = new Lockfile($lockfile); // $this->path . "/" . self::$LockFileName);
|
$this->lockfile = new Lockfile($lockfile); // $this->path . "/" . self::$LockFileName);
|
||||||
}
|
}
|
||||||
|
@ -359,8 +375,6 @@ class Refresher
|
||||||
private function checkUpdates(): ?array
|
private function checkUpdates(): ?array
|
||||||
{
|
{
|
||||||
|
|
||||||
$this->lockfile->lock();
|
|
||||||
|
|
||||||
$checks = $this->config->getChecks();
|
$checks = $this->config->getChecks();
|
||||||
if (count($checks) === 0) {
|
if (count($checks) === 0) {
|
||||||
fwrite(STDERR, "error: couldn't find any images to check\n");
|
fwrite(STDERR, "error: couldn't find any images to check\n");
|
||||||
|
@ -382,8 +396,12 @@ class Refresher
|
||||||
}
|
}
|
||||||
$client = new RegistryV2Client($reg, $credentials);
|
$client = new RegistryV2Client($reg, $credentials);
|
||||||
|
|
||||||
|
try {
|
||||||
$status = $client->getImageStatus($ref->getImage(), $ref->getTag());
|
$status = $client->getImageStatus($ref->getImage(), $ref->getTag());
|
||||||
|
} catch (RequestException $e) {
|
||||||
|
$this->log->append(sprintf(" %s: registry returned error (%s)", $reg."/".$ref->getImage(), $e->getMessage()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$image = $reg."/".$status['image'].":".$status['tag'];
|
$image = $reg."/".$status['image'].":".$status['tag'];
|
||||||
$oldHash = $this->state->get($image);
|
$oldHash = $this->state->get($image);
|
||||||
$newHash = $status['hash'];
|
$newHash = $status['hash'];
|
||||||
|
@ -405,13 +423,27 @@ class Refresher
|
||||||
return empty($update) ? null : $update;
|
return empty($update) ? null : $update;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function doUpdate()
|
public function doUpdate(array $updated)
|
||||||
{
|
{
|
||||||
|
if ($this->options['only']) {
|
||||||
|
$pullSpec = explode(",", $this->options['only']);
|
||||||
|
$this->log->append("Refreshing only: " . join(", ", $pullSpec));
|
||||||
|
} elseif ($this->options['updated'] && ($this->config instanceof ComposeConfiguration)) {
|
||||||
|
$pullSpec = $this->config->getServicesForImages($updated);
|
||||||
|
if (count($pullSpec) == 0) {
|
||||||
|
$this->log->append("Fatal: failed to resolve updated services to docker-compose pull");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->log->append("Refreshing updated: " . join(", ", $pullSpec));
|
||||||
|
} else {
|
||||||
|
$pullSpec = null;
|
||||||
|
}
|
||||||
|
|
||||||
$this->log->append("Pulling updated images...");
|
$this->log->append("Pulling updated images...");
|
||||||
$this->exec("docker-compose pull --quiet");
|
$this->exec("docker-compose pull --quiet ". ($pullSpec?join(" ",array_map('escapeshellarg', $pullSpec)):'') );
|
||||||
if (!$this->options['pull']) {
|
if (!$this->options['pull']) {
|
||||||
$this->log->append("Refreshing updated containers...");
|
$this->log->append("Refreshing updated containers...");
|
||||||
$this->exec("docker-compose up -d");
|
$this->exec("docker-compose up -d". ($pullSpec?join(" ",array_map('escapeshellarg', $pullSpec)):'') );
|
||||||
if ($this->options['prune']) {
|
if ($this->options['prune']) {
|
||||||
$this->log->append("Pruning dangling images...");
|
$this->log->append("Pruning dangling images...");
|
||||||
$this->exec("docker image prune -f");
|
$this->exec("docker image prune -f");
|
||||||
|
|
|
@ -23,7 +23,11 @@ class RegistryV2Client
|
||||||
|
|
||||||
public function getManifest(string $image, string $tag)
|
public function getManifest(string $image, string $tag)
|
||||||
{
|
{
|
||||||
$response = $this->client->get("{$image}/manifests/{$tag}");
|
$response = $this->client->get("{$image}/manifests/{$tag}", [
|
||||||
|
'headers' => [
|
||||||
|
//'Accept' => 'application/vnd.docker.distribution.manifest.v2+json',
|
||||||
|
]
|
||||||
|
]);
|
||||||
$body = $response->getBody();
|
$body = $response->getBody();
|
||||||
return json_decode($body);
|
return json_decode($body);
|
||||||
}
|
}
|
||||||
|
@ -32,12 +36,16 @@ class RegistryV2Client
|
||||||
{
|
{
|
||||||
$manifest = $this->getManifest($image, $tag);
|
$manifest = $this->getManifest($image, $tag);
|
||||||
|
|
||||||
$fslayers = (array)$manifest->fsLayers;
|
//print_r($manifest);
|
||||||
$fshashes = array_map(fn($layer) => $layer->blobSum, $fslayers);
|
|
||||||
|
$fslayers = (array)(($manifest->fsLayers??$manifest->layers)??[]);
|
||||||
|
$fshashes = array_map(fn($layer) => $layer->blobSum??$layer->digest, $fslayers);
|
||||||
$metahash = hash("sha256", join("|", $fshashes));
|
$metahash = hash("sha256", join("|", $fshashes));
|
||||||
|
|
||||||
$history = $manifest->history[0];
|
if (isset($manifest->history)) {
|
||||||
$info = json_decode($history->v1Compatibility);
|
$history = $manifest->history[0];
|
||||||
|
$info = json_decode($history->v1Compatibility);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'image' => $image,
|
'image' => $image,
|
||||||
|
|
|
@ -8,6 +8,8 @@ class Lockfile
|
||||||
|
|
||||||
private int $maxLock = 3600;
|
private int $maxLock = 3600;
|
||||||
|
|
||||||
|
private bool $locked = false;
|
||||||
|
|
||||||
public function __construct(string $filename)
|
public function __construct(string $filename)
|
||||||
{
|
{
|
||||||
$this->filename = $filename;
|
$this->filename = $filename;
|
||||||
|
@ -22,15 +24,16 @@ class Lockfile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
touch($this->filename);
|
touch($this->filename);
|
||||||
|
$this->locked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function release()
|
public function release()
|
||||||
{
|
{
|
||||||
if (file_exists($this->filename)) {
|
if ($this->locked && file_exists($this->filename)) {
|
||||||
unlink($this->filename);
|
unlink($this->filename);
|
||||||
}
|
}
|
||||||
|
$this->locked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,10 @@ NOW="$(date +"%Y-%m-%d")"
|
||||||
# create output directory
|
# create output directory
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
|
|
||||||
# update version.php and build thephar
|
# update version.php and build the phar
|
||||||
echo "<?php define(\"APP_VERSION\", \"${TAG}\"); define(\"BUILD_DATE\", \"${NOW}\");" > src/version.php
|
echo "<?php define(\"APP_VERSION\", \"${TAG}\"); define(\"BUILD_DATE\", \"${NOW}\");" > src/version.php
|
||||||
tools/pharlite
|
tools/pharlite
|
||||||
|
#phpxmake -o "$OUT.phpx" index.php src vendor
|
||||||
|
|
||||||
# copy raw phar into destination
|
# copy raw phar into destination
|
||||||
cp -v fresh.phar "$OUT.phar"
|
cp -v fresh.phar "$OUT.phar"
|
||||||
|
|
Loading…
Reference in New Issue