Initial commit
This commit is contained in:
		
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# Fresh: Keeping your docker stacks up to date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					    -d,--dir <path>         Chdir to path on startup
 | 
				
			||||||
 | 
					    -c,--config <file>      Read file as docker-compose.yml
 | 
				
			||||||
 | 
					    -i,--image <image>      Only check if a newer image exists, set exitcode
 | 
				
			||||||
 | 
					    -p,--pull               Only pull, don't restart anything
 | 
				
			||||||
 | 
					    --credentials <type>    Credentials loader type (auto, basic)
 | 
				
			||||||
 | 
					    --check                 Only check (set exitcode)
 | 
				
			||||||
 | 
					    --slack <url>           Notify a Slack webhook on update
 | 
				
			||||||
 | 
					    -q,--quiet              Don't show any extra output (except dockers)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Update everything and up any new containers in current dir:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					$ fresh.phar 
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Manually pulling when changed:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					if ! fresh.phar --check; then
 | 
				
			||||||
 | 
					    docker-compose pull
 | 
				
			||||||
 | 
					    docker-compose up -d
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										133
									
								
								bin/freshdocker
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										133
									
								
								bin/freshdocker
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env php
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\Configuration\DockerComposeConfiguration;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\Configuration\LocalConfiguration;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\Credentials\BasicCredentialsLoader;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\Hooks\SlackHook;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\ImageReference;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\Registry\RegistryV2Client;
 | 
				
			||||||
 | 
					use NoccyLabs\FreshDocker\State\PersistentState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require_once __DIR__."/../vendor/autoload.php";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$opts = getopt("hd:c:i:q", [ "help", "dir:", "config:", "image:", "pull", "credentials:", "check", "slack:", "quiet" ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (array_key_exists('h', $opts) || array_key_exists('help', $opts)) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    printf("Usage:\n    %s [options]\n\n", basename($argv[0]));
 | 
				
			||||||
 | 
					    printf("Options:\n");
 | 
				
			||||||
 | 
					    foreach([
 | 
				
			||||||
 | 
					        '-h,--help' => "Show this help",
 | 
				
			||||||
 | 
					        '-d,--dir DIR' => "Change working directory to DIR",
 | 
				
			||||||
 | 
					        '-c,--config FILE' => "Use a custom configuration (not implemented)",
 | 
				
			||||||
 | 
					        '-i,--image REF' => "Check a specific image instead of using config/docker-compose",
 | 
				
			||||||
 | 
					        '--pull' => "Only pull if new, don't up",
 | 
				
			||||||
 | 
					        '--credentials TYPE' => "Select credentials loader (auto or basic)",
 | 
				
			||||||
 | 
					        '--check' => "Only check, set exit code 1 if not fresh",
 | 
				
			||||||
 | 
					        '-q,--quiet' => "Don't show any output (except dockers)",
 | 
				
			||||||
 | 
					    ] 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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$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) {
 | 
				
			||||||
 | 
					        //printf("notice: missing credentials for %s, skipping image %s\n", $reg, $check);
 | 
				
			||||||
 | 
					        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");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exit($update?1:0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function truncate(?string $hash): string
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    if ($hash === null) return '*';
 | 
				
			||||||
 | 
					    return substr($hash, 0, 4) . ".." . substr($hash, -4, 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										25
									
								
								composer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								composer.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "name": "noccylabs/fresh-docker",
 | 
				
			||||||
 | 
					    "description": "Update docker-compose stacks when new versions are pushed",
 | 
				
			||||||
 | 
					    "type": "application",
 | 
				
			||||||
 | 
					    "license": "MIT",
 | 
				
			||||||
 | 
					    "autoload": {
 | 
				
			||||||
 | 
					        "psr-4": {
 | 
				
			||||||
 | 
					            "NoccyLabs\\FreshDocker\\": "src/"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "require": {
 | 
				
			||||||
 | 
					        "php": "^8.0",
 | 
				
			||||||
 | 
					        "ext-json": "*",
 | 
				
			||||||
 | 
					        "symfony/yaml": "^6.0",
 | 
				
			||||||
 | 
					        "guzzlehttp/guzzle": "^7.4"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "bin": [
 | 
				
			||||||
 | 
					        "bin/freshdocker"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "extra": {
 | 
				
			||||||
 | 
					        "phar": {
 | 
				
			||||||
 | 
					            "output": "fresh.phar"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/Configuration/DockerComposeConfiguration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Configuration/DockerComposeConfiguration.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Symfony\Component\Yaml\Yaml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DockerComposeConfiguration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private array $checks = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(string $configfile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->loadComposeFile($configfile);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function loadComposeFile($configfile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $config = Yaml::parseFile($configfile);
 | 
				
			||||||
 | 
					        $services = $config['services']??[];
 | 
				
			||||||
 | 
					        foreach ($services as $name=>$service) {
 | 
				
			||||||
 | 
					            $image = $service['image']??null;
 | 
				
			||||||
 | 
					            if ($image && !in_array($image,$this->checks)) {
 | 
				
			||||||
 | 
					                $this->checks[] = $image;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getChecks(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->checks;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/Configuration/LocalConfiguration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/Configuration/LocalConfiguration.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Symfony\Component\Yaml\Yaml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LocalConfiguration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private array $config = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(string $configfile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->config = Yaml::parseFile($configfile);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getChecks(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->config['check']??[];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/Credentials/BasicCredentialsLoader.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/Credentials/BasicCredentialsLoader.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\Credentials;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BasicCredentialsLoader
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private array $auth = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->load();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function load()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $config = getenv("HOME")."/.docker/config.json";
 | 
				
			||||||
 | 
					        if (!file_exists($config)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $conf = json_decode(file_get_contents($config));
 | 
				
			||||||
 | 
					        $this->auth = isset($conf->auths) ? (array)$conf->auths : [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getCredentials(string $repo): ?array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!array_key_exists($repo, $this->auth))
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        $auth = $this->auth[$repo]->auth;
 | 
				
			||||||
 | 
					        return explode(":", base64_decode($auth), 2);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/Hooks/SlackHook.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/Hooks/SlackHook.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\Hooks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use GuzzleHttp\Client;
 | 
				
			||||||
 | 
					use RuntimeException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SlackHook
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private array $options = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(array $options = [])
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->options = $options;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function sendMessage(string $text, array $options)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $mergedOptions = array_merge($this->options, $options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $url = $mergedOptions['url'] ?? throw new RuntimeException("Empty hook url");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $body = $this->makeBody($text, $mergedOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $client = new Client();
 | 
				
			||||||
 | 
					        $client->post($url, [
 | 
				
			||||||
 | 
					            'json' => $body
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function makeBody(string $text, array $options)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        /*
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					  "channel": "town-square",
 | 
				
			||||||
 | 
					  "username": "test-automation",
 | 
				
			||||||
 | 
					  "icon_url": "https://mattermost.org/wp-content/uploads/2016/04/icon.png",
 | 
				
			||||||
 | 
					  "text": "#### Test results for July 27th, 2017\n<!channel> please review failed tests.\n
 | 
				
			||||||
 | 
					  | Component  | Tests Run   | Tests Failed                                   |
 | 
				
			||||||
 | 
					  |:-----------|:-----------:|:-----------------------------------------------|
 | 
				
			||||||
 | 
					  | Server     | 948         | :white_check_mark: 0                           |
 | 
				
			||||||
 | 
					  | Web Client | 123         | :warning: 2 [(see details)](http://linktologs) |
 | 
				
			||||||
 | 
					  | iOS Client | 78          | :warning: 3 [(see details)](http://linktologs) |
 | 
				
			||||||
 | 
					  "
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					        */
 | 
				
			||||||
 | 
					        $body = [];
 | 
				
			||||||
 | 
					        if (array_key_exists('channel', $options)) $body['channel'] = $options['channel'];
 | 
				
			||||||
 | 
					        if (array_key_exists('username', $options)) $body['username'] = $options['username'];
 | 
				
			||||||
 | 
					        if (array_key_exists('icon_url', $options)) $body['icon_url'] = $options['icon_url'];
 | 
				
			||||||
 | 
					        $body['text'] = $text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $body;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/ImageReference.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/ImageReference.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImageReference
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private $image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $tag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $registry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $scheme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(string $image)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // if (!preg_match('/^http[s]:\/\//i', $image)) {
 | 
				
			||||||
 | 
					        $parts = explode("/", $image);
 | 
				
			||||||
 | 
					        if (count($parts) < 3) {
 | 
				
			||||||
 | 
					            $image = "https://registry.hub.docker.com/{$image}";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $image = "https://{$image}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        $parts = parse_url($image);
 | 
				
			||||||
 | 
					        $image = ltrim($parts['path'],'/');
 | 
				
			||||||
 | 
					        if (str_contains($image,':')) {
 | 
				
			||||||
 | 
					            [$image, $tag] = explode(":", $image, 2);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $tag = 'latest';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!str_contains($image,'/')) {
 | 
				
			||||||
 | 
					            $image = "library/{$image}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $this->image = $image;
 | 
				
			||||||
 | 
					        $this->tag = $tag;
 | 
				
			||||||
 | 
					        $this->registry = $parts['host'];
 | 
				
			||||||
 | 
					        $this->scheme = $parts['scheme'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getRegistry(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->registry;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getImage(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->image;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getTag(): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->tag;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/Registry/RegistryV2Client.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/Registry/RegistryV2Client.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\Registry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use GuzzleHttp\Client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RegistryV2Client
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private Client $client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(string $registry, ?array $auth)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->createClient($registry, $auth);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function createClient(string $registry, ?array $auth)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->client = new Client([
 | 
				
			||||||
 | 
					            'base_uri' => 'https://' . $registry . '/v2/',
 | 
				
			||||||
 | 
					            'auth' => $auth,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getManifest(string $image, string $tag)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $response = $this->client->get("{$image}/manifests/{$tag}");
 | 
				
			||||||
 | 
					        $body = $response->getBody();
 | 
				
			||||||
 | 
					        return json_decode($body);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getImageStatus(string $image, string $tag)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $manifest = $this->getManifest($image, $tag);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $fslayers = (array)$manifest->fsLayers;
 | 
				
			||||||
 | 
					        $fshashes = array_map(fn($layer) => $layer->blobSum, $fslayers);
 | 
				
			||||||
 | 
					        $metahash = hash("sha256", join("|", $fshashes));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $history = $manifest->history[0];
 | 
				
			||||||
 | 
					        $info = json_decode($history->v1Compatibility);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'image' => $image,
 | 
				
			||||||
 | 
					            'tag' => $tag,
 | 
				
			||||||
 | 
					            'hash' => $metahash,
 | 
				
			||||||
 | 
					            'created' => $info->created??null,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/State/PersistentState.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/State/PersistentState.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\FreshDocker\State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Symfony\Component\Yaml\Yaml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PersistentState
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    private string $filename;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private array $state = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private bool $dirty = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(string $filename)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->filename = $filename;
 | 
				
			||||||
 | 
					        register_shutdown_function([$this,"flush"]);
 | 
				
			||||||
 | 
					        if (file_exists($filename)) {
 | 
				
			||||||
 | 
					            $this->state = Yaml::parseFile($filename);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function flush()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!$this->dirty) return;
 | 
				
			||||||
 | 
					        $yaml = Yaml::dump($this->state);
 | 
				
			||||||
 | 
					        file_put_contents($this->filename."~", $yaml);
 | 
				
			||||||
 | 
					        rename($this->filename."~", $this->filename);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function set(string $key, $value)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->state[$key] = $value;
 | 
				
			||||||
 | 
					        $this->dirty = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function get(string $key, $default=null): mixed
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->state[$key] ?? $default;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function unset(string $key)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        unset($this->state[$key]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user