├── .dockerignore ├── .gitignore ├── .travis.yml ├── README.md ├── app ├── .htaccess ├── AppCache.php ├── AppKernel.php ├── Resources │ ├── DoctrineMigrations │ │ ├── Version20160106103017.php │ │ ├── Version20160111143118.php │ │ └── Version20160305162418.php │ ├── FOSUserBundle │ │ └── views │ │ │ ├── Registration │ │ │ └── register_content.html.twig │ │ │ ├── Security │ │ │ └── login.html.twig │ │ │ └── layout.html.twig │ ├── KnpPaginatorBundle │ │ └── views │ │ │ └── Pagination │ │ │ └── twitter_bootstrap_v3_pagination.html.twig │ ├── assets │ │ ├── js │ │ │ └── repository.js │ │ └── scss │ │ │ ├── main.scss │ │ │ └── pager.scss │ ├── broker │ │ └── mapping.yml │ ├── translations │ │ └── messages.en.yml │ └── views │ │ ├── account │ │ └── index.html.twig │ │ ├── admin │ │ ├── repository │ │ │ └── index.html.twig │ │ └── user │ │ │ └── index.html.twig │ │ ├── base.html.twig │ │ ├── default │ │ ├── index.html.twig │ │ ├── last_public_repositories.html.twig │ │ ├── most_pulled_repositories.html.twig │ │ └── most_stared_repositories.html.twig │ │ ├── layout.html.twig │ │ ├── layout │ │ ├── navbar.html.twig │ │ └── notifications.html.twig │ │ ├── macros.html.twig │ │ ├── repository │ │ ├── _edit.html.twig │ │ ├── _view.html.twig │ │ ├── index.html.twig │ │ ├── layout.html.twig │ │ └── webhooks.html.twig │ │ └── search │ │ └── results.html.twig ├── autoload.php └── config │ ├── config.yml │ ├── config_dev.yml │ ├── config_prod.yml │ ├── config_test.yml │ ├── fos_user.yml │ ├── parameters.yml.dist │ ├── routing.yml │ ├── routing_dev.yml │ ├── security.yml │ ├── services.yml │ └── swarrot.yml ├── behat.yml.dist ├── bin ├── console ├── run-tests └── symfony_requirements ├── bower.json ├── composer.json ├── composer.lock ├── docker-compose.yml ├── docker ├── build │ ├── Dockerfile │ ├── Dockerfile-from-source │ ├── README-short.md │ ├── README.md │ └── entrypoint.sh ├── docker-compose.nodejs.yml └── elasticsearch │ └── Dockerfile ├── features ├── account.feature ├── bootstrap │ ├── AuthenticationContext.php │ ├── EntityContext.php │ └── RestContext.php ├── fixtures │ ├── layers │ │ ├── 03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb │ │ └── a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 │ └── manifests │ │ ├── hello-world:latest.json │ │ ├── secret-world:latest.json │ │ ├── test~hello-world:latest.json │ │ └── test~secret-world:latest.json ├── homepage.feature ├── registration.feature ├── registry │ ├── access_manifest.feature │ ├── token.feature │ ├── upload_manifest.feature │ └── version.feature ├── repository_description.feature └── search.feature ├── gulpfile.js ├── package.json ├── phpunit.xml.dist ├── src ├── .htaccess └── AppBundle │ ├── AppBundle.php │ ├── Broker │ └── Processor │ │ └── WebhookProcessor.php │ ├── Command │ └── SetupBrokerCommand.php │ ├── Controller │ ├── AccountController.php │ ├── Admin │ │ ├── RepositoryController.php │ │ └── UserController.php │ ├── DefaultController.php │ ├── Registry │ │ ├── LayerController.php │ │ ├── ManifestController.php │ │ ├── TokenController.php │ │ └── VersionController.php │ ├── RepositoryController.php │ └── SearchController.php │ ├── Entity │ ├── Layer.php │ ├── Manifest.php │ ├── ManifestRepository.php │ ├── Repository.php │ ├── RepositoryRepository.php │ ├── RepositoryStar.php │ ├── RepositoryStarListener.php │ ├── RepositoryStarRepository.php │ ├── User.php │ └── Webhook.php │ ├── Event │ ├── DelayedEvent.php │ └── ManifestEvent.php │ ├── EventListener │ ├── DelayedEventListener.php │ ├── HeaderResponseListener.php │ ├── ManifestPullListener.php │ ├── ManifestPushListener.php │ └── RegistryExceptionListener.php │ ├── Form │ └── Type │ │ ├── RepositoryType.php │ │ └── WebhookType.php │ ├── Manager │ ├── LayerManager.php │ ├── RepositoryStarManager.php │ └── SearchManager.php │ ├── Security │ ├── RegistryEntryPoint.php │ └── Voter │ │ └── RepositoryVoter.php │ └── Twig │ └── AppExtension.php ├── var ├── SymfonyRequirements.php ├── cache │ └── .gitkeep ├── jwt │ ├── private.pem.dist │ └── public.pem.dist ├── logs │ └── .gitkeep └── storage │ └── .gitkeep └── web ├── .htaccess ├── app.php ├── app_dev.php ├── apple-touch-icon.png ├── assets ├── css │ └── main.css ├── fonts │ ├── bootstrap │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 └── js │ └── main.js ├── config.php ├── favicon.ico └── robots.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | /app/config/parameters.yml 3 | /build/ 4 | /phpunit.xml 5 | /var/* 6 | !/var/cache 7 | /var/cache/* 8 | !var/cache/.gitkeep 9 | !/var/logs 10 | /var/logs/* 11 | !var/logs/.gitkeep 12 | !/var/sessions 13 | /var/sessions/* 14 | !var/sessions/.gitkeep 15 | !var/storage 16 | /var/storage/* 17 | !var/storage/.gitkeep 18 | !var/jwt 19 | /var/jwt/* 20 | !var/jwt/*.dist 21 | !var/SymfonyRequirements.php 22 | /vendor/ 23 | /web/bundles/ 24 | /bin/* 25 | !/bin/run-tests 26 | !bin/console 27 | !bin/symfony_requirements 28 | /bower_components/ 29 | /node_modules/ 30 | /.sass-cache/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /app/config/parameters.yml 2 | /build/ 3 | /phpunit.xml 4 | /var/* 5 | !/var/cache 6 | /var/cache/* 7 | !var/cache/.gitkeep 8 | !/var/logs 9 | /var/logs/* 10 | !var/logs/.gitkeep 11 | !/var/sessions 12 | /var/sessions/* 13 | !var/sessions/.gitkeep 14 | !var/storage 15 | /var/storage/* 16 | !var/storage/.gitkeep 17 | !var/jwt 18 | /var/jwt/* 19 | !var/jwt/*.dist 20 | !var/SymfonyRequirements.php 21 | /vendor/ 22 | /web/bundles/ 23 | /bin/* 24 | !/bin/run-tests 25 | !bin/console 26 | !bin/symfony_requirements 27 | /bower_components/ 28 | /node_modules/ 29 | /.sass-cache/ 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - hhvm 8 | 9 | services: 10 | - mysql 11 | - elasticsearch 12 | 13 | matrix: 14 | allow_failures: 15 | - php: hhvm 16 | 17 | env: 18 | global: 19 | - DK_DATABASE_HOST=127.0.0.1 20 | - DK_DATABASE_PASSWORD=null 21 | - DK_ELASTICSEARCH_HOST=127.0.0.1 22 | - DK_ELASTICSEARCH_PORT=9200 23 | 24 | before_script: 25 | - cp var/jwt/private.pem.dist var/jwt/private.pem 26 | - cp var/jwt/public.pem.dist var/jwt/public.pem 27 | - if [[ $TRAVIS_PHP_VERSION != hhvm ]]; then phpenv config-rm xdebug.ini; fi; 28 | - composer self-update 29 | - composer install --no-interaction 30 | 31 | script: 32 | - ./bin/run-tests 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dunkerque 2 | ========= 3 | 4 | [![Build Status](https://api.travis-ci.org/iamluc/dunkerque.png?branch=master)](https://travis-ci.org/iamluc/dunkerque) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/8789214a-26f9-42b6-a98b-de4e3fd5ba8e/mini.png)](https://insight.sensiolabs.com/projects/8789214a-26f9-42b6-a98b-de4e3fd5ba8e) 5 | 6 | # About 7 | 8 | Docker hub & registry. 9 | 10 | Written in PHP with Symfony. 11 | 12 | **THIS PROJECT IS IN ALPHA STATE** 13 | 14 | # Docker image 15 | 16 | If you just want to use dunkerque, use the [docker image on the docker hub](https://hub.docker.com/r/iamluc/dunkerque/), 17 | and read the dedicated [README](https://github.com/iamluc/dunkerque/blob/master/docker/build/README.md) 18 | 19 | # Install 20 | 21 | Base 22 | 23 | ```sh 24 | # Clone repository 25 | git clone https://github.com/iamluc/dunkerque 26 | 27 | # Enter directory 28 | cd dunkerque 29 | 30 | # Run server (Adapt file `docker-compose.yml` to your needs) 31 | docker-compose up -d 32 | 33 | # Generate keys (Default passphrase is `DunkerqueIsOnFire`) 34 | docker-compose run --rm app openssl genrsa -out var/jwt/private.pem -aes256 4096 35 | docker-compose run --rm app openssl rsa -pubout -in var/jwt/private.pem -out var/jwt/public.pem 36 | 37 | # Install dependencies 38 | docker-compose run --rm app composer install 39 | 40 | # Initialize database 41 | docker-compose run --rm app bin/console doctrine:database:create --if-not-exists 42 | docker-compose run --rm app bin/console doctrine:migrations:migrate 43 | 44 | # Initialize search 45 | docker-compose run --rm app bin/console fos:elastica:populate 46 | 47 | # Create a user 48 | docker-compose run --rm app bin/console fos:user:create 49 | ``` 50 | 51 | # Develop on Dunkerque 52 | 53 | ```sh 54 | # Install dev dependencies 55 | docker-compose -f docker/docker-compose.nodejs.yml run --rm nodejs npm install 56 | 57 | # Push (already existing) repository to Dunkerque 58 | docker push 127.0.0.1:8000/user/repo 59 | 60 | # Compile SASS and JS 61 | docker-compose -f docker/docker-compose.nodejs.yml run --rm nodejs gulp 62 | 63 | # Run tests 64 | docker-compose run --rm app bin/run-tests 65 | ``` 66 | 67 | # LICENSE 68 | 69 | [MIT](https://opensource.org/licenses/MIT) 70 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /app/AppCache.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), ['dev', 'test'], true)) { 34 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); 35 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 36 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); 37 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); 38 | } 39 | 40 | return $bundles; 41 | } 42 | 43 | public function getRootDir() 44 | { 45 | return __DIR__; 46 | } 47 | 48 | public function getCacheDir() 49 | { 50 | return dirname(__DIR__).'/var/cache/'.$this->getEnvironment(); 51 | } 52 | 53 | public function getLogDir() 54 | { 55 | return dirname(__DIR__).'/var/logs'; 56 | } 57 | 58 | public function registerContainerConfiguration(LoaderInterface $loader) 59 | { 60 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Resources/DoctrineMigrations/Version20160106103017.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 20 | 21 | $this->addSql('CREATE TABLE webhook (id INT AUTO_INCREMENT NOT NULL, repository_id INT NOT NULL, name VARCHAR(50) NOT NULL, url VARCHAR(255) NOT NULL, last_call DATETIME DEFAULT NULL, last_status VARCHAR(50) DEFAULT NULL, INDEX IDX_8A74175650C9D4F7 (repository_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 22 | $this->addSql('CREATE TABLE repository (id INT AUTO_INCREMENT NOT NULL, owner_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, title VARCHAR(255) DEFAULT NULL, description LONGTEXT DEFAULT NULL, private TINYINT(1) NOT NULL, stars INT NOT NULL, pulls INT NOT NULL, INDEX IDX_5CFE57CD7E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 23 | $this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(255) NOT NULL, username_canonical VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, email_canonical VARCHAR(255) NOT NULL, enabled TINYINT(1) NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, last_login DATETIME DEFAULT NULL, locked TINYINT(1) NOT NULL, expired TINYINT(1) NOT NULL, expires_at DATETIME DEFAULT NULL, confirmation_token VARCHAR(255) DEFAULT NULL, password_requested_at DATETIME DEFAULT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', credentials_expired TINYINT(1) NOT NULL, credentials_expire_at DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D64992FC23A8 (username_canonical), UNIQUE INDEX UNIQ_8D93D649A0D96FBF (email_canonical), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 24 | $this->addSql('CREATE TABLE layer (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(255) NOT NULL, digest VARCHAR(255) DEFAULT NULL, status INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 25 | $this->addSql('CREATE TABLE manifest (id INT AUTO_INCREMENT NOT NULL, repository_id INT NOT NULL, tag VARCHAR(255) NOT NULL, digest VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, pulls INT NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_A6FA684050C9D4F7 (repository_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 26 | $this->addSql('ALTER TABLE webhook ADD CONSTRAINT FK_8A74175650C9D4F7 FOREIGN KEY (repository_id) REFERENCES repository (id)'); 27 | $this->addSql('ALTER TABLE repository ADD CONSTRAINT FK_5CFE57CD7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)'); 28 | $this->addSql('ALTER TABLE manifest ADD CONSTRAINT FK_A6FA684050C9D4F7 FOREIGN KEY (repository_id) REFERENCES repository (id)'); 29 | } 30 | 31 | /** 32 | * @param Schema $schema 33 | */ 34 | public function down(Schema $schema) 35 | { 36 | // this down() migration is auto-generated, please modify it to your needs 37 | $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 38 | 39 | $this->addSql('ALTER TABLE webhook DROP FOREIGN KEY FK_8A74175650C9D4F7'); 40 | $this->addSql('ALTER TABLE manifest DROP FOREIGN KEY FK_A6FA684050C9D4F7'); 41 | $this->addSql('ALTER TABLE repository DROP FOREIGN KEY FK_5CFE57CD7E3C61F9'); 42 | $this->addSql('DROP TABLE webhook'); 43 | $this->addSql('DROP TABLE repository'); 44 | $this->addSql('DROP TABLE user'); 45 | $this->addSql('DROP TABLE layer'); 46 | $this->addSql('DROP TABLE manifest'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Resources/DoctrineMigrations/Version20160111143118.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 20 | 21 | $this->addSql('CREATE TABLE repository_star (user_id INT NOT NULL, repository_id INT NOT NULL, INDEX IDX_7AF57DFCA76ED395 (user_id), INDEX IDX_7AF57DFC50C9D4F7 (repository_id), PRIMARY KEY(user_id, repository_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 22 | $this->addSql('ALTER TABLE repository_star ADD CONSTRAINT FK_7AF57DFCA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); 23 | $this->addSql('ALTER TABLE repository_star ADD CONSTRAINT FK_7AF57DFC50C9D4F7 FOREIGN KEY (repository_id) REFERENCES repository (id)'); 24 | } 25 | 26 | /** 27 | * @param Schema $schema 28 | */ 29 | public function down(Schema $schema) 30 | { 31 | // this down() migration is auto-generated, please modify it to your needs 32 | $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 33 | 34 | $this->addSql('DROP TABLE repository_star'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Resources/DoctrineMigrations/Version20160305162418.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 20 | 21 | $this->addSql('CREATE UNIQUE INDEX UNIQ_5CFE57CD5E237E06 ON repository (name)'); 22 | } 23 | 24 | /** 25 | * @param Schema $schema 26 | */ 27 | public function down(Schema $schema) 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 31 | 32 | $this->addSql('DROP INDEX UNIQ_5CFE57CD5E237E06 ON repository'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Resources/FOSUserBundle/views/Registration/register_content.html.twig: -------------------------------------------------------------------------------- 1 | {% trans_default_domain 'FOSUserBundle' %} 2 | 3 | {{ form_start(form, {'method': 'post', 'action': path('fos_user_registration_register'), 'attr': {'class': 'fos_user_registration_register'}}) }} 4 | {{ form_widget(form) }} 5 |
6 | 7 |
8 | {{ form_end(form) }} 9 | -------------------------------------------------------------------------------- /app/Resources/FOSUserBundle/views/Security/login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "FOSUserBundle::layout.html.twig" %} 2 | 3 | {% trans_default_domain 'FOSUserBundle' %} 4 | 5 | {% block fos_user_content %} 6 | {% if error %} 7 |
{{ error.messageKey|trans(error.messageData, 'security') }}
8 | {% endif %} 9 | 10 |
11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 28 |
29 | 30 | 31 |
32 | {% endblock fos_user_content %} 33 | -------------------------------------------------------------------------------- /app/Resources/FOSUserBundle/views/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 | {% block fos_user_content %}{% endblock %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /app/Resources/KnpPaginatorBundle/views/Pagination/twitter_bootstrap_v3_pagination.html.twig: -------------------------------------------------------------------------------- 1 | {% if pageCount > 1 %} 2 | 77 | {% endif %} 78 | -------------------------------------------------------------------------------- /app/Resources/assets/js/repository.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#repository-star').click(function(e) { 3 | var link = $(this); 4 | var icon = link.find('.glyphicon'); 5 | 6 | $.ajax( 7 | icon.hasClass('glyphicon-star') ? link.attr('data-url-unstar') : link.attr('data-url-star'), 8 | {method: 'PUT'} 9 | ).done(function(data) { 10 | if (data.starred) { 11 | icon.addClass('glyphicon-star').removeClass('glyphicon-star-empty'); 12 | } else { 13 | icon.addClass('glyphicon-star-empty').removeClass('glyphicon-star'); 14 | } 15 | }); 16 | e.preventDefault(); 17 | }); 18 | }) 19 | -------------------------------------------------------------------------------- /app/Resources/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "_bootstrap"; 2 | @import "pager"; 3 | 4 | .tabs-menu { 5 | margin-bottom: 20px; 6 | } 7 | 8 | .panel-body > ul { 9 | padding-left: 10px; 10 | } 11 | 12 | li { 13 | list-style: none; 14 | } 15 | -------------------------------------------------------------------------------- /app/Resources/assets/scss/pager.scss: -------------------------------------------------------------------------------- 1 | .pager-filter-container { 2 | display: none; 3 | padding-top: 5px; 4 | margin-left: -15px; 5 | } 6 | 7 | input:focus ~ .pager-filter-container, .pager-filter-container:hover { 8 | display: block; 9 | z-index: 1000; 10 | position: absolute; 11 | padding-bottom: 160px; /* Large padding-bottom to block mouseout */ 12 | } 13 | 14 | .pager-filter-container-body { 15 | position: relative; 16 | background-color: white; 17 | border: 1px solid #DDDDDD; 18 | padding: 20px 40px; 19 | -webkit-border-radius: 0px 0px 5px 5px; 20 | -moz-border-radius: 0px 0px 5px 5px; 21 | border-radius: 0px 0px 5px 5px; 22 | } 23 | 24 | .pager-filter-container-body:after, .pager-filter-container-body:before { 25 | bottom: 100%; 26 | border: solid transparent; 27 | content: " "; 28 | height: 0; 29 | width: 0; 30 | position: absolute; 31 | pointer-events: none; 32 | } 33 | 34 | .pager-filter-container-body:after { 35 | border-color: rgba(255, 255, 255, 0); 36 | border-bottom-color: #ffffff; 37 | border-width: 10px; 38 | left: 15%; 39 | margin-left: -10px; 40 | } 41 | 42 | .pager-filter-container-body:before { 43 | border-color: rgba(221, 221, 221, 0); 44 | border-bottom-color: #DDDDDD; 45 | border-width: 11px; 46 | left: 15%; 47 | margin-left: -11px; 48 | } 49 | 50 | .masked { 51 | position: absolute; 52 | left: -500px; 53 | } -------------------------------------------------------------------------------- /app/Resources/broker/mapping.yml: -------------------------------------------------------------------------------- 1 | /: 2 | exchanges: 3 | dk.manifest: 4 | type: topic 5 | durable: true 6 | 7 | queues: 8 | dk.manifest_webhook: 9 | durable: true 10 | bindings: 11 | - 12 | exchange: dk.manifest 13 | routing_key: push 14 | -------------------------------------------------------------------------------- /app/Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | welcome_user: "Welcome %username%" 2 | 3 | welcome_dunkerque.title: "Welcome to Dunkerque" 4 | welcome_dunkerque.body: "Your new docker registry !" 5 | 6 | users: "Users" 7 | add: "Add" 8 | delete: "Delete" 9 | 10 | invalid_csrf_token: "Invalid token, please refresh your page" 11 | 12 | search.find: "Find..." 13 | search.results_found: "{0} No result found for \"%keyword%\"|{1} 1 result found for \"%keyword%\"|]1,Inf[ %count% results found for \"%keyword%\"" 14 | 15 | sort._score: "Relevance" 16 | sort.pulls: "Pulls" 17 | sort.stars: "Stars" 18 | 19 | repositories: "Repositories" 20 | repositories.most_stared: "Most stared" 21 | repositories.most_pulled: "Most pulled" 22 | repositories.last_public: "Latest public" 23 | 24 | repository.name: "Name" 25 | repository.stars: "Stars" 26 | repository.pulls: "Pulls" 27 | repository.is_private: "Private" 28 | repository.private: "Private" 29 | repository.public: "Public" 30 | repository.description: "Description" 31 | repository.pull: "Pull" 32 | repository.tags: "Tags" 33 | repository.title: "Title" 34 | repository.webhooks: "Webhooks" 35 | 36 | webhook.name: "Name" 37 | webhook.url: "Url" 38 | webhook.add: "Add" 39 | webhook.last_call: "Last call" 40 | webhook.last_status: "Last status" 41 | webhook.confirm_remove: "Do you really want to remove this webhook ?" 42 | webhook.not_found_or_not_granted: "Webhook not found, or you cannot access it." 43 | webhook.removed: "Webhook removed" 44 | webhook.created: "Webhook created" 45 | webhook.list: "Existing webhooks" 46 | -------------------------------------------------------------------------------- /app/Resources/views/account/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% import 'DatathekePagerBundle:Pager:bootstrap3.html.twig' as helper %} 4 | {% import 'macros.html.twig' as macros %} 5 | 6 | {% block content %} 7 |

{{ 'repositories'|trans }}

8 | 9 |
10 | {{ helper.toolbar(pager) }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for repo in pager.items %} 23 | 24 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
{{ helper.orderBy(pager, 'name', 'repository.name'|trans) }}{{ helper.orderBy(pager, 'stars', 'repository.stars'|trans) }}{{ helper.orderBy(pager, 'pulls', 'repository.pulls'|trans) }}{{ helper.orderBy(pager, 'private', 'repository.is_private'|trans) }}
25 | {{ repo.name }} 26 |
{{ repo.title }}
27 |
{{ repo.stars }} {{ repo.pulls }} {{ macros.repository_status(repo) }}
35 | 36 | {{ helper.paginate(pager) }} 37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /app/Resources/views/admin/repository/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 |

{{ 'repositories'|trans }}

5 | {{ datagrid_content(datagrid) }} 6 | {% endblock %} 7 | 8 | {% block stylesheets %} 9 | {{ parent() }} 10 | {{ datagrid_stylesheets(datagrid, {no_external: true}) }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/Resources/views/admin/user/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 |

{{ 'users'|trans }}

5 | {{ datagrid_content(datagrid) }} 6 | {% endblock %} 7 | 8 | {% block stylesheets %} 9 | {{ parent() }} 10 | {{ datagrid_stylesheets(datagrid, {no_externals: true}) }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/Resources/views/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Welcome!{% endblock %} 9 | {% block stylesheets %}{% endblock %} 10 | 11 | 12 | 13 | {% block body %}{% endblock %} 14 | {% block javascripts %}{% endblock %} 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/Resources/views/default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |

{{ 'welcome_dunkerque.title'|trans }}

8 |
9 |
10 | {{ 'welcome_dunkerque.body'|trans }} 11 |
12 |
13 | 14 |
15 |
{{ render(controller('AppBundle:Default:lastPublicRepositories')) }}
16 |
{{ render(controller('AppBundle:Default:mostStaredRepositories')) }}
17 |
{{ render(controller('AppBundle:Default:mostPulledRepositories')) }}
18 |
19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /app/Resources/views/default/last_public_repositories.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'repositories.last_public'|trans }}

4 |
5 |
6 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /app/Resources/views/default/most_pulled_repositories.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'repositories.most_pulled'|trans }}

