#!/usr/bin/env php "Show this help", '-q,--quiet' => "Don't show any output (except dockers)", '-d,--dir DIR' => "Change working directory to DIR", '-i,--image REF' => "Check a specific image instead of using config/docker-compose", '--pull' => "Only pull if new, don't up", '--check' => "Only check, set exit code 1 if not fresh", '--prune' => "Prune dangling images after pull and up", ] as $a=>$b) printf(" %-20s %s\n", $a, $b); printf(" Webhooks:\n"); foreach([ '--slack URL' => "Notify a Slack webhook when updating", ] as $a=>$b) printf(" %-20s %s\n", $a, $b); printf(" Configuration:\n"); foreach([ '-c,--config FILE' => "Use a custom configuration (not implemented)", '--credentials TYPE' => "Select credentials loader (auto or basic)", ] as $a=>$b) printf(" %-20s %s\n", $a, $b); exit(0); } $credentialsLoaderType = $opts['credentials'] ?? 'auto'; $path = realpath($opts['d'] ?? ($opts['dir'] ?? getcwd())); $onlyPull = array_key_exists('p', $opts) || array_key_exists('pull', $opts); $onlyCheck = array_key_exists('check', $opts); $quiet = array_key_exists('q', $opts) || array_key_exists('quiet', $opts); $verbose = array_key_exists('v', $opts); $prune = array_key_exists('prune', $opts); $slackUrl = array_key_exists('slack', $opts) ? $opts['slack'] : null; $slackHook = null; if ($slackUrl) { $slackHook = new SlackHook([ 'url' => $slackUrl ]); } if (!is_dir($path)) { fwrite(STDERR, "error: invalid path for --dir/-d\n"); exit(2); } chdir($path); $path = getcwd(); if (!file_exists($path."/docker-compose.yml")) { fwrite(STDERR, "error: no docker-compose.yml in the current directory\n"); exit(2); } switch ($credentialsLoaderType) { case 'auto': case 'basic': $credentialsLoader = new BasicCredentialsLoader(); break; default: fwrite(STDERR, "error: invalid credentials loader {$credentialsLoaderType}\n"); exit(2); } //$configuration = new LocalConfiguration(getcwd()."/freshdocker.conf"); $configuration = new DockerComposeConfiguration("{$path}/docker-compose.yml"); $state = new PersistentState("{$path}/fresh.yml"); $checks = $configuration->getChecks(); if (count($checks) === 0) { fwrite(STDERR, "error: couldn't find any images to check\n"); exit(2); } $update = false; $log = []; foreach ($checks as $check) { $ref = new ImageReference($check); $reg = $ref->getRegistry(); $credentials = $credentialsLoader->getCredentials($reg); if (!$credentials) { if ($verbose) printf("%s: missing credentials for %s\n", $ref->getImage(), $ref->getRegistry()); continue; } $client = new RegistryV2Client($reg, $credentials); $status = $client->getImageStatus($ref->getImage(), $ref->getTag()); $image = $status['image'].":".$status['tag']; $oldHash = $state->get($image); $newHash = $status['hash']; $state->set($image, $newHash); if ($oldHash != $newHash) { if (!$quiet) printf("%s: %s → %s\n", $image, truncate($oldHash), truncate($newHash)); $log[] = sprintf("%s: %s → %s\n", $image, truncate($oldHash), truncate($newHash)); $update = true; } else { if (!$quiet) printf("%s: %s\n", $image, truncate($newHash)); } } if ($onlyCheck) { exit($update?1:0); } if ($update) { if ($slackHook) { $msg = "Deploying new container versions:\n* ".join("\n* ", $log); $slackHook->sendMessage($msg, []); } passthru("docker-compose pull"); if (!$onlyPull) { passthru("docker-compose up -d"); if ($prune) { passthru("docker image prune -f"); } } } exit($update?1:0); function truncate(?string $hash): string { if ($hash === null) return '*'; return substr($hash, 0, 4) . ".." . substr($hash, -4, 4); }