Added plugins and build tools

This commit is contained in:
2021-12-09 00:58:28 +01:00
parent a0d68a606c
commit eefe53a438
46 changed files with 4049 additions and 1 deletions

4
plugins/README.md Normal file
View File

@ -0,0 +1,4 @@
# Plugins
Install by copying or symlinking into your `.spark/plugins` directory, or whatever
directory you have defined to preload from.

View File

@ -0,0 +1,24 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerBuildCommand extends Command
{
protected function configure()
{
$this->setName("docker:build")
->setDescription("Build an image");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = read_config("docker.json");
print_r($config);
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace SparkPlug\Com\Noccy\Docker\DockerCompose;
class Service
{
private array $service;
private array $environment = [];
private Stack|null $stack;
public function __construct(array $service, ?Stack $stack)
{
$this->service = $service;
$this->stack = $stack;
foreach ($this->service['environment']??[] as $k=>$v) {
if (is_numeric($k)) {
[$k, $v] = explode("=", $v, 2);
}
$this->environment[$k] = $v;
}
}
public function getImage():?String
{
return $this->service['image']??null;
}
public function getEnvironment(): array
{
return $this->environment;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace SparkPlug\Com\Noccy\Docker\DockerCompose;
use Symfony\Component\Yaml\Yaml;
class Stack
{
private array $compose;
private string $version;
private array $services;
public function __construct(string $filename)
{
$this->compose = Yaml::parseFile($filename);
$this->version = $this->compose['version']??null;
$this->enumServices();
}
private function enumServices()
{
foreach ($this->compose['services'] as $service=>$config) {
$this->services[$service] = new Service($config, $this);
}
}
public function getServiceNames(): array
{
return array_keys($this->services);
}
public function getService(string $name): ?Service
{
return $this->services[$name] ?? null;
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class DockerDbExportCommand extends Command
{
protected function configure()
{
$this->setName("docker:db:export")
->setDescription("Export a database")
->addOption("mysql", null, InputOption::VALUE_NONE, "Export from a MySQL database")
->addOption("dsn", null, InputOption::VALUE_REQUIRED, "Database DSN")
->addOption("service", null, InputOption::VALUE_REQUIRED, "Service name in stack")
->addOption("database", null, InputOption::VALUE_REQUIRED, "Database name")
->addOption("output", "o", InputOption::VALUE_REQUIRED, "Output to file instead of stdout")
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$docker = get_plugin("com.noccy.docker");
$service = $input->getOption("service");
$dsn = $input->getOption("dsn");
if ($service) {
$stack = $docker->getComposeStack();
if ($input->getOption("mysql")) {
$dbtype = 'mysql';
} else {
$services = $stack->getServiceNames();
if (!in_array($service, $services)) {
$output->writeln("<error>Invalid service {$service}. Valid are ".join(", ", $services)."</>");
return Command::INVALID;
}
$image = $stack->getService($service)->getImage();
if (preg_match('/(mysql|mariadb)/i', $image)) {
$dbtype = 'mysql';
$env = $stack->getService($service)->getEnvironment();
$database = $env['MYSQL_DATABASE']??null;
if ($dbpass = $env['MYSQL_ROOT_PASSWORD']??null) {
$dbuser = 'root';
} else {
$dbuser = $env['MYSQL_USER']??null;
$dbpass = $env['MYSQL_PASSWORD']??null;
}
} else {
$output->writeln("<error>Unable to determine database type from service</>");
return Command::INVALID;
}
}
$database = $database ?? $input->getOption("database");
if (empty($database)) {
$output->writeln("<error>No --database specified</>");
return Command::INVALID;
}
switch ($dbtype) {
case 'mysql':
$cmd = sprintf("mysqldump -u%s -p%s %s", $dbuser, $dbpass, $database);
break;
}
$this->exportFromService($service, $cmd, $output);
} elseif ($dsn) {
$url = parse_url($dsn);
if (empty($url)) {
$output->writeln("<error>Bad database DSN {$dsn}. Should look like mysql://user:pass@host:port/database</>");
return Command::INVALID;
}
}
return Command::SUCCESS;
}
private function exportFromService(string $service, string $command, OutputInterface $output)
{
$cmd = sprintf("docker-compose exec -T %s %s", $service, $command);
passthru($cmd);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerDownCommand extends Command
{
protected function configure()
{
$this->setName("docker:down")
->setDescription("Stop a stack or container");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = read_config("docker.json");
print_r($config);
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerExecCommand extends Command
{
protected function configure()
{
$this->setName("docker:exec")
->setDescription("Execute scripts in a docker container");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = read_config("docker.json");
print_r($config);
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerStatusCommand extends Command
{
const BULLET="\u{25cf}";
protected function configure()
{
$this->setName("docker:status")
->setDescription("Show docker status");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
exec("docker-compose ps -q", $ids, $ret);
if (count($ids) === 0) return Command::SUCCESS;
exec("docker inspect ".join(" ",$ids), $out, $ret);
$json = json_decode(join("", $out));
$stack = get_plugin('com.noccy.docker')->getComposeStack();
$table = new Table($output);
$table->setStyle("box");
$table->setHeaders([ "Name", "Status", "Image", "Ports" ]);
foreach ($json as $container) {
$startedTs = preg_replace('/(\.([0-9]+)Z)$/', '+0100', $container->State->StartedAt);
$s = date_parse($startedTs);
$started = mktime($s['hour'], $s['minute'], $s['second'], $s['month'], $s['day'], $s['year']) + 3600;
if ($container->State->Dead) {
$status = "<fg=red>".self::BULLET."</> ".$container->State->Status;
} elseif ($container->State->Restarting) {
$status = "<fg=yellow>".self::BULLET."</> ".$container->State->Status;
} elseif ($container->State->Running) {
$elapsed = time() - $started;
if ($elapsed > 60) {
$em = floor($elapsed / 60);
$es = $elapsed - ($em * 60);
if ($em>60) {
$eh = floor($em / 60);
$em = $em - ($eh * 60);
$elapsed = sprintf("%dh%dm%ds", $eh, $em, $es);
} else {
$elapsed = sprintf("%dm%ds", $em, $es);
}
} else {
$elapsed = sprintf("%ds", $elapsed);
}
$status = "<fg=green>".self::BULLET."</> ".$container->State->Status." (<fg=green>{$elapsed}</>)";
} else {
$status = "<fg=red>".self::BULLET."</> ".$container->State->Status;
}
$ports = $container->Config->ExposedPorts??[];
$ports = array_keys((array)$ports);
$table->addRow([ $container->Name, $status, $container->Config->Image, join(", ", $ports) ]);
}
$table->render();
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerUpCommand extends Command
{
protected function configure()
{
$this->setName("docker:up")
->setDescription("Start a stack or container");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$config = read_config("docker.json");
print_r($config);
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,54 @@
<?php // "name":"Docker plugin for SparkPlug", "author":"Noccy"
namespace SparkPlug\Com\Noccy\Docker;
use Spark\Commands\Command;
use SparkPlug;
use SparkPlug\Com\Noccy\Docker\DockerCompose\Stack;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DockerPlug extends SparkPlug
{
private array $compose = [];
private Stack $composeStack;
public function load()
{
$config = read_config("docker.json");
$docker = $config['docker']??[];
$hasCompose = array_key_exists('compose', $docker);
$hasBuild = array_key_exists('build', $docker);
if ($hasCompose) {
$this->compose = $docker['compose'];
}
if ($hasCompose || $hasBuild) {
register_command(new DockerUpCommand);
register_command(new DockerDownCommand);
register_command(new DockerStatusCommand);
}
if ($hasBuild) {
register_command(new DockerBuildCommand);
register_command(new DockerExecCommand);
}
register_command(new DockerDbExportCommand);
}
public function getComposeStack(): ?Stack
{
$base = $this->getProjectDirectory();
if (empty($this->composeStack)) {
$composeFile = $base . "/" . ($this->compose['file']??'docker-compose.yml');
$this->composeStack = new Stack($composeFile);
}
return $this->composeStack;
}
}
//if (file_exists(get_environment()->getConfigDirectory()."/maker.json")) {
register_plugin("com.noccy.docker", new DockerPlug());
//}

View File

@ -0,0 +1,73 @@
<?php
namespace SparkPlug\Com\Noccy\Git;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class GitIgnoreCommand extends Command
{
protected function configure()
{
$this->setName("git:ignore")
->setDescription("List, add or remove paths from gits ignorelists")
->addOption("local","l",InputOption::VALUE_NONE,"Use the local ignore rather than .gitignore")
->addOption("add","a",InputOption::VALUE_NONE,"Add a pattern")
->addOption("remove","r",InputOption::VALUE_NONE,"Attempt to remove a pattern")
->addArgument("pattern", InputArgument::OPTIONAL, "Pattern to add or remove")
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$local = $input->getOption("local");
$root = $this->getEnvironment()->getProjectDirectory();
$file = $root . (!$local ? "/.gitignore" : "/.git/info/exclude");
$pattern = $input->getArgument("pattern");
if (empty($pattern)) {
if (file_exists($file)) {
$ignores = file($file, FILE_IGNORE_NEW_LINES);
foreach ($ignores as $ignore) {
if (str_starts_with(trim($ignore),'#')) {
$output->writeln("<fg=green>".$ignore."</>");
} else {
$output->writeln("<fg=white>".$ignore."</>");
}
}
return Command::SUCCESS;
}
$output->writeln("<info>Empty list</>");
return Command::SUCCESS;
} elseif ($input->getOption("add") && $pattern) {
if (file_exists($file)) {
$ignores = file($file, FILE_IGNORE_NEW_LINES);
} else {
$ignores = [];
}
array_push($ignores, $pattern);
file_put_contents($file, join("\n", $ignores));
$output->writeln("<info>Updated {$file}</>");
return Command::SUCCESS;
} elseif ($input->getOption("remove") && $pattern) {
if (file_exists($file)) {
$ignores = file($file, FILE_IGNORE_NEW_LINES);
$ignores = array_filter($ignores, function ($v) use ($pattern) {
return $v != $pattern;
});
$output->writeln("<info>Updated {$file}</>");
file_put_contents($file, join("\n", $ignores));
return Command::SUCCESS;
}
$output->writeln("<info>Not updating non-existing file {$file}</>");
return Command::SUCCESS;
}
$output->writeln("<error>Expected no pattern, --add pattern or --remove pattern</>");
return Command::INVALID;
}
}

View File

@ -0,0 +1,25 @@
<?php // "name":"Git plugin for SparkPlug", "author":"Noccy"
namespace SparkPlug\Com\Noccy\Git;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GitPlug extends SparkPlug
{
public function load()
{
$root = $this->getProjectDirectory();
if (!file_exists($root."/.git")) {
return;
}
register_command(new GitIgnoreCommand());
}
}
//if (file_exists(get_environment()->getConfigDirectory()."/maker.json")) {
register_plugin("com.noccy.git", new GitPlug());
//}

View File

@ -0,0 +1,42 @@
<?php // "name":"Build stuff from stuff", "author":"Noccy"
namespace SparkPlug\Com\Noccy\Maker;
use Spark\Commands\Command;
use SparkPlug;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakerPlug extends SparkPlug
{
public function load()
{
$config = json_decode(file_get_contents(get_environment()->getConfigDirectory()."/maker.json"), true);
foreach ($config as $rule=>$info) {
if (str_starts_with($rule, '@')) {
$rule = substr($rule, 1);
register_command(new class($rule) extends Command {
private $rule;
public function __construct($rule)
{
$this->rule = $rule;
parent::__construct();
}
protected function configure()
{
$this->setName("make:{$this->rule}");
$this->setDescription("Run the {$this->rule} maker task");
}
protected function execute(InputInterface $input, OutputInterface $output)
{
}
});
}
}
}
}
if (file_exists(get_environment()->getConfigDirectory()."/maker.json")) {
register_plugin("com.noccy.maker", new MakerPlug());
}

View File

@ -0,0 +1,36 @@
<?php
namespace SparkPlug\Com\Noccy\Pdo;
use Spark\Commands\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class PdoExecCommand extends Command {
protected function execute(InputInterface $input, OutputInterface $output)
{
$source = $input->getOption("res");
$sourcePdo = get_resource($source)->getPDO();
if (!$sourcePdo) {
$output->writeln("<error>Invalid resource: {$source}</>");
return Command::INVALID;
}
$query = $input->getArgument('query');
$stmt = $sourcePdo->prepare($query);
$stmt->execute();
return Command::SUCCESS;
}
protected function configure() {
$this->setName("pdo:exec");
$this->setDescription("Run a query without returning data");
$this->addOption("res", "r", InputOption::VALUE_REQUIRED, "Resource to query", "db");
$this->addArgument("query", InputArgument::REQUIRED, "SQL query to execute");
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace SparkPlug\Com\Noccy\Pdo;
use Spark\Commands\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class PdoQueryCommand extends Command {
protected function execute(InputInterface $input, OutputInterface $output)
{
$source = $input->getOption("res");
$sourcePdo = get_resource($source)->getPDO();
if (!$sourcePdo) {
$output->writeln("<error>Invalid resource: {$source}</>");
return Command::INVALID;
}
$box = $input->getOption('box');
$query = $input->getArgument('query');
$vert = $input->getOption("vertical");
$unserialize = $input->getOption("unserialize");
$stmt = $sourcePdo->query($query);
$stmt->execute();
$table = new Table($output);
$table->setStyle($box?"box":"compact");
$hasColumns = false;
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if (!$hasColumns) {
if ($vert) {
$table->setHeaders([ "Field", "VarType", "Value" ]);
} else {
$table->setHeaders(array_keys($row));
}
$hasColumns = true;
} else {
if ($vert) {
if ($box) {
$table->addRow(new TableSeparator());
} else {
$table->addRow(["","","-----"]);
}
}
}
if ($vert) {
foreach ($row as $k=>$v) {
$vv = $v;
if ($unserialize) {
$j = @json_decode($v);
$p = @unserialize($v);
if ($j) {
$v = $j;
$vv = json_encode($v, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
} elseif ($p) {
$v = $p;
$vv = json_encode($p, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
}
$table->addRow([ $k, gettype($v), $vv ]);
}
} else {
$table->addRow($row);
}
}
$table->render();
return Command::SUCCESS;
}
protected function configure() {
$this->setName("pdo:query");
$this->setDescription("Run a query against a defined PDO connection");
$this->addOption("res", "r", InputOption::VALUE_REQUIRED, "Resource to query", "db");
$this->addOption("vertical", "l", InputOption::VALUE_NONE, "Print result as rows instead of columns");
$this->addOption("box", null, InputOption::VALUE_NONE, "Use boxed table");
$this->addOption("unserialize", "u", InputOption::VALUE_NONE, "Attempt to unserialize serialized data");
$this->addArgument("query", InputArgument::REQUIRED, "SQL query to execute");
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace SparkPlug\Com\Noccy\Pdo;
use Spark\Resource\ResourceType;
use PDO;
class PdoResource extends ResourceType
{
private PDO|null $pdo = null;
private array $options;
public function __construct(array $options)
{
$this->options = $options;
}
private function createFromURI(string $uri)
{
$uris = parse_url($uri);
$username = $uris['user']??null;
$password = $uris['pass']??null;
switch ($uris['scheme']??null) {
case 'mysql':
$database = ltrim($uris['path']??null, '/');
$dsn = sprintf("mysql:host=%s;port=%d;dbname=%s", $uris['host']??'127.0.0.1', $uris['port']??3306, $database);
break;
case 'sqlite':
$database = $uris['path']??':memory:';
$dsn = sprintf("sqlite:%s", $database);
break;
default:
fprintf(STDERR, "error: Unable to create PDO resource from URI, invalid type %s\n", $uris['scheme']??null);
return;
}
$this->pdo = new \PDO($dsn, $username, $password);
}
public function getPDO(): ?PDO
{
if (!$this->pdo) {
$this->createFromURI($this->options['uri']);
}
return $this->pdo;
}
public function info()
{
return $this->options['uri'];
}
public function createTable(string $name, array $columns, bool $ifNotExists=false)
{
}
}

View File

@ -0,0 +1,16 @@
<?php // "name":"Access databases through PDO", "author":"Noccy"
namespace SparkPlug\Com\Noccy\Pdo;
use SparkPlug;
class PdoPlugin extends SparkPlug {
public function load()
{
register_command(new PdoQueryCommand());
register_command(new PdoExecCommand());
}
}
register_plugin("com.noccy.pdo", new PdoPlugin);
register_resource_type("pdo", PdoResource::class);