4 |
5 |
6 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/Resources/views/default/most_stared_repositories.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ 'repositories.most_stared'|trans }}

4 |
5 |
6 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/Resources/views/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 | {{ include('layout/navbar.html.twig') }} 5 | 6 |
7 |
8 | {{ include('layout/notifications.html.twig') }} 9 | 10 | {% block content %} 11 | {% block fos_user_content %}{% endblock %} 12 | {% endblock %} 13 |
14 |
15 | {% endblock %} 16 | 17 | {% block stylesheets %} 18 | 19 | {% endblock %} 20 | 21 | {% block javascripts %} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/Resources/views/layout/navbar.html.twig: -------------------------------------------------------------------------------- 1 | 72 | -------------------------------------------------------------------------------- /app/Resources/views/layout/notifications.html.twig: -------------------------------------------------------------------------------- 1 | {% for message in app.session.flashbag.get('error') %} 2 |
3 | 4 | {{ message|trans }} 5 |
6 | {% endfor %} 7 | 8 | {% for message in app.session.flashbag.get('success') %} 9 |
10 | 11 | {{ message|trans }} 12 |
13 | {% endfor %} 14 | 15 | {% for message in app.session.flashbag.get('info') %} 16 |
17 | 18 | {{ message|trans }} 19 |
20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /app/Resources/views/macros.html.twig: -------------------------------------------------------------------------------- 1 | {% macro repository_status(repository) %} 2 | {% if repository.private %} 3 | 4 | {{ 'repository.private'|trans }} 5 | 6 | 7 | {% else %} 8 | 9 | {{ 'repository.public'|trans }} 10 | 11 | 12 | {% endif %} 13 | {% endmacro %} 14 | 15 | {% macro repository_star(repository) %} 16 | {% set data_url_star = path('repository_star', {'name': repository.name, 'action': 'star' }) %} 17 | {% set data_url_unstar = path('repository_star', {'name': repository.name, 'action': 'unstar' }) %} 18 | 19 | 20 | 21 | {% endmacro %} 22 | 23 | {% macro repository_pull_count(repository) %} 24 | 25 | {{ repository.pulls }} 26 | 27 | {% endmacro %} 28 | 29 | {% macro repository_star_count(repository) %} 30 | 31 | {{ repository.stars }} 32 | 33 | {% endmacro %} 34 | -------------------------------------------------------------------------------- /app/Resources/views/repository/_edit.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | {{ form_widget(form) }} 3 | 4 | 5 | {{ form_end(form) }} 6 | -------------------------------------------------------------------------------- /app/Resources/views/repository/_view.html.twig: -------------------------------------------------------------------------------- 1 |

{{ 'repository.title'|trans }}

2 | 3 |
4 | {{ repository.title|default(repository.name) }} 5 |
6 | 7 |

{{ 'repository.description'|trans }}

8 | 9 |
10 | {{ repository.description|default('-') }} 11 |
12 | -------------------------------------------------------------------------------- /app/Resources/views/repository/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'repository/layout.html.twig' %} 2 | 3 | {% set active_tab = 'description' %} 4 | 5 | {% block tab_content %} 6 |
7 | {% if is_granted('REPO_WRITE', repository) %} 8 | {{ include('repository/_edit.html.twig') }} 9 | {% else %} 10 | {{ include('repository/_view.html.twig') }} 11 | {% endif %} 12 |
13 |
14 |

{{ 'repository.pull'|trans }}

15 | 16 |
17 | 18 |
19 | 20 |

{{ 'repository.tags'|trans }}

21 | 22 | 32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /app/Resources/views/repository/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% import 'macros.html.twig' as macros %} 4 | 5 | {% block content %} 6 | 7 | {{ macros.repository_status(repository) }} 8 | 9 | 10 | 19 | 20 |
21 | 27 |
28 | 29 |
30 |
31 | {% block tab_content %}{% endblock %} 32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /app/Resources/views/repository/webhooks.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'repository/layout.html.twig' %} 2 | 3 | {% set active_tab = 'webhooks' %} 4 | 5 | {% import 'DatathekePagerBundle:Pager:bootstrap3.html.twig' as helper %} 6 | 7 | {% block tab_content %} 8 |

{{ 'webhook.list'|trans }}

9 | 10 |
11 | {{ helper.toolbar(pager) }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for webhook in pager.items %} 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | {% endfor %} 39 | 40 |
{{ helper.orderBy(pager, 'name', 'webhook.name'|trans) }}{{ helper.orderBy(pager, 'url', 'webhook.url'|trans) }}{{ helper.orderBy(pager, 'lastCall', 'webhook.last_call'|trans) }}{{ helper.orderBy(pager, 'lastStatus', 'webhook.last_status'|trans) }}{{ 'delete'|trans }}
{{ webhook.name }}{{ webhook.url }} 29 | {% if webhook.lastCall %} 30 | {{ webhook.lastCall|date|default('-') }} 31 | {% else %} 32 | - 33 | {% endif %} 34 | {{ webhook.lastStatus|default('-') }}
41 | 42 | {{ helper.paginate(pager) }} 43 |
44 | 45 |

{{ 'webhook.add'|trans }}

46 | 47 | {{ form_start(form) }} 48 | {{ form_widget(form) }} 49 | 50 | {{ form_end(form) }} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /app/Resources/views/search/results.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% import 'DatathekePagerBundle:Pager:bootstrap3.html.twig' as helper %} 4 | {% import 'macros.html.twig' as macros %} 5 | 6 | {% block content %} 7 |

8 | {{ 'search.results_found'|transchoice(pager.totalItemCount, { '%count%': pager.totalItemCount, '%keyword%': keyword }) }} 9 |

