363 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
		
		
			
		
	
	
			363 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| 
								 | 
							
								<?php
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								namespace NoccyLabs\FreshDocker;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\State\Log;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Configuration\ComposeConfiguration;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Configuration\LocalConfiguration;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Credentials\BasicCredentialsLoader;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Credentials\CredentialsLoaderInterface;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Hooks\SlackHook;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\ImageReference;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\Registry\RegistryV2Client;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\State\PersistentState;
							 | 
						||
| 
								 | 
							
								use NoccyLabs\FreshDocker\State\Lockfile;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								class Refresher
							 | 
						||
| 
								 | 
							
								{
							 | 
						||
| 
								 | 
							
								    private static array $optionsMap = [
							 | 
						||
| 
								 | 
							
								        'General' => [
							 | 
						||
| 
								 | 
							
								            'help'  => [ 'h', 'help', "Show this help" ],
							 | 
						||
| 
								 | 
							
								            'quiet' => [ 'q', 'quiet', "Don't show any output" ],
							 | 
						||
| 
								 | 
							
								            'verbose' => [ 'v', null, "Show debug output", null, false ],
							 | 
						||
| 
								 | 
							
								            'path'  => [ 'd:', 'dir:', "Change working directory", "FRESH_DIR" ],
							 | 
						||
| 
								 | 
							
								            'image' => [ 'i:', 'image:', "Check a specific image instead of images from config", "FRESH_IMAGE" ],
							 | 
						||
| 
								 | 
							
								            'pull'  => [ null, 'pull', "Only pull if updated, don't up" ],
							 | 
						||
| 
								 | 
							
								            'check' => [ null, 'check', "Only check for updates, set exit code" ],
							 | 
						||
| 
								 | 
							
								            'prune' => [ null, 'prune', "Prune dangling images after pull and up" ],
							 | 
						||
| 
								 | 
							
								        ],
							 | 
						||
| 
								 | 
							
								        'Hooks' => [
							 | 
						||
| 
								 | 
							
								            'slack' => [ null, 'slack:', "Notify a slack webhook when updating", "FRESH_SLACK" ],
							 | 
						||
| 
								 | 
							
								            'script' => [ null, 'after:', "Invoke script after updating", "FRESH_AFTER" ],
							 | 
						||
| 
								 | 
							
								        ],
							 | 
						||
| 
								 | 
							
								        'Config' => [
							 | 
						||
| 
								 | 
							
								            'config' => [ 'c:', 'config:', "Use custom configuration file", "FRESH_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" ],
							 | 
						||
| 
								 | 
							
								        ]
							 | 
						||
| 
								 | 
							
								    ];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private array $options = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /** @var HookInterface[] The hooks to invoke */
							 | 
						||
| 
								 | 
							
								    private array $hooks = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private ?string $path = null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private Log $log;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private $config;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private CredentialsLoaderInterface $credentialsLoader;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private PersistentState $state;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private Lockfile $lockfile;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public function __construct()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $this->log = new Log();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Parse the command line and set values from getopt output or environment
							 | 
						||
| 
								 | 
							
								     * variables.
							 | 
						||
| 
								 | 
							
								     * 
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    protected function parseCommandLine()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $opts = [];
							 | 
						||
| 
								 | 
							
								        $long = [];
							 | 
						||
| 
								 | 
							
								        $short = null;
							 | 
						||
| 
								 | 
							
								        $envs = [];
							 | 
						||
| 
								 | 
							
								        $parsed = [];
							 | 
						||
| 
								 | 
							
								        foreach (self::$optionsMap as $group=>$options) {
							 | 
						||
| 
								 | 
							
								            $opts = array_merge($opts, $options);
							 | 
						||
| 
								 | 
							
								            foreach ($options as $optname=>$opt) {
							 | 
						||
| 
								 | 
							
								                if ($opt[0]) $short .= $opt[0];
							 | 
						||
| 
								 | 
							
								                if ($opt[1]) $long[] = $opt[1];
							 | 
						||
| 
								 | 
							
								                if ($opt[3]??null) $envs[$opt[3]] = $optname;
							 | 
						||
| 
								 | 
							
								                $parsed[$optname] = $opt[4] ?? null;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        foreach ($envs as $env=>$dest) {
							 | 
						||
| 
								 | 
							
								            $val = getenv($env) ?? $_SERVER[$env];
							 | 
						||
| 
								 | 
							
								            if ($val) {
							 | 
						||
| 
								 | 
							
								                $parsed[$dest] = $val;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $geto = getopt($short, $long);
							 | 
						||
| 
								 | 
							
								        foreach ($geto as $k=>$v) {
							 | 
						||
| 
								 | 
							
								            foreach ($opts as $o=>$oo) {
							 | 
						||
| 
								 | 
							
								                $h = null;
							 | 
						||
| 
								 | 
							
								                if ($k == rtrim($oo[0],':')) {
							 | 
						||
| 
								 | 
							
								                    $h = str_ends_with($oo[0],':');
							 | 
						||
| 
								 | 
							
								                } elseif ($k == rtrim($oo[1],':')) {
							 | 
						||
| 
								 | 
							
								                    $h = str_ends_with($oo[0],':');
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                if ($h !== null) {
							 | 
						||
| 
								 | 
							
								                    $parsed[$o] = $h ? $v : true;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->options = $parsed;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->log->setVerbose($parsed['verbose']);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public function printUsage()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        printf("fresh.phar v%s - (c) 2022, NoccyLabs / GPL v3 or later\n", APP_VERSION);
							 | 
						||
| 
								 | 
							
								        printf("Check for updates to docker images or compose stacks.\n\n");
							 | 
						||
| 
								 | 
							
								        printf("Usage:\n    %s [options]\n\n", basename($GLOBALS['argv'][0]));
							 | 
						||
| 
								 | 
							
								        printf("Options:\n");
							 | 
						||
| 
								 | 
							
								        foreach(self::$optionsMap as $group=>$options) {
							 | 
						||
| 
								 | 
							
								            printf("  %s:\n", $group);
							 | 
						||
| 
								 | 
							
								            foreach ($options as $opt) {
							 | 
						||
| 
								 | 
							
								                $s = rtrim($opt[0], ':');
							 | 
						||
| 
								 | 
							
								                $l = rtrim($opt[1], ':');
							 | 
						||
| 
								 | 
							
								                $h = str_ends_with($opt[0],':') || str_ends_with($opt[1],':');
							 | 
						||
| 
								 | 
							
								                $d = $opt[4]??null;
							 | 
						||
| 
								 | 
							
								                $e = $opt[3]??null;
							 | 
						||
| 
								 | 
							
								                $optkey = null;
							 | 
						||
| 
								 | 
							
								                if ($s) $optkey .= "-" . $s;
							 | 
						||
| 
								 | 
							
								                if ($s&&$l) $optkey .= ",";
							 | 
						||
| 
								 | 
							
								                if ($l) $optkey .= "--" . $l;
							 | 
						||
| 
								 | 
							
								                if ($h) $optkey .= " VALUE";
							 | 
						||
| 
								 | 
							
								                printf("    %-25s %s", $optkey, $opt[2]);
							 | 
						||
| 
								 | 
							
								                if ($d) printf(" (default: %s)", $d);
							 | 
						||
| 
								 | 
							
								                if ($e) printf(" [\$%s]", $e);
							 | 
						||
| 
								 | 
							
								                echo "\n";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        printf("\nNote:\n");
							 | 
						||
| 
								 | 
							
								        printf("    When invoked without any flags, fresh will look for a docker-compose.yml file and\n");
							 | 
						||
| 
								 | 
							
								        printf("    check all images in repositories for which credentials are available. If any of those\n");
							 | 
						||
| 
								 | 
							
								        printf("    images have been updated, the new images will be pulled and the containers recreated\n");
							 | 
						||
| 
								 | 
							
								        printf("    and restarted. If you only want to pull, use the --pull flag. If you only want to check\n");
							 | 
						||
| 
								 | 
							
								        printf("    for updates, use the --check flag and check the exit code.\n");
							 | 
						||
| 
								 | 
							
								            
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public function run()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $this->parseCommandLine();
							 | 
						||
| 
								 | 
							
								        if ($this->options['help']) {
							 | 
						||
| 
								 | 
							
								            $this->printUsage();
							 | 
						||
| 
								 | 
							
								            exit(0);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        try {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $this->setupDirectory();
							 | 
						||
| 
								 | 
							
								            $this->setupCredentialsLoader();
							 | 
						||
| 
								 | 
							
								            $this->setupConfiguration();
							 | 
						||
| 
								 | 
							
								            $this->setupHooks();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $updated = $this->checkUpdates();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            // If called with --check, only return exit status
							 | 
						||
| 
								 | 
							
								            if ($this->options['check']) {
							 | 
						||
| 
								 | 
							
								                exit(($updated === null) ? 0 : 1);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            if ($updated !== null) {
							 | 
						||
| 
								 | 
							
								                $this->callHooks($updated);
							 | 
						||
| 
								 | 
							
								                $this->doUpdate($updated);
							 | 
						||
| 
								 | 
							
								                $this->callScript();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								                
							 | 
						||
| 
								 | 
							
								            exit(($updated === null) ? 0 : 1);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        } catch (\Throwable $t) {
							 | 
						||
| 
								 | 
							
								            fprintf(STDERR, "fatal: %s (%s#%d)\n", $t->getMessage(), $t->getFile(), $t->getLine());
							 | 
						||
| 
								 | 
							
								            if (!$this->options['verbose']) {
							 | 
						||
| 
								 | 
							
								                fprintf(STDERR, $this->log->asString()."\n");
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            exit(2);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Makes sure the directory is valid, and prepares the state file and the lockfile
							 | 
						||
| 
								 | 
							
								     * to be used.
							 | 
						||
| 
								 | 
							
								     * 
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    private function setupDirectory()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $this->path = $this->options['path'] ? realpath($this->options['path']) : getcwd();
							 | 
						||
| 
								 | 
							
								        if (!is_dir($this->path)) {
							 | 
						||
| 
								 | 
							
								            fwrite(STDERR, "error: No such path {$this->options['path']}\n");
							 | 
						||
| 
								 | 
							
								            exit(2);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        $this->log->append("Working dir: ".$this->path);
							 | 
						||
| 
								 | 
							
								        chdir($this->path);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->state = new PersistentState($this->path . "/fresh.yml");
							 | 
						||
| 
								 | 
							
								        $this->lockfile = new Lockfile($this->path . "/fresh.lock");
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Initialize the credentials loader to retrieve registry credentials
							 | 
						||
| 
								 | 
							
								     * 
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    private function setupCredentialsLoader()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        switch ($this->options['credentials']) {
							 | 
						||
| 
								 | 
							
								            case 'auto':
							 | 
						||
| 
								 | 
							
								            case 'basic':
							 | 
						||
| 
								 | 
							
								                $this->credentialsLoader = new BasicCredentialsLoader();
							 | 
						||
| 
								 | 
							
								                $this->log->append("Using BasicCredentialsLoader for credentials");
							 | 
						||
| 
								 | 
							
								                break;
							 | 
						||
| 
								 | 
							
								            default:
							 | 
						||
| 
								 | 
							
								                fwrite(STDERR, "error: Invalid credentials loader type\n");
							 | 
						||
| 
								 | 
							
								                exit(2);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Initialize the configuration to use for updating
							 | 
						||
| 
								 | 
							
								     * 
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    private function setupConfiguration()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (file_exists($this->path."/docker-compose.yml")) {
							 | 
						||
| 
								 | 
							
								            $this->config = new ComposeConfiguration($this->path."/docker-compose.yml");
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								            fwrite(STDERR, "error: Couldn't find a supported configuration file\n");
							 | 
						||
| 
								 | 
							
								            exit(2);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    /**
							 | 
						||
| 
								 | 
							
								     * Check for newer versions of the image(s)
							 | 
						||
| 
								 | 
							
								     * 
							 | 
						||
| 
								 | 
							
								     * @return array|null An array of information on updated images
							 | 
						||
| 
								 | 
							
								     */
							 | 
						||
| 
								 | 
							
								    private function checkUpdates(): ?array
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->lockfile->lock();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $checks = $this->config->getChecks();
							 | 
						||
| 
								 | 
							
								        if (count($checks) === 0) {
							 | 
						||
| 
								 | 
							
								            fwrite(STDERR, "error: couldn't find any images to check\n");
							 | 
						||
| 
								 | 
							
								            exit(2);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        $update = [];
							 | 
						||
| 
								 | 
							
								        $quiet = $this->options['quiet'];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->log->append("Checking ".count($checks)." images");
							 | 
						||
| 
								 | 
							
								        foreach ($checks as $check) {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $ref = new ImageReference($check);
							 | 
						||
| 
								 | 
							
								            $reg = $ref->getRegistry();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $credentials = $this->credentialsLoader->getCredentials($reg);
							 | 
						||
| 
								 | 
							
								            if (!$credentials) {
							 | 
						||
| 
								 | 
							
								                $this->log->append(sprintf("  %s: missing credentials for registry", $reg."/".$ref->getImage(), $ref->getRegistry()));
							 | 
						||
| 
								 | 
							
								                continue;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            $client = new RegistryV2Client($reg, $credentials);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $status = $client->getImageStatus($ref->getImage(), $ref->getTag());
							 | 
						||
| 
								 | 
							
								            $image = $reg."/".$status['image'].":".$status['tag'];
							 | 
						||
| 
								 | 
							
								            $oldHash = $this->state->get($image);
							 | 
						||
| 
								 | 
							
								            $newHash = $status['hash'];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            if ($oldHash != $newHash) {
							 | 
						||
| 
								 | 
							
								                if (!$quiet && !$this->options['verbose']) printf("%s: %s → %s\n", $image, $this->truncateHash($oldHash), $this->truncateHash($newHash));
							 | 
						||
| 
								 | 
							
								                $this->log->append(sprintf("  %s: %s → %s", $image, $this->truncateHash($oldHash), $this->truncateHash($newHash)));
							 | 
						||
| 
								 | 
							
								                $this->state->set($image, $newHash);
							 | 
						||
| 
								 | 
							
								                $update[] = (object)[
							 | 
						||
| 
								 | 
							
								                    'ref' => $ref,
							 | 
						||
| 
								 | 
							
								                    'old' => $oldHash,
							 | 
						||
| 
								 | 
							
								                    'new' => $newHash
							 | 
						||
| 
								 | 
							
								                ];
							 | 
						||
| 
								 | 
							
								            } else {
							 | 
						||
| 
								 | 
							
								                $this->log->append(sprintf("  %s: %s", $image, $this->truncateHash($newHash)));
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        return empty($update) ? null : $update;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    public function doUpdate()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $this->log->append("Pulling updated images...");
							 | 
						||
| 
								 | 
							
								        $this->exec("docker-compose pull -q");
							 | 
						||
| 
								 | 
							
								        if (!$this->options['pull']) {
							 | 
						||
| 
								 | 
							
								            $this->log->append("Refreshing updated containers...");
							 | 
						||
| 
								 | 
							
								            $this->exec("docker-compose up -d");
							 | 
						||
| 
								 | 
							
								            if ($this->options['prune']) {
							 | 
						||
| 
								 | 
							
								                $this->log->append("Pruning dangling images...");
							 | 
						||
| 
								 | 
							
								                $this->exec("docker image prune -f");
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $this->log->append("Flushing state...");
							 | 
						||
| 
								 | 
							
								        $this->state->flush();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private function setupHooks()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $slackUrl = $this->options['slack'];
							 | 
						||
| 
								 | 
							
								        if ($slackUrl) {
							 | 
						||
| 
								 | 
							
								            $this->hooks[] = new SlackHook([
							 | 
						||
| 
								 | 
							
								                'url' => $slackUrl
							 | 
						||
| 
								 | 
							
								            ]);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private function callHooks(array $updated)
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $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);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        foreach ($this->hooks as $hook) {
							 | 
						||
| 
								 | 
							
								            $hook->sendMessage($msg);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private function callScript()
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $script = $this->options['script'];
							 | 
						||
| 
								 | 
							
								        if ($script) {
							 | 
						||
| 
								 | 
							
								            $this->exec($script);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private function truncateHash(?string $hash): string
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        if ($hash === null) return '?';
							 | 
						||
| 
								 | 
							
								        return substr($hash, 0, 4) . ".." . substr($hash, -4, 4);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    private function exec(string $cmdl)
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								        $this->log->append("$ {$cmdl}");
							 | 
						||
| 
								 | 
							
								        $fd = popen($cmdl." 2>&1","r");
							 | 
						||
| 
								 | 
							
								        while (!feof($fd)) {
							 | 
						||
| 
								 | 
							
								            $s = rtrim(fgets($fd));
							 | 
						||
| 
								 | 
							
								            if (trim($s))
							 | 
						||
| 
								 | 
							
								                $this->log->append($s);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        fclose($fd);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 |