commit 592f5579abfcbd7cb4b34e6f6d7b4f0f6fe9a46c Author: Christopher Vagnetoft Date: Tue Sep 27 12:29:56 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a6ec21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/*.phar diff --git a/README.md b/README.md new file mode 100644 index 0000000..7470f7c --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# ServerCtl - Services for Developers + + + + + +## Examples + +Say you need a Mongodb instance for something. Try this: + +``` +$ serverctl start mongo +Started mongo + Mongo client: 27017 +$ +``` + +What just happened? Well, the `start` command pulled and started a new container +from the latest docker image using sensible defaults from the service registry. + +Let's start another one: + +``` +$ serverctl start mongo --instance other --portoffset 1 +Started mongo + Mongo client: 27018 +$ +``` + +Check it out! + +``` +$ serverctl status +Service Instance Ports +mongo default 27017 + other 27018 +$ serverctl stop mongo --all +Stopped mongo +Stopped mongo +$ +``` + + +## FAQ + +### Where is the data stored? + +* Data goes in `$HOME/.var/serverctl` + diff --git a/bin/serverctl b/bin/serverctl new file mode 100755 index 0000000..c2fe626 --- /dev/null +++ b/bin/serverctl @@ -0,0 +1,9 @@ +#!/usr/bin/env php +run(); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0b688c2 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "noccylabs/serverctl", + "description": "Start and stop services for development", + "type": "application", + "license": "GPL-3.0-or-later", + "autoload": { + "psr-4": { + "NoccyLabs\\Serverctl\\": "src/" + } + }, + "authors": [ + { + "name": "Christopher Vagnetoft", + "email": "cvagnetoft@gmail.com" + } + ], + "require": { + "symfony/console": "^6.1", + "noccylabs/spinner": "^0.1.0" + }, + "bin": [ + "bin/serverctl" + ], + "extra": { + "phar": { + "output": "serverctl.phar" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d7c1f88 --- /dev/null +++ b/composer.lock @@ -0,0 +1,761 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "21d444d8fc7b7dc1b673054b1a48432a", + "packages": [ + { + "name": "noccylabs/spinner", + "version": "0.1.0", + "dist": { + "type": "zip", + "url": "https://dev.noccylabs.info/api/packages/noccy/composer/files/noccylabs%2Fspinner/0.1.0/noccylabs-spinner.0.1.0.zip", + "shasum": "5be298051ee740bcb7fa6aac0fa57ae3bbf0771e" + }, + "type": "library", + "autoload": { + "psr-4": { + "NoccyLabs\\Spinner\\": "src/" + } + }, + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Christopher Vagnetoft", + "email": "cvagnetoft@gmail.com" + } + ], + "description": "Indicate activity in console applications", + "time": "2022-09-22T00:11:39+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "symfony/console", + "version": "v6.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/7fccea8728aa2d431a6725b02b3ce759049fc84d", + "reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.1.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-08-26T10:32:31+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "433d05519ce6990bf3530fba6957499d327395c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", + "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-30T19:18:58+00:00" + }, + { + "name": "symfony/string", + "version": "v6.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "290972cad7b364e3befaa74ba0ec729800fb161c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/290972cad7b364e3befaa74ba0ec729800fb161c", + "reference": "290972cad7b364e3befaa74ba0ec729800fb161c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.1.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-08-12T18:05:43+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/registry/mariadb.json b/registry/mariadb.json new file mode 100644 index 0000000..eb45073 --- /dev/null +++ b/registry/mariadb.json @@ -0,0 +1,30 @@ +{ + "$type": "service", + "name": "mariadb", + "description": "MariaDB (MySQL fork)", + "author": null, + "image": "mariadb", + "ports": [ + { "port": 3306, "info": "MySQL client" } + ], + "environment": { + "MARIADB_ROOT_PASSWORD": "toor" + }, + "persistence": [ + { "path": "/data", "hint": "data" } + ], + "scripts": { + "mongosh": { + "info": "Open the mongo shell", + "execute": "mongosh" + }, + "export": { + "info": "Export the database", + "execute": "mysqldump " + }, + "import": { + "info": "Import the database", + "execute": "mongoimport" + } + } +} diff --git a/registry/mongo.json b/registry/mongo.json new file mode 100644 index 0000000..74bb229 --- /dev/null +++ b/registry/mongo.json @@ -0,0 +1,31 @@ +{ + "$type": "service", + "name": "mongo", + "description": "MongoDB", + "author": null, + "image": "mongo", + "ports": [ + { "port": 27017, "info": "Mongo client" } + ], + "persistence": [ + { "path": "/data/db", "hint": "data" } + ], + "environment": { + "MONGO_INITDB_ROOT_PASSWORD": "toor", + "MONGO_INITDB_ROOT_USERNAME": "root" + }, + "scripts": { + "shell": { + "info": "Open the mongo shell", + "execute": "mongosh" + }, + "export": { + "info": "Export the database", + "execute": "mongodump" + }, + "import": { + "info": "Import the database", + "execute": "mongoimport" + } + } +} diff --git a/registry/mysql.json b/registry/mysql.json new file mode 100644 index 0000000..7852831 --- /dev/null +++ b/registry/mysql.json @@ -0,0 +1,30 @@ +{ + "$type": "service", + "name": "mysql", + "description": "MySQL", + "author": null, + "image": "mysql", + "ports": [ + { "port": 3306, "info": "MySQL client" } + ], + "environment": { + "MARIADB_ROOT_PASSWORD": "toor" + }, + "persistence": [ + { "path": "/data", "hint": "data" } + ], + "scripts": { + "shell": { + "info": "Open the mongo shell", + "execute": "mysql -uroot -p%{MARIADB_ROOT_PASSWORD}" + }, + "export": { + "info": "Export the database", + "execute": "mysqldump -uroot -p%{MARIADB_ROOT_PASSWORD}" + }, + "import": { + "info": "Import the database", + "execute": "mysql -uroot -p%{MARIADB_ROOT_PASSWORD}" + } + } +} diff --git a/registry/postgres.json b/registry/postgres.json new file mode 100644 index 0000000..d831791 --- /dev/null +++ b/registry/postgres.json @@ -0,0 +1,30 @@ +{ + "$type": "service", + "name": "postgres", + "description": "Postgres SQL server", + "author": null, + "image": "postgres", + "ports": [ + { "port": 5432, "info": "Postgres client" } + ], + "environment": { + "POSTGRES_PASSWORD": "password" + }, + "persistence": [ + { "path": "/data", "hint": "data" } + ], + "scripts": { + "mongosh": { + "info": "Open a pg shell", + "execute": "psql --user postgres" + }, + "export": { + "info": "Export the database", + "execute": "mysqldump " + }, + "import": { + "info": "Import the database", + "execute": "mongoimport" + } + } +} diff --git a/src/Commands/Command.php b/src/Commands/Command.php new file mode 100644 index 0000000..ec6ad6d --- /dev/null +++ b/src/Commands/Command.php @@ -0,0 +1,26 @@ +getApplication()->getServiceRegistry(); + } + + protected function getContainerManager(): ContainerManager + { + return $this->getApplication()->getContainerManager(); + } +} \ No newline at end of file diff --git a/src/Commands/ExecCommand.php b/src/Commands/ExecCommand.php new file mode 100644 index 0000000..0538739 --- /dev/null +++ b/src/Commands/ExecCommand.php @@ -0,0 +1,51 @@ +addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default"); + $this->addOption("script", "s", InputOption::VALUE_NONE, "The command is a script (use 'list' for list)"); + $this->addArgument("service", InputArgument::REQUIRED, "The service name"); + $this->addArgument("execute", InputArgument::REQUIRED, "The command or script to execute"); + $this->addArgument("arguments", InputArgument::IS_ARRAY, "Arguments"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $serviceRegistry = $this->getServiceRegistry(); + $containerManager = $this->getContainerManager(); + $instanceName = $input->getOption("instance"); + + $serviceName = $input->getArgument("service"); + $command = $input->getArgument("execute"); + + $serviceInfo = $serviceRegistry->findServiceByName($serviceName); + + if ($command == "list") { + $scripts = (array)($serviceInfo['scripts']??[]); + $output->writeln("Available scripts:"); + foreach ($scripts as $script=>$meta) { + $output->writeln(sprintf(" %s - %s", $script, $meta['info']??"?")); + } + return self::SUCCESS; + } else { + $cmdl = [ $input->getArgument("execute") ]; + array_push($cmdl, ...$input->getArgument("arguments")); + $containerManager->execute($serviceInfo, $instanceName, $cmdl); + } + + return self::SUCCESS; + } + +} \ No newline at end of file diff --git a/src/Commands/FindCommand.php b/src/Commands/FindCommand.php new file mode 100644 index 0000000..11538bf --- /dev/null +++ b/src/Commands/FindCommand.php @@ -0,0 +1,33 @@ +addArgument("service", InputArgument::OPTIONAL, "Search query"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $serviceRegistry = $this->getServiceRegistry(); + + $services = $serviceRegistry->findAllServices(); + + $output->writeln("Available services:"); + foreach ($services as $service) { + $output->writeln(sprintf(" %s - %s", $service['name'], $service['description']??"?")); + } + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/StartCommand.php b/src/Commands/StartCommand.php new file mode 100644 index 0000000..b8f9482 --- /dev/null +++ b/src/Commands/StartCommand.php @@ -0,0 +1,45 @@ +addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default"); + $this->addOption("portoffset", "p", InputOption::VALUE_REQUIRED, "Offset port numbers by value", 0); + $this->addArgument("service", InputArgument::REQUIRED, "The service name"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $serviceRegistry = $this->getServiceRegistry(); + $containerManager = $this->getContainerManager(); + + + $serviceName = $input->getArgument("service"); + + $serviceInfo = $serviceRegistry->findServiceByName($serviceName); + if (!$serviceInfo) { + $output->writeln("No such service in registry"); + return self::FAILURE; + } + + $options = [ + 'name' => $input->getOption("instance"), + 'portoffset' => $input->getOption("portoffset") + ]; + + $containerManager->startService($serviceInfo, $options); + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/StatusCommand.php b/src/Commands/StatusCommand.php new file mode 100644 index 0000000..b89210e --- /dev/null +++ b/src/Commands/StatusCommand.php @@ -0,0 +1,34 @@ +write(" Getting status\r"); + for ($n = 0; $n < 250; $n++) { + if ($output->isDecorated()) $output->write("({$spinner})\r"); + usleep(10000); + } + + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Commands/StopCommand.php b/src/Commands/StopCommand.php new file mode 100644 index 0000000..64382cd --- /dev/null +++ b/src/Commands/StopCommand.php @@ -0,0 +1,39 @@ +addOption("instance", "I", InputOption::VALUE_REQUIRED, "Specify the instance name", "default"); + $this->addArgument("service", InputArgument::REQUIRED, "The service name"); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $serviceRegistry = $this->getServiceRegistry(); + $containerManager = $this->getContainerManager(); + + + $serviceName = $input->getArgument("service"); + + $serviceInfo = $serviceRegistry->findServiceByName($serviceName); + if (!$serviceInfo) { + $output->writeln("No such service in registry"); + return self::FAILURE; + } + + $containerManager->stopService($serviceInfo, $input->getOption("instance")); + + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/src/ConsoleApplication.php b/src/ConsoleApplication.php new file mode 100644 index 0000000..6c98e51 --- /dev/null +++ b/src/ConsoleApplication.php @@ -0,0 +1,48 @@ +serviceRegistry = new ServiceRegistry( + paths: [ + __DIR__."/../registry", + dirname(realpath($GLOBALS['argv'][0]))."/registry", + "/usr/share/serverctl/registry", + getenv("HOME")."/.share/serverctl/registry" + ] + ); + $this->containerManager = new ContainerManager( + dataPath: null + ); + + $this->add(new Commands\StartCommand()); + $this->add(new Commands\StopCommand()); + $this->add(new Commands\ExecCommand()); + $this->add(new Commands\FindCommand()); + $this->add(new Commands\StatusCommand()); + } + + public function getServiceRegistry(): ServiceRegistry + { + return $this->serviceRegistry; + } + + public function getContainerManager(): ContainerManager + { + return $this->containerManager; + } +} \ No newline at end of file diff --git a/src/Container/ContainerManager.php b/src/Container/ContainerManager.php new file mode 100644 index 0000000..ff773b8 --- /dev/null +++ b/src/Container/ContainerManager.php @@ -0,0 +1,132 @@ +dataPath = $dataPath ?? (getenv("HOME")."/.var/serverctl"); + } + + /** + * Start a service + * + * Instance options: + * name: Instance name (default) + * portoffset: Port number offset (0) + * + * @param array $service The service definition from the registry + * @param array $options Instance options + */ + public function startService(array $service, array $options) + { + $args = []; + + $serviceName = $service['name']; + $instanceName = $options['name']??'default'; + + $containerName = "sm_".$serviceName."_".$instanceName; + + $args[] = 'run'; + $args[] = '--rm'; // remove container after run + $args[] = '-d'; + $args[] = '--name'; + $args[] = $containerName; + + // Map the ports + $ports = (array)($service['ports']??[]); + foreach ($ports as $port) { + $args[] = '-p'; + $args[] = $port['port']; + } + + // Get the paths to persist + $volumes = (array)($service['persistence']??[]); + $volumePath = $this->getServiceDataPath($service)."/".$instanceName; + foreach ($volumes as $volume) { + // volume { path, hint } + $path = $volume['path']; + $hint = $volume['hint'] ?? crc32($path); + $args[] = '-v'; // add volume + $args[] = $volumePath."/".$hint.":".$path; + } + + // Get environment + $envs = (array)($service['environment']??[]); + foreach ($envs as $env=>$value) { + $args[] = '-e'; + // TODO: use environment if set (override) + $args[] = sprintf("%s=%s", $env, $value); + } + + $args[] = $service['image']; + + $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); + + // TODO: Write command line, env and meta to state file + + echo "$ {$cmdl}\n"; + passthru($cmdl); + + } + + /** + * Stop a service + */ + public function stopService(array $service, string $instanceName) + { + $args = []; + + $serviceName = $service['name']; + $instanceName = $options['name']??'default'; + $containerName = "sm_".$serviceName."_".$instanceName; + + $args[] = 'stop'; + $args[] = $containerName; + + $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); + + echo "$ {$cmdl}\n"; + passthru($cmdl); + + } + + public function execute(array $service, string $instanceName, array $command) + { + $args = []; + + $serviceName = $service['name']; + $instanceName = $options['name']??'default'; + $containerName = "sm_".$serviceName."_".$instanceName; + + $args[] = 'exec'; + $args[] = '-it'; + $args[] = $containerName; + + array_push($args, ...$command); + + $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); + + echo "$ {$cmdl}\n"; + passthru($cmdl); + + } + + /** + * Get running services + */ + public function getRunningServices(): array + { + return []; + } + + public function getServiceDataPath(array $service) + { + return $this->dataPath."/".$service['name']; + } + +} \ No newline at end of file diff --git a/src/Registry/ServiceRegistry.php b/src/Registry/ServiceRegistry.php new file mode 100644 index 0000000..389cb11 --- /dev/null +++ b/src/Registry/ServiceRegistry.php @@ -0,0 +1,57 @@ +readPath($path); + } + } + } + + private function readPath(string $path) + { + $iter = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $path + ) + ); + + /** @var SplFileInfo $iteminfo */ + foreach ($iter as $itempath=>$iteminfo) { + if (!fnmatch("*.json", $itempath)) continue; + $json = file_get_contents($itempath); + $parsed = json_decode($json, true); + $this->services[$itempath] = $parsed; + } + + usort($this->services, function ($a,$b) { + return $a['name'] <=> $b['name']; + }); + } + + public function findServiceByName(string $name): ?array + { + foreach ($this->services as $service) { + if ($service['name'] === $name) return $service; + } + return null; + } + + public function findAllServices(): array + { + return $this->services; + } + +} \ No newline at end of file