10 | 11 | {% if pager.totalItemCount %} 12 |
13 | {{ helper.toolbar(pager) }} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for repo in pager.items %} 26 | 27 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
{{ helper.orderBy(pager, 'name', 'repository.name'|trans) }}{{ helper.orderBy(pager, 'stars', 'repository.stars'|trans) }}{{ helper.orderBy(pager, 'pulls', 'repository.pulls'|trans) }}{{ helper.orderBy(pager, 'private', 'repository.is_private'|trans) }}
28 | {{ repo.name }} 29 |
{{ repo.data.title }}
30 |
{{ repo.data.stars }} {{ repo.data.pulls }} {{ macros.repository_status(repo) }}
38 | 39 | {{ helper.paginate(pager) }} 40 |
41 | {% endif %} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /app/autoload.php: -------------------------------------------------------------------------------- 1 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev'); 21 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod'; 22 | 23 | if ($debug) { 24 | Debug::enable(); 25 | } 26 | 27 | $kernel = new AppKernel($env, $debug); 28 | $application = new Application($kernel); 29 | $application->run($input); 30 | -------------------------------------------------------------------------------- /bin/run-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | export SYMFONY_ENV=test 5 | 6 | if [ ! -f var/jwt/private.pem ]; then 7 | cp var/jwt/private.pem.dist var/jwt/private.pem 8 | cp var/jwt/public.pem.dist var/jwt/public.pem 9 | fi 10 | 11 | rm -rf var/cache/test 12 | bin/console doctrine:database:create --no-interaction --if-not-exists 13 | bin/console doctrine:schema:drop --no-interaction --full-database --force 14 | bin/console doctrine:migrations:migrate --no-interaction 15 | 16 | bin/behat $@ 17 | -------------------------------------------------------------------------------- /bin/symfony_requirements: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getPhpIniConfigPath(); 9 | 10 | echo_title('Symfony Requirements Checker'); 11 | 12 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 13 | if ($iniPath) { 14 | echo_style('green', ' '.$iniPath); 15 | } else { 16 | echo_style('warning', ' WARNING: No configuration file (php.ini) used by PHP!'); 17 | } 18 | 19 | echo PHP_EOL.PHP_EOL; 20 | 21 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 22 | 23 | $messages = array(); 24 | foreach ($symfonyRequirements->getRequirements() as $req) { 25 | /** @var $req Requirement */ 26 | if ($helpText = get_error_message($req, $lineSize)) { 27 | echo_style('red', 'E'); 28 | $messages['error'][] = $helpText; 29 | } else { 30 | echo_style('green', '.'); 31 | } 32 | } 33 | 34 | $checkPassed = empty($messages['error']); 35 | 36 | foreach ($symfonyRequirements->getRecommendations() as $req) { 37 | if ($helpText = get_error_message($req, $lineSize)) { 38 | echo_style('yellow', 'W'); 39 | $messages['warning'][] = $helpText; 40 | } else { 41 | echo_style('green', '.'); 42 | } 43 | } 44 | 45 | if ($checkPassed) { 46 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects'); 47 | } else { 48 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects'); 49 | 50 | echo_title('Fix the following mandatory requirements', 'red'); 51 | 52 | foreach ($messages['error'] as $helpText) { 53 | echo ' * '.$helpText.PHP_EOL; 54 | } 55 | } 56 | 57 | if (!empty($messages['warning'])) { 58 | echo_title('Optional recommendations to improve your setup', 'yellow'); 59 | 60 | foreach ($messages['warning'] as $helpText) { 61 | echo ' * '.$helpText.PHP_EOL; 62 | } 63 | } 64 | 65 | echo PHP_EOL; 66 | echo_style('title', 'Note'); 67 | echo ' The command console could use a different php.ini file'.PHP_EOL; 68 | echo_style('title', '~~~~'); 69 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 70 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 71 | echo ' server using the '; 72 | echo_style('yellow', 'web/config.php'); 73 | echo ' script.'.PHP_EOL; 74 | echo PHP_EOL; 75 | 76 | exit($checkPassed ? 0 : 1); 77 | 78 | function get_error_message(Requirement $requirement, $lineSize) 79 | { 80 | if ($requirement->isFulfilled()) { 81 | return; 82 | } 83 | 84 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 85 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 86 | 87 | return $errorMessage; 88 | } 89 | 90 | function echo_title($title, $style = null) 91 | { 92 | $style = $style ?: 'title'; 93 | 94 | echo PHP_EOL; 95 | echo_style($style, $title.PHP_EOL); 96 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 97 | echo PHP_EOL; 98 | } 99 | 100 | function echo_style($style, $message) 101 | { 102 | // ANSI color codes 103 | $styles = array( 104 | 'reset' => "\033[0m", 105 | 'red' => "\033[31m", 106 | 'green' => "\033[32m", 107 | 'yellow' => "\033[33m", 108 | 'error' => "\033[37;41m", 109 | 'success' => "\033[37;42m", 110 | 'title' => "\033[34m", 111 | ); 112 | $supports = has_color_support(); 113 | 114 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 115 | } 116 | 117 | function echo_block($style, $title, $message) 118 | { 119 | $message = ' '.trim($message).' '; 120 | $width = strlen($message); 121 | 122 | echo PHP_EOL.PHP_EOL; 123 | 124 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT).PHP_EOL); 126 | echo_style($style, str_pad($message, $width, ' ', STR_PAD_RIGHT).PHP_EOL); 127 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 128 | } 129 | 130 | function has_color_support() 131 | { 132 | static $support; 133 | 134 | if (null === $support) { 135 | if (DIRECTORY_SEPARATOR == '\\') { 136 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 137 | } else { 138 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 139 | } 140 | } 141 | 142 | return $support; 143 | } 144 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iamluc/dunkerque", 3 | "dependencies": { 4 | "bootstrap-sass-official": "~3.3.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iamluc/dunkerque", 3 | "license": "MIT", 4 | "type": "project", 5 | "description": "Docker hub & registry", 6 | "keywords": ["docker", "hub", "registry"], 7 | "authors": [ 8 | { 9 | "name": "Luc Vieillescazes", 10 | "email": "luc@vieillescazes.net" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { "": "src/" }, 15 | "classmap": [ "app/AppKernel.php", "app/AppCache.php" ] 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { "Tests\\": "tests/" } 19 | }, 20 | "repositories": [ 21 | { 22 | "type": "vcs", 23 | "url": "https://github.com/iamluc/rabbit-mq-admin-toolkit" 24 | } 25 | ], 26 | "require": { 27 | "php": ">=5.5.9", 28 | "symfony/symfony": "3.0.*", 29 | "doctrine/orm": "^2.5", 30 | "doctrine/doctrine-bundle": "^1.6", 31 | "doctrine/doctrine-cache-bundle": "^1.2", 32 | "symfony/swiftmailer-bundle": "^2.3", 33 | "symfony/monolog-bundle": "^2.8", 34 | "sensio/distribution-bundle": "^5.0", 35 | "sensio/framework-extra-bundle": "^3.0.2", 36 | "incenteev/composer-parameter-handler": "^2.0", 37 | "ramsey/uuid": "^2.8", 38 | "friendsofsymfony/user-bundle": "~2.0@dev", 39 | "friendsofsymfony/elastica-bundle": "dev-master", 40 | "datatheke/pager-bundle": "^0.5.2", 41 | "swarrot/swarrot-bundle": "^1.3", 42 | "odolbeau/rabbit-mq-admin-toolkit": "dev-symfony3", 43 | "php-amqplib/php-amqplib": "^2.6", 44 | "doctrine/doctrine-migrations-bundle": "^1.1", 45 | "lexik/jwt-authentication-bundle": "^1.3", 46 | "oneup/flysystem-bundle": "^1.2" 47 | }, 48 | "require-dev": { 49 | "sensio/generator-bundle": "^3.0", 50 | "symfony/phpunit-bridge": "^3.0", 51 | "behat/behat": "~3.1@dev", 52 | "behat/symfony2-extension": "^2.0", 53 | "behat/mink-extension": "^2.0", 54 | "behat/mink-browserkit-driver": "^1.2", 55 | "knplabs/friendly-contexts": "^0.7", 56 | "behatch/contexts": "dev-master", 57 | "behat/mink-goutte-driver": "^1.2" 58 | }, 59 | "scripts": { 60 | "post-root-package-install": [ 61 | "SymfonyStandard\\Composer::hookRootPackageInstall" 62 | ], 63 | "post-install-cmd": [ 64 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 65 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 66 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 67 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 68 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 69 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 70 | ], 71 | "post-update-cmd": [ 72 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 73 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 74 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 75 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 76 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 77 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 78 | ] 79 | }, 80 | "config": { 81 | "bin-dir": "bin" 82 | }, 83 | "extra": { 84 | "symfony-app-dir": "app", 85 | "symfony-bin-dir": "bin", 86 | "symfony-var-dir": "var", 87 | "symfony-web-dir": "web", 88 | "symfony-tests-dir": "tests", 89 | "symfony-assets-install": "relative", 90 | "incenteev-parameters": { 91 | "file": "app/config/parameters.yml", 92 | "env-map": { 93 | "storage_path": "DK_STORAGE_PATH", 94 | "secret": "DK_SECRET", 95 | "database_host": "DK_DATABASE_HOST", 96 | "database_port": "DK_DATABASE_PORT", 97 | "database_name": "DK_DATABASE_NAME", 98 | "database_user": "DK_DATABASE_USER", 99 | "database_password": "DK_DATABASE_PASSWORD", 100 | "rabbitmq_host": "DK_RABBITMQ_HOST", 101 | "rabbitmq_port": "DK_RABBITMQ_PORT", 102 | "rabbitmq_login": "DK_RABBITMQ_LOGIN", 103 | "rabbitmq_password": "DK_RABBITMQ_PASSWORD", 104 | "jwt_key_pass_phrase": "DK_JWT_KEY_PASS_PHRASE", 105 | "trusted_proxies": "DK_TRUSTED_PROXIES", 106 | "elasticsearch_host": "DK_ELASTICSEARCH_HOST", 107 | "elasticsearch_port": "DK_ELASTICSEARCH_PORT" 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | app: 2 | image: iamluc/symfony 3 | container_name: dunkerque 4 | volumes: 5 | - .:/var/www/html 6 | # To speed up composer 7 | # - ~/.composer:/var/www/.composer 8 | 9 | environment: 10 | - DOCKER_ENV=dev 11 | ports: 12 | # Adapt to you need 13 | - 8000:80 14 | 15 | links: 16 | - mariadb:db 17 | - rabbitmq:rabbitmq 18 | - elasticsearch:elasticsearch 19 | 20 | # Uncomment lines below to enable Blackfire 21 | # You have to export env variables (BLACKFIRE_SERVER_ID and BLACKFIRE_SERVER_TOKEN) before running docker-compose 22 | # - blackfire:blackfire 23 | # 24 | #blackfire: 25 | # image: blackfire/blackfire 26 | # environment: 27 | # - BLACKFIRE_SERVER_ID 28 | # - BLACKFIRE_SERVER_TOKEN 29 | 30 | 31 | phpmyadmin: 32 | image: phpmyadmin/phpmyadmin 33 | container_name: dunkerque_phpmyadmin 34 | links: 35 | - mariadb:db 36 | 37 | mariadb: 38 | image: mariadb:10 39 | environment: 40 | - MYSQL_ROOT_PASSWORD=dkpassword 41 | 42 | rabbitmq: 43 | image: rabbitmq:3-management 44 | container_name: dunkerque_rabbitmq 45 | environment: 46 | - RABBITMQ_DEFAULT_USER=dunkerque 47 | - RABBITMQ_DEFAULT_PASS=dkpassword 48 | ports: 49 | - 15672:15672 50 | 51 | workerwebhook: 52 | image: iamluc/symfony 53 | volumes_from: 54 | - app 55 | links: 56 | - rabbitmq:rabbitmq 57 | - mariadb:db 58 | environment: 59 | - SYMFONY_ENV=dev 60 | command: sleep 5 && bin/console dunkerque:broker:setup && bin/console swarrot:consume:webhook 61 | 62 | elasticsearch: 63 | image: elasticsearch:1 64 | container_name: dunkerque_elasticsearch 65 | ports: 66 | - 9200:9200 67 | -------------------------------------------------------------------------------- /docker/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM iamluc/symfony 2 | 3 | MAINTAINER Luc Vieillescazes 4 | 5 | RUN rm index.html \ 6 | && wget -O - https://github.com/iamluc/dunkerque/archive/master.tar.gz | tar xz --strip 1 \ 7 | && composer install --no-interaction --no-dev --no-scripts \ 8 | && rm -Rf var/cache/* var/logs/* .composer web/app_dev.php web/config.php \ 9 | && mkdir /data \ 10 | && chown www-data:www-data var/cache var/logs /data 11 | 12 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 13 | 14 | VOLUME ["/data"] 15 | 16 | ENV SYMFONY_ENV prod 17 | ENV DK_STORAGE_PATH /data 18 | ENV DK_JWT_KEY_PASS_PHRASE DunkerqueIsOnFire 19 | -------------------------------------------------------------------------------- /docker/build/Dockerfile-from-source: -------------------------------------------------------------------------------- 1 | FROM iamluc/symfony 2 | 3 | MAINTAINER Luc Vieillescazes 4 | 5 | COPY . ./ 6 | 7 | RUN rm index.html \ 8 | && composer install --no-interaction --no-dev --no-scripts \ 9 | && rm -Rf var/cache/* var/logs/* .composer web/app_dev.php web/config.php \ 10 | && mkdir /data \ 11 | && chown www-data:www-data var/cache var/logs /data 12 | 13 | COPY docker/build/entrypoint.sh /usr/local/bin/entrypoint.sh 14 | 15 | VOLUME ["/data"] 16 | 17 | ENV SYMFONY_ENV prod 18 | ENV DK_STORAGE_PATH /data 19 | ENV DK_JWT_KEY_PASS_PHRASE DunkerqueIsOnFire 20 | -------------------------------------------------------------------------------- /docker/build/README-short.md: -------------------------------------------------------------------------------- 1 | Docker hub & registry written in PHP with Symfony. 2 | -------------------------------------------------------------------------------- /docker/build/README.md: -------------------------------------------------------------------------------- 1 | ### About 2 | 3 | Docker hub & registry written in PHP with Symfony. 4 | 5 | Souce code: [https://github.com/iamluc/dunkerque](https://github.com/iamluc/dunkerque) 6 | Docker image: [https://hub.docker.com/r/iamluc/dunkerque/](https://hub.docker.com/r/iamluc/dunkerque/) 7 | 8 | ### Run dunkerque 9 | 10 | Run the containers stack with a similar `docker-compose.yml` file: 11 | 12 | ```yml 13 | app: 14 | image: iamluc/dunkerque 15 | ports: 16 | - 80:80 17 | links: 18 | - mariadb:db 19 | - rabbitmq:rabbitmq 20 | - elasticsearch:elasticsearch 21 | 22 | mariadb: 23 | image: mariadb:10 24 | environment: 25 | - MYSQL_ROOT_PASSWORD=dkpassword 26 | 27 | elasticsearch: 28 | image: elasticsearch:1 29 | 30 | rabbitmq: 31 | image: rabbitmq:3-management 32 | environment: 33 | - RABBITMQ_DEFAULT_USER=dunkerque 34 | - RABBITMQ_DEFAULT_PASS=dkpassword 35 | 36 | workerwebhook: 37 | image: iamluc/dunkerque 38 | volumes_from: 39 | - app 40 | links: 41 | - mariadb:db 42 | - rabbitmq:rabbitmq 43 | command: sleep 5 && bin/console dunkerque:broker:setup && bin/console swarrot:consume:webhook 44 | ``` 45 | 46 | ### Update 47 | 48 | To update, check that your custom `docker-compose.yml` is up-to-date. 49 | Then run 50 | 51 | ``` 52 | # Download new images 53 | docker-compose pull 54 | 55 | # Recreate containers 56 | docker-compose up -d 57 | 58 | # Repopulate the elasticsearch index (used for the search) 59 | docker-compose run --rm app bin/console fos:elastica:populate 60 | ``` 61 | 62 | ### Use your registry with docker 63 | 64 | Please note that currently the image exposes only port 80. 65 | You must setup a proxy (like [nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy/)) with a certificate to use HTTPS. 66 | Without HTTPS, you must add the `--insecure-registry` option to your daemon configuration. See https://docs.docker.com/registry/insecure/. 67 | 68 | To push an image, you can follow this tutorial: https://www.digitalocean.com/community/tutorials/how-to-set-up-a-private-docker-registry-on-ubuntu-14-04#step-seven-—-publish-to-your-docker-registry 69 | 70 | ### Configure 71 | 72 | You can use environment variables to configure the `iamluc/dunkerque` image: 73 | 74 | | Variable name | Default value | 75 | |-------------------------|---------------------| 76 | | DK_STORAGE_PATH | /data | 77 | -------------------------------------------------------------------------------- /docker/build/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Generate keys needed by JWT 5 | if [ ! -f var/jwt/private.pem ]; then 6 | openssl genrsa -passout env:DK_JWT_KEY_PASS_PHRASE -out var/jwt/private.pem -aes256 4096 7 | openssl rsa -pubout -passin env:DK_JWT_KEY_PASS_PHRASE -in var/jwt/private.pem -out var/jwt/public.pem 8 | fi 9 | 10 | # Init project with environment variables given at runtime 11 | composer run-script post-install-cmd --no-interaction --no-dev 12 | 13 | # Delete cache created by root 14 | rm -rf var/cache/* var/logs/* 15 | 16 | if [ "$1" = 'apache2ctl' ]; then 17 | # Let's time to other containers (i.e. mysql) 18 | sleep 5 19 | 20 | # Warmup cache 21 | su www-data -s /bin/bash -c "bin/console cache:warmup --no-interaction" 22 | 23 | # Setup/update database 24 | su www-data -s /bin/bash -c "bin/console doctrine:database:create --no-interaction --if-not-exists" 25 | su www-data -s /bin/bash -c "bin/console doctrine:migrations:migrate --no-interaction" 26 | 27 | # let's start apache as root 28 | exec "$@" 29 | else 30 | # change to user www-data 31 | su www-data -s /bin/bash -c "$*" 32 | fi 33 | -------------------------------------------------------------------------------- /docker/docker-compose.nodejs.yml: -------------------------------------------------------------------------------- 1 | nodejs: 2 | image: jeanberu/ggbcls 3 | volumes: 4 | - ..:/src 5 | -------------------------------------------------------------------------------- /docker/elasticsearch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elasticsearch:1 2 | 3 | RUN plugin -install mobz/elasticsearch-head 4 | 5 | EXPOSE 9200 6 | EXPOSE 9300 7 | -------------------------------------------------------------------------------- /features/account.feature: -------------------------------------------------------------------------------- 1 | Feature: Test account 2 | 3 | @reset-schema 4 | Scenario: Fake background (for performance) 5 | Given I have users: 6 | | username | password | roles | 7 | | admin | admin | ROLE_ADMIN | 8 | | test | test | ROLE_USER | 9 | And the following repositories: 10 | | name | owner | private | stars | pulls | 11 | | hello-world | admin | false | 3 | 12 | 12 | And I have manifests: 13 | | hello-world:latest.json | 14 | 15 | Scenario: I go to a non-existing account 16 | Given I go to "/u/john" 17 | Then the response status code should be 404 18 | 19 | Scenario: I go to a existing account 20 | Given I go to "/u/admin" 21 | Then the response status code should be 200 22 | And I should see a table with 1 row 23 | And I should see the following table: 24 | | Name | Stars | Pulls | Private | 25 | | hello-world | 3 | 12 | Public | 26 | 27 | Scenario: I am authenticated 28 | Given I am authenticated as "test" 29 | And I go to "/" 30 | Then the response status code should be 200 31 | And I should see "test" 32 | And I should see "Log out" 33 | -------------------------------------------------------------------------------- /features/bootstrap/AuthenticationContext.php: -------------------------------------------------------------------------------- 1 | getEnvironment(); 29 | $this->minkContext = $environment->getContext('Behat\MinkExtension\Context\MinkContext'); 30 | } 31 | 32 | /** 33 | * @Given I am authenticated as :username 34 | */ 35 | public function iAmAuthenticatedAs($username) 36 | { 37 | /** @var \Behat\Mink\Session $session */ 38 | $minkSession = $this->minkContext->getSession(); 39 | 40 | /** @var \Behat\Symfony2Extension\Driver\KernelDriver $driver */ 41 | $driver = $minkSession->getDriver(); 42 | if (!$driver instanceof BrowserKitDriver) { 43 | throw new UnsupportedDriverActionException('This step is only supported by the BrowserKitDriver', $driver); 44 | } 45 | 46 | /** @var \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider */ 47 | $userProvider = $this->getContainer()->get('fos_user.user_provider.username'); 48 | $user = $userProvider->loadUserByUsername($username); 49 | 50 | $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles()); 51 | 52 | $client = $driver->getClient(); 53 | $session = $client->getContainer()->get('session'); 54 | $session->set('_security_main', serialize($token)); 55 | $session->save(); 56 | 57 | $cookie = new Cookie($session->getName(), $session->getId()); 58 | $client->getCookieJar()->set($cookie); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /features/bootstrap/EntityContext.php: -------------------------------------------------------------------------------- 1 | resolveEntity('users')->getName(); 20 | $encoder = $this->getContainer()->get('security.password_encoder'); 21 | 22 | $rows = $table->getRows(); 23 | $headers = array_shift($rows); 24 | 25 | foreach ($rows as $row) { 26 | $values = array_combine($headers, $row); 27 | $entity = new $entityName; 28 | $reflection = new \ReflectionClass($entity); 29 | 30 | // Encode password 31 | $values['password'] = $encoder->encodePassword($entity, $values['password']); 32 | $values['enabled'] = true; 33 | 34 | do { 35 | $this 36 | ->getRecordBag() 37 | ->getCollection($reflection->getName()) 38 | ->attach($entity, $values); 39 | $reflection = $reflection->getParentClass(); 40 | } while (false !== $reflection); 41 | 42 | $this 43 | ->getEntityHydrator() 44 | ->hydrate($this->getEntityManager(), $entity, $values) 45 | ->completeRequired($this->getEntityManager(), $entity); 46 | 47 | $this->getEntityManager()->persist($entity); 48 | } 49 | 50 | $this->getEntityManager()->flush(); 51 | } 52 | 53 | /** 54 | * @Given I have manifests: 55 | */ 56 | public function iHaveManifests(TableNode $table) 57 | { 58 | $manifestsPath = __DIR__.'/../fixtures/manifests/'; 59 | $em = $this->getEntityManager(); 60 | 61 | foreach ($table->getRows() as $row) { 62 | 63 | $content = file_get_contents($manifestsPath.$row[0]); 64 | $repository = $em->getRepository('AppBundle:Repository')->findOneByName(json_decode($content, true)['name']); 65 | 66 | $manifest = new Manifest($repository); 67 | $manifest->setContent($content); 68 | 69 | $em->persist($manifest); 70 | } 71 | 72 | $em->flush(); 73 | } 74 | 75 | /** 76 | * @Given Entities are indexed 77 | */ 78 | public function entitiesAreIndexed() 79 | { 80 | $process = new Process('bin/console fos:elastica:populate'); 81 | $process->run(); 82 | if (!$process->isSuccessful()) { 83 | throw new \RuntimeException('Unable to populate elasticsearch'); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /features/bootstrap/RestContext.php: -------------------------------------------------------------------------------- 1 | request = $request; 19 | 20 | parent::__construct($request); 21 | } 22 | 23 | /** 24 | * @BeforeScenario @fixtures 25 | */ 26 | public function loadFixtures(BeforeScenarioScope $scope) 27 | { 28 | $this->headers = []; 29 | $this->iAddHeaderEqualTo('PHP_AUTH_USER', null); 30 | $this->iAddHeaderEqualTo('PHP_AUTH_PW', null); 31 | } 32 | 33 | /** 34 | * @BeforeScenario 35 | */ 36 | public function beforeScenario(BeforeScenarioScope $scope) 37 | { 38 | $this->variables = []; 39 | } 40 | 41 | /** 42 | * @AfterScenario 43 | */ 44 | public function afterScenario(AfterScenarioScope $scope) 45 | { 46 | foreach ($this->headers as $name) { 47 | $this->iAddHeaderEqualTo($name, null); 48 | } 49 | } 50 | 51 | /** 52 | * @Given I set basic authentication with :username and :password 53 | */ 54 | public function iAmAuthenticatedAs($username, $password) 55 | { 56 | // Does not work ! 57 | // But let it to be future proof 58 | $this->getSession()->setBasicAuth($username, $password); 59 | 60 | // Workaround 61 | $this->iAddHeaderEqualTo('PHP_AUTH_USER', $username); 62 | $this->iAddHeaderEqualTo('PHP_AUTH_PW', $password); 63 | } 64 | 65 | /** 66 | * @Then I set header :name to :value 67 | */ 68 | public function iAddHeaderEqualTo($name, $value) 69 | { 70 | $this->headers[] = $name; 71 | 72 | parent::iAddHeaderEqualTo($name, $value); 73 | } 74 | 75 | /** 76 | * @Then the response should be equal to file :filename 77 | */ 78 | public function theResponseShouldBeEqualToFile($filename) 79 | { 80 | $fixturesPath = __DIR__.'/../fixtures/'; 81 | 82 | $actual = $this->request->getContent(); 83 | $message = "The content of file '$filename' is not equal to the response of the current page"; 84 | $this->assertEquals(file_get_contents($fixturesPath.$filename), $actual, $message); 85 | } 86 | 87 | /** 88 | * @Then I store value of header :header to variable :name 89 | */ 90 | public function iStoreValueOfHeaderToVariable($header, $name) 91 | { 92 | $this->variables[$name] = $this->request->getHttpHeader($header); 93 | } 94 | 95 | /** 96 | * @Override I send a :method request to :url 97 | */ 98 | public function iSendARequestTo($method, $url, PyStringNode $body = null) 99 | { 100 | $vars = array_map(function ($val) {return '{'.$val.'}';}, array_keys($this->variables)); 101 | $url = strtr($url, array_combine($vars, array_values($this->variables))); 102 | 103 | return $this->request->send( 104 | $method, 105 | $this->locatePath($url), 106 | [], 107 | [], 108 | $body !== null ? $body->getRaw() : null 109 | ); 110 | } 111 | 112 | /** 113 | * @Then I send a :method request to :url with body :filename 114 | */ 115 | public function iSendARequestToWithBodyFilename($method, $url, $filename) 116 | { 117 | $fixturesPath = __DIR__.'/../fixtures/'; 118 | 119 | $vars = array_map(function ($val) {return '{'.$val.'}';}, array_keys($this->variables)); 120 | $url = strtr($url, array_combine($vars, array_values($this->variables))); 121 | 122 | return $this->request->send( 123 | $method, 124 | $this->locatePath($url), 125 | [], 126 | [], 127 | file_get_contents($fixturesPath.$filename) 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /features/fixtures/layers/03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/features/fixtures/layers/03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb -------------------------------------------------------------------------------- /features/fixtures/layers/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/features/fixtures/layers/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 -------------------------------------------------------------------------------- /features/fixtures/manifests/hello-world:latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "tag": "latest", 4 | "architecture": "amd64", 5 | "fsLayers": [ 6 | { 7 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 8 | }, 9 | { 10 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 11 | }, 12 | { 13 | "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 14 | } 15 | ], 16 | "history": [ 17 | { 18 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 19 | }, 20 | { 21 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 22 | }, 23 | { 24 | "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" 25 | } 26 | ], 27 | "schemaVersion": 1, 28 | "signatures": [ 29 | { 30 | "header": { 31 | "jwk": { 32 | "crv": "P-256", 33 | "kid": "MPKU:VKH5:HUOL:LSVI:HZ3C:VWZE:MGMT:JWUY:JGOC:4ZNL:PEPS:3WH5", 34 | "kty": "EC", 35 | "x": "nGNhgUR4P65-qIrXj6pIa7dc30ntpJuYS4KIphNf8ks", 36 | "y": "wxFL_4C2wKl603N2W-2GUrvAcarhR0wxwKSXpus21kY" 37 | }, 38 | "alg": "ES256" 39 | }, 40 | "signature": "FbbW1VrVsunBtacEuC2kTku0ivBKE5kkADVT9F7Tw11dEuQUXBhDkKl8XcLArtODP46QbdyPY8NlbDvhGXdN3A", 41 | "protected": "eyJmb3JtYXRMZW5ndGgiOjQ3ODksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0yNFQxMjo1MzoxM1oifQ" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /features/fixtures/manifests/secret-world:latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secret-world", 3 | "tag": "latest", 4 | "architecture": "amd64", 5 | "fsLayers": [ 6 | { 7 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 8 | }, 9 | { 10 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 11 | }, 12 | { 13 | "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 14 | } 15 | ], 16 | "history": [ 17 | { 18 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 19 | }, 20 | { 21 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 22 | }, 23 | { 24 | "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" 25 | } 26 | ], 27 | "schemaVersion": 1, 28 | "signatures": [ 29 | { 30 | "header": { 31 | "jwk": { 32 | "crv": "P-256", 33 | "kid": "MPKU:VKH5:HUOL:LSVI:HZ3C:VWZE:MGMT:JWUY:JGOC:4ZNL:PEPS:3WH5", 34 | "kty": "EC", 35 | "x": "nGNhgUR4P65-qIrXj6pIa7dc30ntpJuYS4KIphNf8ks", 36 | "y": "wxFL_4C2wKl603N2W-2GUrvAcarhR0wxwKSXpus21kY" 37 | }, 38 | "alg": "ES256" 39 | }, 40 | "signature": "FbbW1VrVsunBtacEuC2kTku0ivBKE5kkADVT9F7Tw11dEuQUXBhDkKl8XcLArtODP46QbdyPY8NlbDvhGXdN3A", 41 | "protected": "eyJmb3JtYXRMZW5ndGgiOjQ3ODksImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0yNFQxMjo1MzoxM1oifQ" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /features/fixtures/manifests/test~hello-world:latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test/hello-world", 3 | "tag": "latest", 4 | "architecture": "amd64", 5 | "fsLayers": [ 6 | { 7 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 8 | }, 9 | { 10 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 11 | }, 12 | { 13 | "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 14 | } 15 | ], 16 | "history": [ 17 | { 18 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 19 | }, 20 | { 21 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 22 | }, 23 | { 24 | "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" 25 | } 26 | ], 27 | "schemaVersion": 1, 28 | "signatures": [ 29 | { 30 | "header": { 31 | "jwk": { 32 | "crv": "P-256", 33 | "kid": "MPKU:VKH5:HUOL:LSVI:HZ3C:VWZE:MGMT:JWUY:JGOC:4ZNL:PEPS:3WH5", 34 | "kty": "EC", 35 | "x": "nGNhgUR4P65-qIrXj6pIa7dc30ntpJuYS4KIphNf8ks", 36 | "y": "wxFL_4C2wKl603N2W-2GUrvAcarhR0wxwKSXpus21kY" 37 | }, 38 | "alg": "ES256" 39 | }, 40 | "signature": "mC26itQhWvCFYjBDz4DctL9Pf1YfsoXDgln08tnB2im_hczzbJOSOqQXDnVDY6JyOl_in00zKJCELYOEBTJtEQ", 41 | "protected": "eyJmb3JtYXRMZW5ndGgiOjQ3OTQsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0yNFQxMjo1MjowNloifQ" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /features/fixtures/manifests/test~secret-world:latest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test/secret-world", 3 | "tag": "latest", 4 | "architecture": "amd64", 5 | "fsLayers": [ 6 | { 7 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 8 | }, 9 | { 10 | "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 11 | }, 12 | { 13 | "blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 14 | } 15 | ], 16 | "history": [ 17 | { 18 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 19 | }, 20 | { 21 | "v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" 22 | }, 23 | { 24 | "v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n" 25 | } 26 | ], 27 | "schemaVersion": 1, 28 | "signatures": [ 29 | { 30 | "header": { 31 | "jwk": { 32 | "crv": "P-256", 33 | "kid": "MPKU:VKH5:HUOL:LSVI:HZ3C:VWZE:MGMT:JWUY:JGOC:4ZNL:PEPS:3WH5", 34 | "kty": "EC", 35 | "x": "nGNhgUR4P65-qIrXj6pIa7dc30ntpJuYS4KIphNf8ks", 36 | "y": "wxFL_4C2wKl603N2W-2GUrvAcarhR0wxwKSXpus21kY" 37 | }, 38 | "alg": "ES256" 39 | }, 40 | "signature": "mC26itQhWvCFYjBDz4DctL9Pf1YfsoXDgln08tnB2im_hczzbJOSOqQXDnVDY6JyOl_in00zKJCELYOEBTJtEQ", 41 | "protected": "eyJmb3JtYXRMZW5ndGgiOjQ3OTQsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wOS0yNFQxMjo1MjowNloifQ" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /features/homepage.feature: -------------------------------------------------------------------------------- 1 | Feature: Test homepage 2 | 3 | Scenario: Test homepage is correct 4 | Given I am on the homepage 5 | Then the response status code should be 200 6 | Then I should see "Dunkerque" 7 | 8 | @reset-schema 9 | Scenario: Fake background (for performance) 10 | Given I have users: 11 | | username | password | roles | 12 | | admin | admin | ROLE_ADMIN | 13 | | test | test | ROLE_USER | 14 | And the following repositories: 15 | | name | owner | private | pulls | 16 | | hello-world | admin | false | 9876 | 17 | | secret-world | admin | true | 0 | 18 | | test/hello-world | test | false | 42 | 19 | | test/secret-world | test | true | 654 | 20 | When I am on the homepage 21 | Then I should see "hello-world" 22 | And I should see "Most pulled" 23 | And I should see "9876" 24 | And I should not see "secret-world" 25 | -------------------------------------------------------------------------------- /features/registration.feature: -------------------------------------------------------------------------------- 1 | Feature: Test registration 2 | 3 | Scenario: I can access the registration page 4 | Given I go to "/register/" 5 | Then the response status code should be 200 6 | -------------------------------------------------------------------------------- /features/registry/access_manifest.feature: -------------------------------------------------------------------------------- 1 | Feature: Test access manifests 2 | 3 | @reset-schema 4 | Scenario: Fake background (for performance) 5 | Given I have users: 6 | | username | password | roles | 7 | | admin | admin | ROLE_ADMIN | 8 | | test | test | ROLE_USER | 9 | And the following repositories: 10 | | name | owner | private | 11 | | hello-world | admin | false | 12 | | secret-world | admin | true | 13 | | test/hello-world | test | false | 14 | | test/secret-world | test | true | 15 | And I have manifests: 16 | | hello-world:latest.json | 17 | | secret-world:latest.json | 18 | | test~hello-world:latest.json | 19 | | test~secret-world:latest.json | 20 | 21 | Scenario: As an anonymous user, I cannot access a private manifest 22 | Given I send a "GET" request to "/v2/test/secret-world/manifests/latest" 23 | Then the response status code should be 401 24 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 25 | 26 | Scenario: As an anonymous user, I can access a public manifest 27 | Given I send a "GET" request to "/v2/test/hello-world/manifests/latest" 28 | Then the response status code should be 200 29 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 30 | 31 | @registry2 32 | Scenario: As a simple user, access unknown manifest from my namespace 33 | Given I set basic authentication with "test" and "test" 34 | When I send a "GET" request to "/v2/test/goodbye-world/manifests/latest" 35 | Then the response status code should be 404 36 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 37 | 38 | Scenario: As a simple user, access public manifest from my namespace 39 | Given I set basic authentication with "test" and "test" 40 | When I send a "GET" request to "/v2/test/hello-world/manifests/latest" 41 | Then the response status code should be 200 42 | And the response should be equal to file "manifests/test~hello-world:latest.json" 43 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 44 | And the header "Content-Type" should contain "application/json" 45 | 46 | Scenario: As a simple user, access private manifest from my namespace 47 | Given I set basic authentication with "test" and "test" 48 | When I send a "GET" request to "/v2/test/secret-world/manifests/latest" 49 | Then the response status code should be 200 50 | And the response should be equal to file "manifests/test~secret-world:latest.json" 51 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 52 | And the header "Content-Type" should contain "application/json" 53 | 54 | Scenario: As a simple user, access private manifest from the root namespace 55 | Given I set basic authentication with "test" and "test" 56 | When I send a "GET" request to "/v2/secret-world/manifests/latest" 57 | Then the response status code should be 403 58 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 59 | 60 | Scenario: As a simple user, access public manifest from the root namespace 61 | Given I set basic authentication with "test" and "test" 62 | When I send a "GET" request to "/v2/hello-world/manifests/latest" 63 | Then the response status code should be 200 64 | And the response should be equal to file "manifests/hello-world:latest.json" 65 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 66 | 67 | Scenario: As an admin, access private manifest from the root namespace 68 | Given I set basic authentication with "admin" and "admin" 69 | When I send a "GET" request to "/v2/hello-world/manifests/latest" 70 | Then the response status code should be 200 71 | And the response should be equal to file "manifests/hello-world:latest.json" 72 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 73 | -------------------------------------------------------------------------------- /features/registry/token.feature: -------------------------------------------------------------------------------- 1 | Feature: Test token endpoint 2 | 3 | Scenario: As an anonymous user, I can access token endpoint 4 | Given I send a "GET" request to "/token" 5 | Then the response status code should be 200 6 | And the JSON node "token" should exist 7 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 8 | -------------------------------------------------------------------------------- /features/registry/upload_manifest.feature: -------------------------------------------------------------------------------- 1 | Feature: Test add manifest 2 | 3 | @reset-schema 4 | Scenario: Fake background (for performance) 5 | Given I have users: 6 | | username | password | roles | 7 | | test | test | ROLE_USER | 8 | 9 | Scenario: As an anonymous user, I cannot upload a manifest 10 | Given I send a "POST" request to "/v2/test/hello-world/blobs/uploads/" 11 | Then the response status code should be 401 12 | 13 | @registry2 14 | Scenario: As a simple user, upload layers and add a manifest 15 | Given I set basic authentication with "test" and "test" 16 | 17 | When I send a "POST" request to "/v2/test/hello-world/blobs/uploads/" 18 | Then the response status code should be 202 19 | And the header "Docker-Upload-UUID" should contain "-" 20 | And I store value of header "Location" to variable "location" 21 | 22 | When I send a "PUT" request to "{location}&digest=sha256%3Aa3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" with body "layers/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 23 | Then the response status code should be 201 24 | And the header "Location" should contain "/v2/test/hello-world/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 25 | And the header "Docker-Content-Digest" should be equal to "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 26 | 27 | When I send a "HEAD" request to "/v2/test/hello-world/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 28 | Then the response status code should be 200 29 | 30 | When I send a "POST" request to "/v2/test/hello-world/blobs/uploads/" 31 | Then the response status code should be 202 32 | And the header "Docker-Upload-UUID" should contain "-" 33 | And I store value of header "Location" to variable "location" 34 | 35 | When I send a "PUT" request to "{location}&digest=sha256%3A03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" with body "layers/03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 36 | Then the response status code should be 201 37 | And the header "Location" should contain "/v2/test/hello-world/blobs/sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 38 | And the header "Docker-Content-Digest" should be equal to "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 39 | 40 | When I send a "PUT" request to "/v2/test/hello-world/manifests/latest" with body "manifests/test~hello-world:latest.json" 41 | Then the response status code should be 201 42 | And the header "Location" should contain "/v2/test/hello-world/manifests/sha256:9956e7d769a4cfeba2e342b92adf58e403affd8a77ef0710c4fb01e948fc2bbe" 43 | And the header "Docker-Content-Digest" should be equal to "sha256:9956e7d769a4cfeba2e342b92adf58e403affd8a77ef0710c4fb01e948fc2bbe" 44 | 45 | @registry2 46 | Scenario: As a simple user, upload layer with PATCH 47 | Given I set basic authentication with "test" and "test" 48 | 49 | When I send a "POST" request to "/v2/test/hello-world/blobs/uploads/" 50 | Then the response status code should be 202 51 | And the header "Docker-Upload-UUID" should contain "-" 52 | And I store value of header "Location" to variable "location" 53 | 54 | When I send a "PATCH" request to "{location}" with body "layers/03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 55 | Then the response status code should be 202 56 | And the header "Range" should contain "0-600" 57 | 58 | When I send a "PUT" request to "{location}&digest=sha256%3A03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 59 | Then the response status code should be 201 60 | And the header "Location" should contain "/v2/test/hello-world/blobs/sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 61 | And the header "Docker-Content-Digest" should be equal to "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb" 62 | -------------------------------------------------------------------------------- /features/registry/version.feature: -------------------------------------------------------------------------------- 1 | Feature: Test version endpoint 2 | 3 | Scenario: As an anonymous user, I cannot access version endpoint 4 | Given I send a "GET" request to "/v2/" 5 | Then the response status code should be 401 6 | And the response should be in JSON 7 | And the JSON node "errors[0].code" should be equal to "UNAUTHORIZED" 8 | And the JSON node "errors[0].message" should be equal to "access to the requested resource is not authorized" 9 | And the header "WWW-Authenticate" should contain 'Bearer realm="http' 10 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 11 | 12 | @reset-schema 13 | Scenario: As a user with valid credentials, I can access version endpoint 14 | Given I have users: 15 | | username | password | 16 | | test | test | 17 | When I set basic authentication with "test" and "test" 18 | And I send a "GET" request to "/v2/" 19 | Then the response status code should be 200 20 | And the response should be in JSON 21 | And the JSON should be equal to: 22 | """ 23 | {} 24 | """ 25 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 26 | 27 | Scenario: As a user with invalid credentials, I cannot access version endpoint 28 | Given I set basic authentication with "test_KO" and "test1234" 29 | And I send a "GET" request to "/v2/" 30 | Then the response status code should be 401 31 | And the response should be in JSON 32 | And the JSON node "errors[0].code" should be equal to "UNAUTHORIZED" 33 | And the JSON node "errors[0].message" should be equal to "access to the requested resource is not authorized" 34 | And the header "WWW-Authenticate" should contain "Bearer" 35 | And the header "Docker-Distribution-Api-Version" should contain "registry/2.0" 36 | -------------------------------------------------------------------------------- /features/repository_description.feature: -------------------------------------------------------------------------------- 1 | Feature: Test repository description page 2 | 3 | @reset-schema 4 | Scenario: Fake background (for performance) 5 | Given I have users: 6 | | username | password | roles | 7 | | admin | admin | ROLE_ADMIN | 8 | | test | test | ROLE_USER | 9 | And the following repositories: 10 | | name | owner | private | stars | pulls | description | 11 | | hello-world | admin | true | 3 | 12 | | 12 | | dummy | admin | false | 0 | 0 | Dunkerque dummy image | 13 | | dunkerque | admin | false | 1000 | 234 | Dunkerque official image | 14 | And the following repositoryStars: 15 | | user | repository | 16 | | test | dunkerque | 17 | | admin | dunkerque | 18 | And I have manifests: 19 | | hello-world:latest.json | 20 | 21 | Scenario: I go to a non-existing repository 22 | Given I go to "/r/admin/docker" 23 | Then the response status code should be 404 24 | 25 | Scenario: As an anonymous user, I go to a private repository 26 | Given I go to "/r/hello-world" 27 | Then the response status code should be 200 28 | And I should be on "/login" 29 | 30 | Scenario: As an anonymous user, I go to a public repository 31 | Given I go to "/r/dunkerque" 32 | Then the response status code should be 200 33 | And I should see "Dunkerque official image" 34 | And I should not see an "a#repository-star" element 35 | 36 | Scenario: As an authenticated user, I go to a public repository 37 | Given I am authenticated as "test" 38 | And I go to "/r/dunkerque" 39 | Then I should see an "a#repository-star" element 40 | And I should see an "span.glyphicon.glyphicon-star" element 41 | Given I go to "/r/dummy" 42 | Then I should see an "a#repository-star" element 43 | And I should see an "span.glyphicon.glyphicon-star-empty" element 44 | 45 | Scenario: As an authenticated user, I go to a private repository 46 | Given I am authenticated as "test" 47 | And I go to "/r/hello-world" 48 | Then the response status code should be 403 49 | -------------------------------------------------------------------------------- /features/search.feature: -------------------------------------------------------------------------------- 1 | Feature: Test search 2 | 3 | @reset-schema 4 | Scenario: Fake background (for performance) 5 | Given I have users: 6 | | username | password | roles | 7 | | admin | admin | ROLE_ADMIN | 8 | | test | test | ROLE_USER | 9 | And the following repositories: 10 | | name | owner | private | stars | pulls | description | 11 | | not_found | admin | false | 3 | 12 | Not found | 12 | | admin_public_1 | admin | false | 3 | 12 | Dunkerque official image | 13 | | admin_public_2 | admin | false | 0 | 0 | Dunkerque almost official image | 14 | | admin_private_1 | admin | true | 1000 | 234 | Dunkerque | 15 | | test_private_1 | test | true | 1000 | 234 | Dunkerque | 16 | And Entities are indexed 17 | 18 | Scenario: I search a non-existing repository 19 | Given I go to "/" 20 | And I fill in "search_keyword" with "unknown" 21 | And I press "search_submit" 22 | Then the response status code should be 200 23 | And I should see "No result found for \"unknown\"" 24 | And I should not see an "table" element 25 | 26 | Scenario: I search public repositories 27 | Given I go to "/" 28 | And I fill in "search_keyword" with "Dunkerque" 29 | And I press "search_submit" 30 | Then the response status code should be 200 31 | And I should see "2 results found for \"Dunkerque\"" 32 | And I should see the following table: 33 | | Name | Stars | Pulls | Private | 34 | | admin_public_1 | 3 | 12 | Public | 35 | | admin_public_2 | 0 | 0 | Public | 36 | 37 | Scenario: As a user, I search public and private repositories 38 | Given I am authenticated as "test" 39 | And I go to "/" 40 | And I fill in "search_keyword" with "Dunkerque" 41 | And I press "search_submit" 42 | Then the response status code should be 200 43 | And I should see "3 results found for \"Dunkerque\"" 44 | And I should see the following table: 45 | | Name | Stars | Pulls | Private | 46 | | test_private_1 | 1000 | 234 | Private | 47 | | admin_public_1 | 3 | 12 | Public | 48 | | admin_public_2 | 0 | 0 | Public | 49 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | concat = require('gulp-concat'), 3 | sass = require('gulp-ruby-sass'), 4 | uglify = require('gulp-uglify'); 5 | 6 | /** 7 | * Config 8 | */ 9 | var config = { 10 | bowerDir: './bower_components', 11 | assetsSrc: './app/Resources/assets', 12 | assetsDest: './web/assets' 13 | }; 14 | 15 | config.fonts = { 16 | src: config.bowerDir + '/bootstrap-sass-official/assets/fonts/bootstrap/*', 17 | dest: config.assetsDest + '/fonts/bootstrap' 18 | }; 19 | 20 | config.sass = { 21 | src: config.assetsSrc + '/scss/main.scss', 22 | dest: config.assetsDest + '/css', 23 | loadPath: [ 24 | config.bowerDir + '/bootstrap-sass-official/assets/stylesheets' 25 | ] 26 | }; 27 | 28 | config.js = { 29 | src: [ 30 | config.bowerDir + '/jquery/dist/jquery.js', 31 | config.bowerDir + '/bootstrap-sass-official/assets/javascripts/bootstrap.js', 32 | config.assetsSrc + '/js/*.js' 33 | ], 34 | dest: config.assetsDest + '/js' 35 | }; 36 | 37 | /** 38 | * Tasks 39 | */ 40 | gulp.task('fonts', function() { 41 | return gulp 42 | .src(config.fonts.src) 43 | .pipe(gulp.dest(config.fonts.dest)); 44 | }); 45 | 46 | gulp.task('css', function() { 47 | return sass(config.sass.src, { 48 | style: 'compressed', 49 | loadPath: config.sass.loadPath 50 | }) 51 | .on('error', sass.logError) 52 | .pipe(gulp.dest(config.sass.dest)); 53 | }); 54 | 55 | gulp.task('js', function () { 56 | return gulp 57 | .src(config.js.src) 58 | .pipe(concat('main.js')) 59 | .pipe(uglify()) 60 | .pipe(gulp.dest(config.js.dest)); 61 | }); 62 | 63 | // Rerun the task when a file changes 64 | gulp.task('watch', function() { 65 | gulp.watch(config.assetsSrc + '/scss/*.scss', ['css']); 66 | gulp.watch(config.assetsSrc + '/js/*.js', ['js']); 67 | }); 68 | 69 | gulp.task('default', ['fonts', 'css', 'js']); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "repository": { 4 | "type" : "git", 5 | "url" : "https://github.com/iamluc/dunkerque.git" 6 | }, 7 | "devDependencies": { 8 | "bower": "^1.7.2", 9 | "gulp": "^3.9.0", 10 | "gulp-concat": "^2.6.0", 11 | "gulp-ruby-sass": "^2.0.3", 12 | "gulp-uglify": "^1.4.1" 13 | }, 14 | "scripts": { 15 | "install": "bower install" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | src/*Bundle/Resources 26 | src/*/*Bundle/Resources 27 | src/*/Bundle/*Bundle/Resources 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/AppBundle/AppBundle.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 21 | } 22 | 23 | public function process(Message $message, array $options) 24 | { 25 | $data = json_decode($message->getBody(), true); 26 | 27 | $manifest = $this->manager->getRepository('AppBundle:Manifest')->find($data['id']); 28 | $webhooks = $this->manager->getRepository('AppBundle:Webhook')->findByRepository($manifest->getRepository()); 29 | 30 | $payload = $this->getPayload($manifest); 31 | 32 | /** @var Webhook $webhook */ 33 | foreach ($webhooks as $webhook) { 34 | $options = [ 35 | 'http' => [ 36 | 'method' => 'POST', 37 | 'header' => "Content-Type: application/json\r\n", 38 | 'content' => $payload, 39 | 'timeout' => 2, 40 | 'ignore_errors' => true, 41 | ], 42 | ]; 43 | $context = stream_context_create($options); 44 | $response = @file_get_contents($webhook->getUrl(), null, $context); 45 | if (isset($http_response_header) && is_array($http_response_header)) { 46 | $status = $http_response_header[0]; 47 | } else { 48 | $status = 'Error'; 49 | } 50 | unset($http_response_header); 51 | 52 | $webhook->setLastStatus($status); 53 | $webhook->setLastCall(new \DateTime()); 54 | } 55 | 56 | $this->manager->flush(); 57 | } 58 | 59 | private function getPayload(Manifest $manifest) 60 | { 61 | $repository = $manifest->getRepository(); 62 | 63 | $data = [ 64 | 'push_data' => [ 65 | 'pushed_at' => time(), 66 | ], 67 | 'repository' => [ 68 | 'status' => 'Active', 69 | 'description' => $repository->getTitle(), 70 | 'full_description' => $repository->getDescription(), 71 | 'repo_name' => $repository->getName(), 72 | 'is_private' => $repository->isPrivate(), 73 | 'star_count' => $repository->getStars(), 74 | ], 75 | ]; 76 | 77 | return json_encode($data); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AppBundle/Command/SetupBrokerCommand.php: -------------------------------------------------------------------------------- 1 | setName('dunkerque:broker:setup') 23 | ->setDescription('Setup broker') 24 | ; 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $input = new ArrayInput( 30 | [ 31 | 'filepath' => 'app/Resources/broker/mapping.yml', 32 | '--vhost' => '/', 33 | ], 34 | new InputDefinition([ 35 | new InputArgument('filepath'), 36 | new InputOption('vhost'), 37 | new InputOption('erase-vhost'), 38 | ]) 39 | ); 40 | 41 | parent::execute($input, $output); 42 | } 43 | 44 | protected function getCredentials(InputInterface $input, OutputInterface $output) 45 | { 46 | return [ 47 | 'host' => $this->container->getParameter('rabbitmq_host'), 48 | 'port' => $this->container->getParameter('rabbitmq_management_port'), 49 | 'user' => $this->container->getParameter('rabbitmq_login'), 50 | 'password' => $this->container->getParameter('rabbitmq_password'), 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/AccountController.php: -------------------------------------------------------------------------------- 1 | getUser() && ($account === $this->getUser()); 25 | 26 | $qb = $this->get('doctrine')->getRepository('AppBundle:Repository')->findByAccount($account, $isOwner); 27 | $pager = $this->get('datatheke.pager')->createHttpPager($qb); 28 | 29 | /** @var PagerView $view */ 30 | $view = $pager->handleRequest($request); 31 | $view->setParameters(['account' => $account->getUsername()]); 32 | 33 | return [ 34 | 'isOwner' => $isOwner, 35 | 'account' => $account, 36 | 'pager' => $view, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Admin/RepositoryController.php: -------------------------------------------------------------------------------- 1 | get('datatheke.datagrid')->createHttpDataGrid('AppBundle:Repository'); 23 | 24 | return [ 25 | 'datagrid' => $datagrid->handleRequest($request), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Admin/UserController.php: -------------------------------------------------------------------------------- 1 | get('datatheke.datagrid')->createHttpDataGrid('AppBundle:User'); 23 | $datagrid->showOnly(['username', 'email', 'enabled', 'id', 'lastLogin', 'locked']); 24 | 25 | return [ 26 | 'datagrid' => $datagrid->handleRequest($request), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | get('doctrine')->getRepository('AppBundle:Repository')->getLatestPublic(); 27 | 28 | return [ 29 | 'repositories' => $repositories, 30 | ]; 31 | } 32 | 33 | /** 34 | * @Template("default/most_stared_repositories.html.twig") 35 | */ 36 | public function mostStaredRepositoriesAction() 37 | { 38 | $repositories = $this->get('doctrine')->getRepository('AppBundle:Repository')->getMostStared(); 39 | 40 | return [ 41 | 'repositories' => $repositories, 42 | ]; 43 | } 44 | 45 | /** 46 | * @Template("default/most_pulled_repositories.html.twig") 47 | */ 48 | public function mostPulledRepositoriesAction() 49 | { 50 | $repositories = $this->get('doctrine')->getRepository('AppBundle:Repository')->getMostPulled(); 51 | 52 | return [ 53 | 'repositories' => $repositories, 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Registry/LayerController.php: -------------------------------------------------------------------------------- 1 | $layer->getDigest(), 38 | 'Content-Length' => $this->get('layer_manager')->getSize($layer), 39 | 'Content-Type' => 'application/octet-stream', 40 | ]; 41 | 42 | if ($request->isMethod('HEAD')) { 43 | return new Response('', Response::HTTP_OK, $headers); 44 | } 45 | 46 | return new StreamedResponse(function () use ($layer) { 47 | fpassthru($this->get('layer_manager')->read($layer)); 48 | }, Response::HTTP_OK, $headers); 49 | } 50 | 51 | /** 52 | * @Route("/uploads/", methods={"POST"}, name="layer_new") 53 | * 54 | * @Security("is_granted('REPO_WRITE', name)") 55 | * 56 | * @link http://docs.docker.com/registry/spec/api/#starting-an-upload 57 | */ 58 | public function newAction($name) 59 | { 60 | // FIXME: keep a link between the layer and the repository ? 61 | 62 | // Create the repository if it does not exist yet 63 | $repository = $this->get('doctrine')->getRepository('AppBundle:Repository')->findByNameOrCreate($name, $this->getUser()); 64 | 65 | $layer = $this->get('layer_manager')->create(); 66 | $this->get('layer_manager')->save($layer); 67 | 68 | return new Response('', Response::HTTP_ACCEPTED, [ 69 | 'Location' => $this->generateUrl('layer_upload', [ 70 | 'name' => $name, 71 | 'uuid' => $layer->getUuid(), 72 | '_state' => uniqid(), // FIXME: not implemented 73 | ], UrlGeneratorInterface::ABSOLUTE_URL), 74 | 'Docker-Upload-UUID' => $layer->getUuid(), 75 | ]); 76 | } 77 | 78 | /** 79 | * @Route("/uploads/{uuid}", methods={"GET"}, name="layer_upload_status") 80 | * 81 | * @ParamConverter(name="repository", options={"mapping": {"name": "name"}}) 82 | * @Security("is_granted('REPO_WRITE', repository)") 83 | * 84 | * @link http://docs.docker.com/registry/spec/api/#upload-progress 85 | */ 86 | public function uploadStatusAction(Repository $repository) 87 | { 88 | return new Response('', 404); 89 | } 90 | 91 | /** 92 | * @Route("/uploads/{uuid}", methods={"PUT", "PATCH"}, name="layer_upload", requirements={"uuid"="[0-9a-z-]+"}) 93 | * 94 | * @ParamConverter(name="repository", options={"mapping": {"name": "name"}}) 95 | * @ParamConverter(name="layer", options={"mapping": {"uuid": "uuid"}}) 96 | * 97 | * @Security("is_granted('REPO_WRITE', repository)") 98 | * 99 | * @link http://docs.docker.com/registry/spec/api/#uploading-the-layer 100 | */ 101 | public function uploadAction(Request $request, Repository $repository, Layer $layer) 102 | { 103 | if (Layer::STATUS_COMPLETE === $layer->getStatus()) { 104 | throw new BadRequestHttpException(sprintf('Layer with uuid "%s" has already been uploaded', $layer->getUuid())); 105 | } 106 | 107 | $finalUpload = $request->query->has('digest'); 108 | $this->get('layer_manager')->write($layer, $request->getContent(true)); 109 | 110 | if (!$finalUpload) { 111 | $layer->setStatus(Layer::STATUS_PARTIAL); 112 | $this->get('layer_manager')->save($layer); 113 | 114 | return new Response('', Response::HTTP_ACCEPTED, [ 115 | 'Location' => $this->generateUrl('layer_upload', [ 116 | 'name' => $repository->getName(), 117 | 'uuid' => $layer->getUuid(), 118 | ], UrlGeneratorInterface::ABSOLUTE_URL), 119 | 'Docker-Upload-UUID' => $layer->getUuid(), 120 | 'Range' => '0-'.($this->get('layer_manager')->getSize($layer) - 1), // FIXME: need '-1' to be compatible with registry:2 121 | ]); 122 | } 123 | 124 | $digest = $this->get('layer_manager')->computeDigest($layer); 125 | 126 | if ($digest !== $request->query->get('digest')) { 127 | throw new BadRequestHttpException(sprintf('Digest does not match with received data (computed: "%s")', $digest)); 128 | } 129 | 130 | $layer->setDigest($digest); 131 | $layer->setStatus(Layer::STATUS_COMPLETE); 132 | $this->get('layer_manager')->save($layer); 133 | 134 | return new Response('', Response::HTTP_CREATED, [ 135 | 'Location' => $this->generateUrl('layer_get', [ 136 | 'name' => $repository->getName(), 137 | 'digest' => $layer->getDigest(), 138 | ], UrlGeneratorInterface::ABSOLUTE_URL), 139 | 'Docker-Content-Digest' => $layer->getDigest(), 140 | ]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Registry/ManifestController.php: -------------------------------------------------------------------------------- 1 | get('event_dispatcher')->dispatch('delayed', new DelayedEvent('kernel.terminate', 'manifest.pull', $event)); 39 | 40 | return new Response($manifest->getContent(), Response::HTTP_OK, [ 41 | 'Content-Type' => 'application/json; charset=utf-8', 42 | ]); 43 | } 44 | 45 | /** 46 | * @Route("/{reference}", methods={"PUT"}, name="manifest_put") 47 | * 48 | * @ParamConverter("manifest", options={"repository_method": "findOneByReferenceOrCreate", "map_method_signature": true}) 49 | * 50 | * @Security("is_granted('REPO_WRITE', repository)") 51 | * 52 | * @link http://docs.docker.com/registry/spec/api/#put-manifest 53 | */ 54 | public function uploadAction(Request $request, Repository $repository, Manifest $manifest, $reference) 55 | { 56 | $manifest->setContent($request->getContent()); 57 | 58 | if ($reference !== $manifest->getTag() && $reference !== $manifest->getDigest()) { 59 | throw new BadRequestHttpException('Provided reference does not match with tag or digest.'); 60 | } 61 | 62 | // TODO: validate layers & signatures 63 | 64 | $this->get('doctrine')->getRepository('AppBundle:Manifest')->save($manifest); 65 | 66 | // Dispatch event 67 | $event = new ManifestEvent($manifest); 68 | $this->get('event_dispatcher')->dispatch('delayed', new DelayedEvent('kernel.terminate', 'manifest.push', $event)); 69 | 70 | return new Response('', Response::HTTP_CREATED, [ 71 | 'Location' => $this->generateUrl('manifest_get', [ 72 | 'name' => $manifest->getRepository()->getName(), 73 | 'reference' => $manifest->getDigest(), 74 | ], UrlGeneratorInterface::ABSOLUTE_URL), 75 | 'Docker-Content-Digest' => $manifest->getDigest(), 76 | ]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Registry/TokenController.php: -------------------------------------------------------------------------------- 1 | getUser(); 23 | if (null === $user) { 24 | $user = new User('Anon.', null); 25 | } 26 | 27 | $token = $this->get('lexik_jwt_authentication.jwt_manager')->create($user); 28 | 29 | return new JsonResponse([ 30 | 'token' => $token, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/Registry/VersionController.php: -------------------------------------------------------------------------------- 1 | 'application/json; charset=utf-8', 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/RepositoryController.php: -------------------------------------------------------------------------------- 1 | isGranted('REPO_WRITE', $repository)) { 33 | $form = $this->createForm(RepositoryType::class, $repository); 34 | $form->handleRequest($request); 35 | if ($form->isValid()) { 36 | $this->get('doctrine')->getManager()->flush(); 37 | 38 | return $this->redirect($this->generateUrl('repository', [ 39 | 'name' => $repository->getName(), 40 | ])); 41 | } 42 | } 43 | 44 | return [ 45 | 'repository' => $repository, 46 | 'form' => isset($form) ? $form->createView() : null, 47 | ]; 48 | } 49 | 50 | /** 51 | * @Route("/r/{name}/~/{action}", requirements={"name"="%regex_name%", "action":"^star|unstar$"}, methods={"PUT"}, name="repository_star") 52 | * 53 | * @ParamConverter("repository", options={"mapping": {"name": "name"}}) 54 | * 55 | * @Security("is_granted('ROLE_USER', repository)") 56 | */ 57 | public function starOrUnstarAction(Repository $repository, $action) 58 | { 59 | $manager = $this->get('repository_star_manager'); 60 | $user = $this->getUser(); 61 | 62 | $starred = $manager->isStarredByUser($repository, $user); 63 | 64 | if (('star' === $action && !$starred) || ('unstar' === $action && $starred)) { 65 | $manager->$action($repository, $user); 66 | $starred = !$starred; 67 | } 68 | 69 | return new JsonResponse(['starred' => $starred]); 70 | } 71 | 72 | /** 73 | * @Route("/r/{name}/~/webhooks", requirements={"name"="%regex_name%"}, methods={"GET", "POST"}, name="repository_webhooks") 74 | * 75 | * @ParamConverter("repository", options={"mapping": {"name": "name"}}) 76 | * 77 | * @Security("is_granted('REPO_WRITE', repository)") 78 | * 79 | * @Template("repository/webhooks.html.twig") 80 | */ 81 | public function webhooksAction(Request $request, Repository $repository) 82 | { 83 | $form = $this->createForm(WebhookType::class, new Webhook($repository)); 84 | $form->handleRequest($request); 85 | if ($form->isValid()) { 86 | $em = $this->get('doctrine')->getManager(); 87 | $em->persist($form->getData()); 88 | $em->flush(); 89 | 90 | $this->addFlash('success', 'webhook.created'); 91 | 92 | return $this->redirect($this->generateUrl('repository_webhooks', [ 93 | 'name' => $repository->getName(), 94 | ])); 95 | } 96 | 97 | $pager = $this->get('datatheke.pager')->createHttpPager('AppBundle:Webhook'); 98 | $pager->setFilter(new Filter(['repository'], [$repository]), 'base'); 99 | 100 | /** @var PagerView $pagerView */ 101 | $pagerView = $pager->handleRequest($request); 102 | $pagerView->setParameters(['name' => $repository->getName()]); 103 | 104 | return [ 105 | 'repository' => $repository, 106 | 'form' => $form->createView(), 107 | 'pager' => $pagerView, 108 | ]; 109 | } 110 | 111 | /** 112 | * @Route("/r/{name}/~/webhooks/{id}/remove", requirements={"name"="%regex_name%"}, methods={"GET", "POST"}, name="repository_webhooks_remove") 113 | * 114 | * @ParamConverter("repository", options={"mapping": {"name": "name"}}) 115 | * 116 | * @Security("is_granted('REPO_WRITE', repository)") 117 | */ 118 | public function removeWebhookAction(Request $request, Repository $repository, $id) 119 | { 120 | if (!$this->isCsrfTokenValid('webhook_remove', $request->query->get('_token'))) { 121 | $this->addFlash('error', 'invalid_csrf_token'); 122 | 123 | return $this->redirectToRoute('repository_webhooks', ['name' => $repository->getName()]); 124 | } 125 | 126 | $webhook = $this->get('doctrine')->getRepository('AppBundle:Webhook')->findOneBy([ 127 | 'repository' => $repository, 128 | 'id' => $id, 129 | ]); 130 | if (null === $webhook) { 131 | $this->addFlash('error', 'webhook.not_found_or_not_granted'); 132 | 133 | return $this->redirectToRoute('repository_webhooks', ['name' => $repository->getName()]); 134 | } 135 | 136 | $manager = $this->get('doctrine')->getManager(); 137 | $manager->remove($webhook); 138 | $manager->flush(); 139 | 140 | $this->addFlash('success', 'webhook.removed'); 141 | 142 | return $this->redirectToRoute('repository_webhooks', ['name' => $repository->getName()]); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/SearchController.php: -------------------------------------------------------------------------------- 1 | query->get('q', ''); 22 | 23 | /** @var HttpPagerInterface $pager */ 24 | $pager = $this->get('search_manager')->createPager($keyword); 25 | 26 | /** @var PagerView $view */ 27 | $view = $pager->handleRequest($request); 28 | $view->setParameters(['q' => $keyword]); 29 | 30 | return [ 31 | 'keyword' => $keyword, 32 | 'pager' => $view, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/Layer.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid ?: Uuid::uuid4()->toString(); 56 | } 57 | 58 | /** 59 | * Get id. 60 | * 61 | * @return int 62 | */ 63 | public function getId() 64 | { 65 | return $this->id; 66 | } 67 | 68 | /** 69 | * Get uuid. 70 | * 71 | * @return guid 72 | */ 73 | public function getUuid() 74 | { 75 | return $this->uuid; 76 | } 77 | 78 | /** 79 | * Set digest. 80 | * 81 | * @param string $digest 82 | * 83 | * @return Layer 84 | */ 85 | public function setDigest($digest) 86 | { 87 | $this->digest = $digest; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get digest. 94 | * 95 | * @return string 96 | */ 97 | public function getDigest() 98 | { 99 | return $this->digest; 100 | } 101 | 102 | /** 103 | * @return int 104 | */ 105 | public function getStatus() 106 | { 107 | return $this->status; 108 | } 109 | 110 | /** 111 | * @param int $status 112 | * 113 | * @return Layer 114 | */ 115 | public function setStatus($status) 116 | { 117 | $this->status = $status; 118 | 119 | return $this; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/Manifest.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 77 | $this->updatedAt = new \DateTime(); 78 | } 79 | 80 | /** 81 | * Get id. 82 | * 83 | * @return int 84 | */ 85 | public function getId() 86 | { 87 | return $this->id; 88 | } 89 | 90 | /** 91 | * @return Repository 92 | */ 93 | public function getRepository() 94 | { 95 | return $this->repository; 96 | } 97 | 98 | /** 99 | * @param Repository $repository 100 | * 101 | * @return Manifest 102 | */ 103 | public function setRepository($repository) 104 | { 105 | $this->repository = $repository; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Get tag. 112 | * 113 | * @return string 114 | */ 115 | public function getTag() 116 | { 117 | return $this->tag; 118 | } 119 | 120 | /** 121 | * Get digest. 122 | * 123 | * @return string 124 | */ 125 | public function getDigest() 126 | { 127 | return $this->digest; 128 | } 129 | 130 | /** 131 | * Set content. 132 | * 133 | * @param string $content 134 | * 135 | * @return Manifest 136 | */ 137 | public function setContent($content) 138 | { 139 | $this->content = $content; 140 | $this->digest = $this->computeDigest($content); 141 | 142 | $decoded = json_decode($content, true); 143 | $this->tag = $decoded['tag']; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get content. 150 | * 151 | * @return string 152 | */ 153 | public function getContent() 154 | { 155 | return $this->content; 156 | } 157 | 158 | /** 159 | * Return the digest of the provided manifest. 160 | * 161 | * @param $manifest 162 | * 163 | * @return string 164 | */ 165 | protected function computeDigest($manifest) 166 | { 167 | // See func `ParsePrettySignature` in https://github.com/docker/libtrust/blob/master/jsonsign.go 168 | $decoded = json_decode($manifest, true); 169 | $formatLength = null; 170 | $formatTail = null; 171 | foreach ($decoded['signatures'] as $signature) { 172 | $header = json_decode(base64_decode($signature['protected']), true); 173 | 174 | $formatLength = $header['formatLength']; 175 | $formatTail = $header['formatTail']; 176 | } 177 | 178 | // Manifest without signatures 179 | $manifest = substr($manifest, 0, $formatLength).base64_decode($formatTail); 180 | 181 | // Digest 182 | return 'sha256:'.hash('sha256', $manifest); 183 | } 184 | 185 | /** 186 | * @return int 187 | */ 188 | public function getPulls() 189 | { 190 | return $this->pulls; 191 | } 192 | 193 | /** 194 | * @param int $pulls 195 | * 196 | * @return $this 197 | */ 198 | public function setPulls($pulls) 199 | { 200 | $this->pulls = $pulls; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @return \DateTime 207 | */ 208 | public function getUpdatedAt() 209 | { 210 | return $this->updatedAt; 211 | } 212 | 213 | /** 214 | * @param \DateTime $updatedAt 215 | * 216 | * @return $this 217 | */ 218 | public function setUpdatedAt($updatedAt) 219 | { 220 | $this->updatedAt = $updatedAt; 221 | 222 | return $this; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/ManifestRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('m') 12 | ->where('m.repository = :repository') 13 | ->andWhere('(m.digest = :reference OR m.tag = :reference)') 14 | ->setParameters([ 15 | 'repository' => $repository, 16 | 'reference' => $reference, 17 | ]) 18 | ; 19 | 20 | return $qb->getQuery()->getOneOrNullResult(); 21 | } 22 | 23 | public function findOneByReferenceOrCreate(Repository $repository, $reference) 24 | { 25 | $manifest = $this->findOneByReference($repository, $reference); 26 | if (null === $manifest) { 27 | $manifest = $this->create($repository); 28 | } 29 | 30 | return $manifest; 31 | } 32 | 33 | public function create(Repository $repository) 34 | { 35 | return new Manifest($repository); 36 | } 37 | 38 | public function save(Manifest $manifest) 39 | { 40 | $this->_em->persist($manifest); 41 | $this->_em->flush(); 42 | } 43 | 44 | public function incrementPulls(Manifest $manifest) 45 | { 46 | // Increment Manifest 47 | $this->_em 48 | ->createQuery(<<setParameter('manifest', $manifest->getId()) 55 | ->execute() 56 | ; 57 | 58 | // Increment Repository 59 | $this->_em 60 | ->createQuery(<<setParameter('repository', $manifest->getRepository()->getId()) 67 | ->execute() 68 | ; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RepositoryRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('r') 13 | ->where('r.owner = :account') 14 | ->setParameter('account', $account) 15 | ; 16 | 17 | if (!$isOwner) { 18 | $qb->andWhere('r.private = false'); 19 | } 20 | 21 | return $qb; 22 | } 23 | 24 | public function findByNameOrCreate($name, User $owner) 25 | { 26 | $repository = $this->findOneByName($name); 27 | if (null === $repository) { 28 | try { 29 | $repository = $this->create($name, $owner); 30 | $this->save($repository); 31 | } catch (UniqueConstraintViolationException $e) { 32 | $repository = $this->findOneByName($name); 33 | } 34 | } 35 | 36 | return $repository; 37 | } 38 | 39 | public function create($name, User $owner) 40 | { 41 | return new Repository($name, $owner); 42 | } 43 | 44 | public function save(Repository $repository) 45 | { 46 | $this->_em->persist($repository); 47 | $this->_em->flush(); 48 | } 49 | 50 | public function getLatestPublic($limit = 10) 51 | { 52 | return $this->findBy(['private' => false], null, $limit); 53 | } 54 | 55 | public function getMostStared($limit = 10) 56 | { 57 | return $this->findBy(['private' => false], ['stars' => 'desc'], $limit); 58 | } 59 | 60 | public function getMostPulled($limit = 10) 61 | { 62 | return $this->findBy(['private' => false], ['pulls' => 'desc'], $limit); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RepositoryStar.php: -------------------------------------------------------------------------------- 1 | setRepository($repository) 44 | ->setUser($user); 45 | } 46 | 47 | /** 48 | * @return User 49 | */ 50 | public function getUser() 51 | { 52 | return $this->user; 53 | } 54 | 55 | /** 56 | * @param User $user 57 | * 58 | * @return $this 59 | */ 60 | public function setUser(User $user) 61 | { 62 | $this->user = $user; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @return Repository 69 | */ 70 | public function getRepository() 71 | { 72 | return $this->repository; 73 | } 74 | 75 | /** 76 | * @param Repository $repository 77 | * 78 | * @return $this 79 | */ 80 | public function setRepository(Repository $repository) 81 | { 82 | $this->repository = $repository; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RepositoryStarListener.php: -------------------------------------------------------------------------------- 1 | elasticaObjectPersister = $elasticaObjectPersister; 18 | } 19 | 20 | /** 21 | * @ORM\PostPersist 22 | */ 23 | public function incStarsCount(RepositoryStar $repositoryStar, LifecycleEventArgs $events) 24 | { 25 | $this->updateStarsCount($repositoryStar, $events->getEntityManager(), '+'); 26 | } 27 | 28 | /** 29 | * @ORM\PostRemove 30 | */ 31 | public function decStarsCount(RepositoryStar $repositoryStar, LifecycleEventArgs $events) 32 | { 33 | $this->updateStarsCount($repositoryStar, $events->getEntityManager(), '-'); 34 | } 35 | 36 | protected function updateStarsCount(RepositoryStar $repositoryStar, EntityManager $em, $operator) 37 | { 38 | $repository = $repositoryStar->getRepository(); 39 | 40 | $qb = $em->createQueryBuilder() 41 | ->update(Repository::class, 'r') 42 | ->set('r.stars', "r.stars $operator 1") 43 | ->where('r.id = :id') 44 | ->setParameter('id', $repository->getId()) 45 | ->getQuery(); 46 | $qb->execute(); 47 | 48 | $em->refresh($repository); 49 | 50 | $this->elasticaObjectPersister->replaceOne($repository); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RepositoryStarRepository.php: -------------------------------------------------------------------------------- 1 | findOneBy([ 12 | 'repository' => $repository, 13 | 'user' => $user, 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/User.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 71 | } 72 | 73 | /** 74 | * Get id. 75 | * 76 | * @return int 77 | */ 78 | public function getId() 79 | { 80 | return $this->id; 81 | } 82 | 83 | /** 84 | * @return Repository 85 | */ 86 | public function getRepository() 87 | { 88 | return $this->repository; 89 | } 90 | 91 | /** 92 | * @param Repository $repository 93 | * 94 | * @return Manifest 95 | */ 96 | public function setRepository($repository) 97 | { 98 | $this->repository = $repository; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Set name. 105 | * 106 | * @param string $name 107 | * 108 | * @return Webhook 109 | */ 110 | public function setName($name) 111 | { 112 | $this->name = $name; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Get name. 119 | * 120 | * @return string 121 | */ 122 | public function getName() 123 | { 124 | return $this->name; 125 | } 126 | 127 | /** 128 | * Set url. 129 | * 130 | * @param string $url 131 | * 132 | * @return Webhook 133 | */ 134 | public function setUrl($url) 135 | { 136 | $this->url = $url; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Get url. 143 | * 144 | * @return string 145 | */ 146 | public function getUrl() 147 | { 148 | return $this->url; 149 | } 150 | 151 | /** 152 | * @return \DateTime 153 | */ 154 | public function getLastCall() 155 | { 156 | return $this->lastCall; 157 | } 158 | 159 | /** 160 | * @param \DateTime $lastCall 161 | * 162 | * @return $this 163 | */ 164 | public function setLastCall(\DateTime $lastCall) 165 | { 166 | $this->lastCall = $lastCall; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * @return string 173 | */ 174 | public function getLastStatus() 175 | { 176 | return $this->lastStatus; 177 | } 178 | 179 | /** 180 | * @param string $lastStatus 181 | * 182 | * @return $this 183 | */ 184 | public function setLastStatus($lastStatus) 185 | { 186 | $this->lastStatus = $lastStatus; 187 | 188 | return $this; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/AppBundle/Event/DelayedEvent.php: -------------------------------------------------------------------------------- 1 | trigger = $trigger; 27 | $this->eventName = $eventName; 28 | $this->event = $event; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getTrigger() 35 | { 36 | return $this->trigger; 37 | } 38 | 39 | /** 40 | * @return mixed 41 | */ 42 | public function getEventName() 43 | { 44 | return $this->eventName; 45 | } 46 | 47 | /** 48 | * @return Event 49 | */ 50 | public function getEvent() 51 | { 52 | return $this->event; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AppBundle/Event/ManifestEvent.php: -------------------------------------------------------------------------------- 1 | manifest = $manifest; 18 | } 19 | 20 | public function getManifest() 21 | { 22 | return $this->manifest; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AppBundle/EventListener/DelayedEventListener.php: -------------------------------------------------------------------------------- 1 | ['onDelayedEvent'], 18 | ]; 19 | } 20 | 21 | public function onDelayedEvent(DelayedEvent $event, $eventName, EventDispatcherInterface $dispatcher) 22 | { 23 | if (!isset($this->events[$event->getTrigger()])) { 24 | $this->events[$event->getTrigger()] = []; 25 | $dispatcher->addListener($event->getTrigger(), [$this, 'triggerEvent']); 26 | } 27 | 28 | $this->events[$event->getTrigger()][] = $event; 29 | } 30 | 31 | public function triggerEvent(Event $triggerEvent, $eventName, EventDispatcherInterface $dispatcher) 32 | { 33 | /** @var DelayedEvent $delayedEvent */ 34 | while ($delayedEvent = array_shift($this->events[$eventName])) { 35 | $dispatcher->dispatch($delayedEvent->getEventName(), $delayedEvent->getEvent()); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AppBundle/EventListener/HeaderResponseListener.php: -------------------------------------------------------------------------------- 1 | ['onKernelResponse'], 14 | ]; 15 | } 16 | 17 | public function onKernelResponse(FilterResponseEvent $event) 18 | { 19 | $event->getResponse()->headers->add([ 20 | 'Docker-Distribution-Api-Version' => 'registry/2.0', 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AppBundle/EventListener/ManifestPullListener.php: -------------------------------------------------------------------------------- 1 | ['onManifestPull'], 20 | ]; 21 | } 22 | 23 | public function __construct(ObjectManager $om) 24 | { 25 | $this->om = $om; 26 | } 27 | 28 | public function onManifestPull(ManifestEvent $event) 29 | { 30 | $this->om->getRepository('AppBundle:Manifest')->incrementPulls($event->getManifest()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/AppBundle/EventListener/ManifestPushListener.php: -------------------------------------------------------------------------------- 1 | ['onManifestPush'], 33 | ]; 34 | } 35 | 36 | public function __construct(ObjectManager $om, Publisher $publisher, SerializerInterface $serializer) 37 | { 38 | $this->om = $om; 39 | $this->publisher = $publisher; 40 | $this->serializer = $serializer; 41 | } 42 | 43 | public function onManifestPush(ManifestEvent $event) 44 | { 45 | $manifest = $event->getManifest(); 46 | 47 | $message = new Message($this->serializer->serialize($manifest, 'json', ['groups' => ['manifest_push']])); 48 | $this->publisher->publish('manifest_push', $message); 49 | 50 | $manifest->setUpdatedAt(new \DateTime()); 51 | $this->om->flush(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AppBundle/EventListener/RegistryExceptionListener.php: -------------------------------------------------------------------------------- 1 | ['onKernelException'], 17 | ]; 18 | } 19 | 20 | public function onKernelException(GetResponseForExceptionEvent $event) 21 | { 22 | $path = $event->getRequest()->getPathInfo(); 23 | if ($path !== '/token' && 0 !== strpos($path, '/v2/')) { 24 | return; 25 | } 26 | 27 | $exception = $event->getException(); 28 | $code = $exception instanceof HttpException ? $exception->getStatusCode() : $exception->getCode(); 29 | $headers = $exception instanceof HttpException ? $exception->getHeaders() : []; 30 | $errorCode = isset(Response::$statusTexts[$code]) ? Response::$statusTexts[$code] : 'error'; 31 | 32 | $response = new JsonResponse([ 33 | 'errors' => [ 34 | [ 35 | 'code' => strtoupper($errorCode), 36 | 'details' => null, 37 | 'message' => $exception->getMessage(), 38 | ], 39 | ], 40 | ], $code, $headers); 41 | 42 | $event->setResponse($response); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/AppBundle/Form/Type/RepositoryType.php: -------------------------------------------------------------------------------- 1 | add('title') 20 | ->add('description') 21 | ->add('private', CheckboxType::class, ['required' => false]) 22 | ; 23 | } 24 | 25 | /** 26 | * @param OptionsResolver $resolver 27 | */ 28 | public function configureOptions(OptionsResolver $resolver) 29 | { 30 | $resolver->setDefaults(array( 31 | 'data_class' => 'AppBundle\Entity\Repository', 32 | )); 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getBlockPrefix() 39 | { 40 | return 'repository'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AppBundle/Form/Type/WebhookType.php: -------------------------------------------------------------------------------- 1 | add('name') 19 | ->add('url') 20 | ; 21 | } 22 | 23 | /** 24 | * @param OptionsResolver $resolver 25 | */ 26 | public function configureOptions(OptionsResolver $resolver) 27 | { 28 | $resolver->setDefaults(array( 29 | 'data_class' => 'AppBundle\Entity\Webhook', 30 | )); 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getBlockPrefix() 37 | { 38 | return 'webhook'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/AppBundle/Manager/LayerManager.php: -------------------------------------------------------------------------------- 1 | om = $om; 24 | $this->fs = $fs; 25 | } 26 | 27 | public function save(Layer $layer) 28 | { 29 | $this->om->persist($layer); 30 | $this->om->flush(); 31 | } 32 | 33 | public function create() 34 | { 35 | return new Layer(); 36 | } 37 | 38 | public function write(Layer $layer, $content) 39 | { 40 | $path = $this->getContentPath($layer); 41 | 42 | # Append content manually as Flysystem does not support it 43 | if ($this->fs->has($path)) { 44 | $stream = fopen('php://temp', 'w'); 45 | stream_copy_to_stream($this->fs->readStream($path), $stream); 46 | stream_copy_to_stream($content, $stream); 47 | } else { 48 | $stream = $content; 49 | } 50 | 51 | $this->fs->putStream($path, $stream); 52 | } 53 | 54 | public function read(Layer $layer) 55 | { 56 | return $this->fs->readStream($this->getContentPath($layer)); 57 | } 58 | 59 | public function getSize(Layer $layer) 60 | { 61 | return $this->fs->getSize($this->getContentPath($layer)); 62 | } 63 | 64 | public function computeDigest(Layer $layer) 65 | { 66 | $hc = hash_init('sha256'); 67 | hash_update_stream($hc, $this->fs->readStream($this->getContentPath($layer))); 68 | 69 | return 'sha256:'.hash_final($hc); 70 | } 71 | 72 | protected function getContentPath(Layer $layer) 73 | { 74 | return 'layers/'.$layer->getUuid(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AppBundle/Manager/RepositoryStarManager.php: -------------------------------------------------------------------------------- 1 | om = $om; 18 | } 19 | 20 | protected function getRepository() 21 | { 22 | return $this->om->getRepository('AppBundle:RepositoryStar'); 23 | } 24 | 25 | public function isStarredByUser(Repository $repository, User $user) 26 | { 27 | $repositoryStar = $this->getRepository()->findOneByRepositoryAndUser($repository, $user); 28 | 29 | return null !== $repositoryStar; 30 | } 31 | 32 | public function star(Repository $repository, User $user) 33 | { 34 | $repositoryStar = RepositoryStar::create($repository, $user); 35 | 36 | $this->om->persist($repositoryStar); 37 | $this->om->flush(); 38 | } 39 | 40 | public function unstar(Repository $repository, User $user) 41 | { 42 | $repositoryStar = $this->getRepository()->findOneByRepositoryAndUser($repository, $user); 43 | 44 | $this->om->remove($repositoryStar); 45 | $this->om->flush(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AppBundle/Manager/SearchManager.php: -------------------------------------------------------------------------------- 1 | pagerFactory = $pagerFactory; 35 | $this->repositoryType = $repositoryType; 36 | $this->tokenStorage = $tokenStorage; 37 | } 38 | 39 | /** 40 | * @param string $keyword 41 | * 42 | * @return HttpPager 43 | */ 44 | public function createPager($keyword) 45 | { 46 | $fields = [ 47 | 'name' => new Field('name'), 48 | 'title' => new Field('title'), 49 | 'description' => new Field('description'), 50 | 'private' => new Field('private'), 51 | ]; 52 | $adapter = new ElasticaAdapter($this->repositoryType, $fields, $this->createSearchQuery($keyword)); 53 | 54 | return $this->pagerFactory->createHttpPager($adapter); 55 | } 56 | 57 | /** 58 | * @param string $keyword 59 | * 60 | * @return Query 61 | */ 62 | protected function createSearchQuery($keyword) 63 | { 64 | return Query::create((new BoolQuery()) 65 | ->addMust($this->getTermQuery($keyword)) 66 | ->addMust($this->getPrivacyQuery()) 67 | ); 68 | } 69 | 70 | /** 71 | * @param string $keyword 72 | * 73 | * @return SimpleQueryString 74 | */ 75 | protected function getTermQuery($keyword) 76 | { 77 | return new SimpleQueryString($keyword, ['name', 'title', 'description']); 78 | } 79 | 80 | /** 81 | * @return BoolQuery 82 | */ 83 | protected function getPrivacyQuery() 84 | { 85 | $query = new BoolQuery(); 86 | $query->addShould(new Term(['private' => false])); 87 | if (($user = $this->tokenStorage->getToken()->getUser()) instanceof User) { 88 | $query->addShould(new Term(['owner.id' => $user->getId()])); 89 | } 90 | 91 | return $query; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AppBundle/Security/RegistryEntryPoint.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 21 | } 22 | 23 | public function start(Request $request, AuthenticationException $authException = null) 24 | { 25 | $tokenEndpoint = $this->urlGenerator->generate('registry_token', [], UrlGeneratorInterface::ABSOLUTE_URL); 26 | $serviceEndpoint = $this->urlGenerator->generate('index', [], UrlGeneratorInterface::ABSOLUTE_URL); 27 | 28 | $data = [ 29 | 'errors' => [ 30 | [ 31 | 'code' => 'UNAUTHORIZED', 32 | 'details' => null, 33 | 'message' => 'access to the requested resource is not authorized', 34 | ], 35 | ], 36 | ]; 37 | 38 | return new JsonResponse($data, 401, [ 39 | 'WWW-Authenticate' => sprintf('Bearer realm="%s",service="%s"', $tokenEndpoint, $serviceEndpoint), 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AppBundle/Security/Voter/RepositoryVoter.php: -------------------------------------------------------------------------------- 1 | om = $om; 36 | $this->roleHierarchyVoter = $roleHierarchyVoter; 37 | $this->logger = $logger; 38 | } 39 | 40 | public function supportsAttribute($attribute) 41 | { 42 | return in_array($attribute, [self::READ, self::WRITE]); 43 | } 44 | 45 | public function supportsClass($class) 46 | { 47 | return true; 48 | } 49 | 50 | public function vote(TokenInterface $token, $repository, array $attributes) 51 | { 52 | // abstain vote by default in case none of the attributes are supported 53 | $vote = self::ACCESS_ABSTAIN; 54 | 55 | foreach ($attributes as $attribute) { 56 | if (!$this->supportsAttribute($attribute)) { 57 | continue; 58 | } 59 | 60 | // as soon as at least one attribute is supported, default is to deny access 61 | $vote = self::ACCESS_DENIED; 62 | 63 | if ($this->isGranted($attribute, $repository, $token)) { 64 | // grant access as soon as at least one voter returns a positive response 65 | return self::ACCESS_GRANTED; 66 | } 67 | } 68 | 69 | return $vote; 70 | } 71 | 72 | protected function isGranted($attribute, $repository, TokenInterface $token) 73 | { 74 | // Admin can do everything 75 | if (VoterInterface::ACCESS_GRANTED === $this->roleHierarchyVoter->vote($token, null, ['ROLE_ADMIN'])) { 76 | return true; 77 | } 78 | 79 | $user = $token->getUser(); 80 | 81 | // We allow to check by repository name 82 | // Needed when pushing the first manifest, that will create the repository 83 | if (!$repository instanceof Repository) { 84 | $name = $repository; 85 | $repository = $this->om->getRepository('AppBundle:Repository')->findOneByName($repository); 86 | if (null === $repository) { 87 | // repository does not exist 88 | // User tries to access root namespace but is not ADMIN 89 | if (false === strpos($name, '/')) { 90 | return false; 91 | } 92 | 93 | // Use not logged 94 | if (!$user instanceof UserInterface) { 95 | return false; 96 | } 97 | 98 | list($tld) = explode('/', $name); 99 | 100 | return $tld === $user->getUsername(); 101 | } 102 | } 103 | 104 | $isOwner = $user instanceof UserInterface && $repository->getOwner() === $user; 105 | 106 | switch ($attribute) { 107 | case self::READ: 108 | return $isOwner || $repository->isPublic(); 109 | 110 | case self::WRITE: 111 | return $isOwner; 112 | } 113 | 114 | return false; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/AppBundle/Twig/AppExtension.php: -------------------------------------------------------------------------------- 1 | repositoryStarManager = $repositoryStarManager; 17 | } 18 | 19 | public function getFunctions() 20 | { 21 | return [ 22 | new \Twig_SimpleFunction('is_starred_by_user', [$this, 'isStarredByUser']), 23 | ]; 24 | } 25 | 26 | public function isStarredByUser(Repository $repository, User $user) 27 | { 28 | return $this->repositoryStarManager->isStarredByUser($repository, $user); 29 | } 30 | 31 | public function getName() 32 | { 33 | return 'app_extension'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /var/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/var/cache/.gitkeep -------------------------------------------------------------------------------- /var/jwt/private.pem.dist: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,882B7F77130F88B7B9DE02C484EBC88E 4 | 5 | 8Xj8pXbsAo9N1IlnzPptUvQOqudFcOnWbEM7zWFKyNzngTexFVPjNZAncR1gLolF 6 | 9o15pzQSQux6KeBWLPf627/7mo+UAXQnj3vOtjONsKN3/v3wvzC7EUSzKBiOT3bb 7 | N4nVrQT8CuPk4LX5mglXupW1TPEujsHiFWusqiwdyMOySt9pudFJHlXNU5XBFIGr 8 | diiToYnxbisBBlKfa2LCx0deIJPCeOy/ggJ39g2A6dYeo/w1RLuMWktUyKsmJr02 9 | FksZ2sRbTLoYxf7uDTvHZ4xuRju8UredZmXzJlfk3gTq1zSmViuzUn9De1nvYNP3 10 | 9FtxCQCrBCBqcu+qSwYYuxm1tTbq/Ad5tKmq3S1M1alotauLrnzd8/Wq9rt5folX 11 | qOHun2NTktuOjeGsdDFprUNFzC7F75nxqbouQpvPtIwxVYUQr5AcQzyyVIyL4h2K 12 | Lqpvqc8AJpNdkcH8fcqtZsd12PstYqqVcd2QuR2Zd9dX+1odfax1pl/Ui18ytB11 13 | Z82OqnxZcDQH13ZXK7zuifarrVwAwBXgye6bjG09coGjOjpWL1wDNQxgEOEQIeSy 14 | Xy5sTK3YnKJYLhORZfcltN8fkfIrjD4HiZGUuCrZ7Yrh+aJn66ioAGZ1O0NjB09v 15 | 3WU+mkNFxpnCaaKz4YWXfNzEpV2QiVELpGmwvPX0gvhnjAnPIMtWrBG4GDjzHsCn 16 | Vz7FNZk1jjMC4/WAbhw95OLtg8DIjrz6/Dw1UPGxDrHC7MoSZjx063xcdF1wl8Lh 17 | SP3Tdrq0dX+CweOdXIM+9bqrsnjPMj/6479Kwb1+Dizzat2pXZb1MOqbiVXNa1sv 18 | VcbMCwz3sNPuMoiB1a+/5pjqnisCWTSHZHzqIz69kRFdSED+nHceUfn5Egeuii3g 19 | 7OB3l1dMrYTJ2jyTWRoNkMY+0C1ec6W+/FtfElX+2//72v15iRMvd1stFNa9TYYt 20 | ODu5ZYBVjz0wMBpw//4NFzBCVq1PUL6e91oQY/W0jQKUBxoDqbJ/s/0/+y7Czewf 21 | zVSoCER9NPYfVqrY0yuZRk5nolGdZKQOyWELQR/1qHlcvzE5tL7FIV+hYQJomR7f 22 | XSEXtWS5YhrVhZ4TLhNj4T6YbKdJnVObS9XWs4fuBfwOlqCFgDYrrJBGZMmEMrWi 23 | NedBE4e+nO4Lf9dtGPN2KhEaKqhqSuyw97DEVdKhx2C39S7EYcZD8TA6Xxsd9UVa 24 | ObJaEab9JyGcm5CsFa4ebxRIg+xXyLP8arHSYH77bG3TFQ3txA3LXIMM9BmEypkZ 25 | GASISS0/vwfFaydYihZjVk8LigLcoGgxHLaQM6rXGTV5xTgVJ/jTYwgEDOkoto5O 26 | 0701vPiUjRiHnVlvYMmy5lDPvtvvrwnH/EDDd3L5Ibly9KOC581niG2/0aUShBih 27 | rK1qXucTQbIkpsU97/ms582gcLr3WgkG+znYjvPRWzPMxiLTzlTkt1KLGHmH4TM4 28 | xIV2kXqnffTm9izpzlt+ZkmGRy+FLlbygfQfKofqj5T0BUT66on9KU0B7GLxkIrh 29 | ffbXTgvm2m2U/2txQSDZxPrz0+7qP8WyS1nlvYhi5vV2YRCjbKr7XfMxGgbXAwfl 30 | oAdtQNMGXKkQaY0JxRRyq6UQvARz3e08Q0x/nAI1VHGBajPjAyq2z+f1/OlZ7Z+w 31 | HX5HqCfHNcgGAlgprzAt6ws1X+N8pGKz3GZpp43T5aySagIaT5rGvLLtVsf0YxL0 32 | l29LxJ7c5+fHQZg5WguJQhBhStCboZZXYBCcACz3G9DelnNyQIlb98LT2+f0H+BO 33 | HW/3ua0KmWpBiGMICeAG1h8wmQ0E3xAjxsLLpjVDM0O0bDBhkxF/NQ9o/bcGBzZ3 34 | rnbHTLdAsX3Jxd/lNN5Rm9Io5jcRbgeOO64gKfUi8DjkuzuBwwUIScrazMzPnLxt 35 | 4npMDcDfaijy/KaZZm0GyxDBP37ms2wrPVNfBJoLlXilRKOaZPgfAJZePsG5bAjj 36 | CwZrW4B5XoP3G0f6IFWibRPrlA8PoU8A1LIwCML0CDVcoG8tj5gsnQ4d+WSjdt0i 37 | wODSIcpn3BwyPDqwXajbgLQN4y16svyedmOuQ3HUHjikIfHXJs83cCHBx7waBrt4 38 | snVNF9ob4Xme56xD67QyA45Hs/IpibEBlz4b5MHI0kNCMS7P1vpk2bX9dm2NXHXN 39 | 5IGjzbcn1mXMhS/yI6DpoJlszh78BoBEjm1q1z2EpmfC1hl/LbxrOiBHdM81Y+bs 40 | cXU9oT9wTt3PvynAijKzCsxJIS7UWnx8wNTGLg/1CHFxnRe6EDiOr/lbG/+CFKYS 41 | zWJ5d8WmeJW12fvYBoxS7apzdyiwa2J6zb47n1FfKZEl62ulxSF/XNTXJeOD6n5D 42 | J9+KZbL54Ts/4nnUF1WGjaZAsjzCMxZy60iHQ/E3xt4kcIdMP91KrC8/iPBKi8qU 43 | /Bij17d6kMo0+cGELjDZ7Ju2VMUKstD4es+iW3TJ/ptqX6IE8+l5aHc+RNqpwnFB 44 | bYunEdS8be0ff4FvlQ7bYWtH4SkZzhvqZ2CWj9LfDX2htXTvIMyEpbYiYUbG98ku 45 | TwqgQ6QW+9PuSeC7RIc+8tMRiEO5QlF+ybVX8l5uDnF1fsXru9rCmMwJ/1G5+s7T 46 | xxec2GANFfVN9RHccluYnnCmucOssiimhZ/TEDfqDkMjr+kU1/96g7oWRYCx3CG2 47 | iIpxUIACLJNPw6FIvu1vtzm8cfNTwYw6gslRhT3WLfFbg7VkRoM6g8nhCaxj6B61 48 | 3TfGoOHoLMK4MY/yJMmOIZbn1KbKrl/HjUg+jrfPOBwhkoLvOTw4pqC3xNRROSqM 49 | HuoiqjYCN5oibn1SAx/UfFIAoskTJGJL7GwOVCJ9V9bG3Tcnbj2yga6Mhu3YOX+m 50 | cZnW7ByjnL+/nAInanqXcwqjPvrcI1km1Qloy13zFOCh2Z62yXiS0KTqvV4REV77 51 | V6tuI4yaMlSOkroZGQeFKXnyKdceFIBZ8+uBrkBdsOoj3orhgi4bAdlsMjOV+Qdp 52 | v2Y4NmR9SOJ2jOUI8Lt0S8sf7wDfsC2JxCavMQHn+y9jh1CREP0oxGSRlq3t92p1 53 | cNiC9RmcFmWqY4njDPylmbEmHkwRrxMIxenrWlufC5XP5L2/sjs4s0hRy5CDQMVv 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /var/jwt/public.pem.dist: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwxghthNY28yQuPMXBDy+ 3 | 3hc803pN852HlBEFHoKN8roRN5HewGkw3MgwnXBIRXAblSmdH8xUQjST9ngGUe+o 4 | vlA1atc9cFYC1siISEbrlrb9srupnzCh5WKIOaoWyAT1fY0MjZgLKCF8Mfcgbskk 5 | 0Is8IHy2wKAQ1BuAbTO7AHDAxTd53+rXByDR0OqpcEhHZ3uAyVu+hT+OFq2SsNKJ 6 | LHsnFLj3zLntwHNOICLUk3l2qraoWZ/T76+HpPZe5YDGgnUdHuaCAUITjCd8VqHd 7 | 0eQKg7D7qpjEI9iGCNWxqVbIQ0t9u6qHqCsLEQlTRYaLyLw6DKfkTWgCNeAY3MDz 8 | VIVJ5DYcZYLhENJQRGwa8v0y/aMkrDfoxeHdcZLSgYprRux2YuttN2L/qqk88XOh 9 | IWp9Ft30+r9ZTDKzH/Tf6llwVHjKfFAFEhYr0I5f05297yldfXpFSTaT1nQZ2pR8 10 | DIUWZ1llHiiKGK3uymoPUqW99OV63FlUfoOYDUihZwheQm7PVUkreKd5n4kQTmEw 11 | Xz16ahk3xWqReM5JgSO6MGpItez5cxagW7WIWvGhAUPrb8n3yp6O/ztJcDwyn1ne 12 | 1zllp2gbircBPoSfzPPAlanwLZgEHpww79F3/q5646kBDvAQYCnQ5z5sLPkN7jb5 13 | IrumAJbGufbw7qiVQF0p3xsCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /var/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/var/logs/.gitkeep -------------------------------------------------------------------------------- /var/storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/var/storage/.gitkeep -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex app.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve 15 | # to the front controller "/app.php" but be rewritten to "/app.php/app". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the app.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/app.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule .? - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule .? %{ENV:BASE}/app.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 302 ^/$ /app.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /web/app.php: -------------------------------------------------------------------------------- 1 | unregister(); 18 | $apcLoader->register(true); 19 | */ 20 | 21 | $kernel = new AppKernel('prod', false); 22 | $kernel->loadClassCache(); 23 | //$kernel = new AppCache($kernel); 24 | 25 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter 26 | //Request::enableHttpMethodParameterOverride(); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /web/app_dev.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 30 | $request = Request::createFromGlobals(); 31 | $response = $kernel->handle($request); 32 | $response->send(); 33 | $kernel->terminate($request, $response); 34 | -------------------------------------------------------------------------------- /web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/apple-touch-icon.png -------------------------------------------------------------------------------- /web/assets/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /web/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /web/assets/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /web/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /web/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /web/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /web/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /web/assets/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/assets/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamluc/dunkerque/36662ef4d28dd61bb5cb03748861d46c5329438e/web/favicon.ico -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | Disallow: 6 | --------------------------------------------------------------------------------