aes3xs/tasker

任务自动化工具

dev-master 2018-02-25 12:48 UTC

This package is not auto-updated.

Last update: 2024-09-29 04:49:37 UTC


README

Tasker是一个自动化工具,用于编写小型和简单的部署脚本。
它适合没有CI集成的单服务器项目。
如果您需要更多功能,请查看Jenkins、TeamCity和其他工具。它并不试图取代它们。Tasker受到Deployer项目的启发,您也可以查看它。Tasker旨在更加灵活(可覆盖)和面向对象,但功能较少。

安装

通过composer安装
composer require aes3xs/tasker

如何部署

一种好的方法是分割部署为本地和远程部分。

本地部分准备代码、预热文件缓存、下载依赖项和其他操作,这些操作会影响项目目录内的所有内容。
没有外部依赖、服务或文件(除依赖项管理和PHP本身)。
没有数据库请求,没有迁移。
此脚本可以在任何地方执行,在构建服务器或本地机器上。它创建一个准备就绪的部署模块,可以直接在生产环境中复制并运行。
在项目中拥有这样的脚本非常有用,它不应有依赖关系,如composer。因为如果从仓库有裸项目,它负责运行。
其中一些步骤有时在composer.json脚本部分中定义。但我更喜欢有独立的文件。

以下是一个示例(./bin/deploy)

#!/bin/bash  
 
set -e
set -o pipefail
 
SYMFONY_ENV=${SYMFONY_ENV:="dev"}
SYMFONY_DEBUG=${SYMFONY_DEBUG:="1"}
for i in "$@"; do case $i in -e=*|--env=*) SYMFONY_ENV="${i#*=}"; shift;; --no-debug) SYMFONY_DEBUG="0"; shift;; *);; esac done
 
ROOT="$(dirname "$(dirname "$(readlink -fm "$0")")")"
PHP=$(which "php") || { echo "PHP is not found" ; exit 1; }
YARN=$(which "yarn") || { echo "Yarn is not found" ; exit 1; }
COMPOSER=$(which "composer") || { echo "Composer is not found" ; exit 1; }
if [ "$SYMFONY_DEBUG" == "0" ]; then NO_DEBUG="--no-debug"; fi
CONSOLE="${ROOT}/bin/console --quiet --env=${SYMFONY_ENV} ${NO_DEBUG}"
 
echo "SYMFONY_ENV   = ${SYMFONY_ENV}"
echo "SYMFONY_DEBUG = ${SYMFONY_DEBUG}"
echo "PROJECT_ROOT  = ${ROOT}"
echo "PHP           = ${PHP}"
echo "YARN          = ${YARN}"
echo "COMPOSER      = ${COMPOSER}"
echo "CONSOLE       = ${CONSOLE}"
echo ""
 
_exec () {
   echo -e "\033[1m[$(date +%T)] >\033[0m" $1
   eval $1
}
 
# Vendors
_exec "cd ${ROOT} && ${COMPOSER} install --prefer-dist --no-progress --no-interaction --optimize-autoloader"
_exec "cd ${ROOT} && ${YARN} install --prod --non-interactive"
 
# Cache
_exec "rm -rf ${ROOT}/var/cache/${SYMFONY_ENV}"
_exec "chmod 0775 ${ROOT}/var/cache"
_exec "${CONSOLE} cache:warmup"
 
# Assets
PATHS=(
"${ROOT}/web/js"
"${ROOT}/web/css"
"${ROOT}/web/bundles"
)
_exec "rm -rf ${PATHS[*]}"
_exec "${CONSOLE} assets:install ${ROOT}/web --symlink --relative"
_exec "${CONSOLE} assetic:dump ${ROOT}/web"
 
# Writable
_exec "if [ ! -w ${ROOT}/var/cache ]; then { echo 'Is not writable' ; exit 1; }; fi"
_exec "if [ ! -w ${ROOT}/var/logs ]; then { echo 'Is not writable' ; exit 1; }; fi"
_exec "if [ ! -w ${ROOT}/var/spool ]; then { echo 'Is not writable' ; exit 1; }; fi"

您也可以使用Tasker编写此脚本,但我建议保留独立的shell脚本。

部署过程的远程部分与外部服务(如nginx、php-fpm、数据库)一起工作。
您还必须能够访问项目仓库以克隆它。

这里有几点重要事项。

首先,以其他用户执行命令。
出于安全原因,服务器上的每个人都必须有自己的凭据。
但项目本身配置为从一个用户运行,例如,www-data
因此,您必须在每个调用中添加类似sudo -u USER的内容,这已经实现了。

其次,在服务器上克隆仓库的认证。
GitLab有登录/密码选项或公钥,例如。
因此,更好的方法是使用SSH密钥,并且相同的密钥用于认证GitLab或其他系统。
这可以通过SSH转发认证来完成。

部署脚本示例

#!/usr/bin/env php
<?php
  
require_once "./vendor/autoload.php";  
  
use Aes3xs\Tasker\Connection\Connection;
use Aes3xs\Tasker\Service\Git;
use Aes3xs\Tasker\Service\Releaser;
use Aes3xs\Tasker\Service\Shell;
use Aes3xs\Tasker\Service\Symfony;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
  
class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    public $repository = 'REPOSITORY_PATH';
    public $deployPath = '/var/www/PROJECT';
    public $runUser = 'www-data';
    public $branch = 'master';

    protected function configure()
    {
        $this
            ->addArgument('server', InputArgument::REQUIRED)
            ->addOption('branch', 'b', InputOption::VALUE_OPTIONAL);
    }

    public function execute($server, Connection $connection)
    {
        if (!in_array($server, ['prod', 'dev'])) {
            throw new \RuntimeException('Server available values: prod, dev');
        }

        $connection
            ->getParameters()
            ->setHost(null)
            ->setForwarding(true); // Configure to deploy on different servers

        $this->branch = 'master'; // Set branch if needed

        try {
            $this->runActions(array_filter($this->getAvailableActions(), function ($actionName) {
                return !in_array($actionName, ['execute', 'failNotify']);
            }), 'Deploy');
        } catch (\Exception $e) {
            $this->runActions(['failNotify'], 'Failback');
        }
    }
    
    public function switchUser(Shell $shell, $runUser)
    {
        $shell->setUser($runUser);

        return $runUser;
    }
    
    public function createRelease(Releaser $releaser, Git $git, Shell $shell, $deployPath, $repository, $branch)
    {
        $releaser->setDeployPath($deployPath);
        $releaser->prepare();

        if ($releaser->isLocked() && $this->askConfirmationQuestion('Deploy is locked. Unlock?')) {
            $releaser->unlock();
        }
        $releaser->lock();

        $releaser->create();
        $releasePath = $releaser->getReleasePath();

        $releases = $releaser->getReleaseList();
        $reference = $releases ? $releaser->getReleasePathByName(reset($releases)) : null;

        $shell->setCwd($releasePath);

        $git->cloneAt($repository, $releasePath, $branch, $reference); // Uses SSH forwarding if presented

        $releaser->updateReleaseShares(['var/logs', 'var/spool', 'var/sessions'], ['app/config/parameters.yml']);

        $shell->chmod('./bin', 0777);
        $shell->exec('./bin/deploy --env=prod --no-debug');
    }
    
    public function migrate(Symfony $symfony)
    {
        $symfony->runCommand('doctrine:migrations:migrate', [], ['allow-no-migration']);
    }

    public function release(Releaser $releaser, Git $git, Shell $shell, $server, $branch)
    {
        $releaser->release();
        $releaser->unlock();

        $last_commits = $git->log($releaser->getReleasePath(), 3);

        $this->info(<<<EOL
Server: {{ server }}
Released: {{ releaser.getReleaseName() }}
Branch: {{ server }}
Last commits:
$last_commits
EOL
        );

        $releaser->cleanup(5);

        $shell->setUser(null);

        $shell->exec("sudo service nginx reload");
        $shell->exec("sudo service php-fpm reload");
    }

    public function shutdownRoutine(Shell $shell)
    {
        $shell->setUser(null);
    }

    public function failNotify()
    {
        $this->error(<<<EOL
Server: {{ server }}
Release: {{ releaser.getReleaseName() }}
Branch: {{ server }}
FALURE
EOL
        );
    }
}

\TestRecipe::run();

概述

Tasker旨在使用一系列操作创建PHP二进制文件。
操作可以在本地或远程服务器上执行。
它们位于"recipe"类中。
Recipe是一个自执行的命令行命令。

#!/usr/bin/env php
<?php  
  
require_once "./vendor/autoload.php";
  
class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    public function execute()
    {
        // Do smth
    }
}
  
\TestRecipe::run();

使用第一行#!/usr/bin/env php使您的文件可执行
添加权限 chmod a+x ./testRecipe
然后添加从composer的自动加载require_once "./vendor/autoload.php";,确保路径正确。
定义您自己的recipe类,从\Aes3xs\Tasker\AbstractRecipe扩展它
添加execute(默认)或其他将首先执行的方法。
调用静态::run()::run('yourMethodName')

操作

在操作中,您可以实际完成工作。
操作是公开的非静态方法。
魔法方法(__*)或以get[A-Z](例如getSmth)开头的方法不能执行。
使用runActions(['prepare', 'release'])runAction('release')调用操作
您可以使用getAvailableActions()获取recipe中所有可用操作的列表
如果您想在执行过程中跳过操作,请调用skipAction('only in production'),就像PhpUnit的$this->markTestSkipped()

#!/usr/bin/env php
<?php  
  
require_once "./vendor/autoload.php";
  
class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    public function execute()
    {
        $this->runActions($this->getAvailableActions(), 'deployment');
    }
    
    public function prepare()
    {
    }
    
    public function deploy($env)
    {
        if ('prod' === $env) {
            $this->runAction('restart');
        }
    }
    
    public function restart()
    {
    }
    
    public function notify($env)
    {
        if ('prod' !== $env) {
             $this->skipAction('Production only');
        }
    }
}
  
\TestRecipe::run();

命令

Recipe是一个带有参数和选项的命令。
它基于Symfony Console组件。
您可以使用 configure() 来定义可用的输入。
同样的方法 addArgument()addOption(),您可以使用所有 Symfony 默认选项。

  • 使用 --help 显示命令信息。
  • 使用 -v-vv-vvv 来使输出更详细。
  • 使用 -q 禁用输出,除了错误。
  • 使用 -n 禁用用户输入(非交互模式)。
<?php
  
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 TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    protected function configure()
    {
        $this
            ->addArgument('argument', InputArgument::REQUIRED)
            ->addOption('option', null, InputOption::VALUE_REQUIRED);
    }
  
    public function doSmthAction(InputInterface $input, $argument, $option)
    {
        $argumentValue = $input->getArgument('argument');
        $optionValue = $input->getArgument('option');
        $argumentValue = $argument; // same
        $optionValue = $option; // same
    }
    
    public function doSmth2Action(OutputInterface $output)
    {
        $output->writeln('Hello');
    }
}

您可以通过名称访问定义的输入。

有一些辅助方法用于用户交互。
要获取布尔结果 $this->askConfirmationQuestion()
要从数组选项中选择 $this->askChoiceQuestion()(它返回值,而不是键)。
要获取字符串输入 $this->askQuestion()

<?php
  
class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    public function askSmthAction()
    {
        $result = $this->askConfirmationQuestion('Are you sure?', false);
        
        $result = $this->askChoiceQuestion('Pick a color', ['red', 'green', 'blue'], 'green');
        
        $result = $this->askQuestion('Enter your name', 'anonymous');
    }
}

您可以实现自己的问题或覆盖这些方法。

自动注入资源。

资源是操作的参数。
您可以使用资源名称或类名称将现有资源连接到操作调用。
类和名称可以指向不同的资源,因此结果可能是不可预测的。请避免这些情况。

资源的优先级顺序为:

  • 配方中的 get[A-Z] 方法,某些类型的动态属性。
  • 公共非静态配方属性。
  • 输入参数。
  • 输入选项。
  • 容器服务(内部)。
  • 容器参数(内部)。

如果有具有相同名称或类名称的资源,则将使用第一个出现。
Snake_case 和 camelCase 视为相同。

可用的容器服务。

  • 输入(Symfony\Component\Console\Input\InputInterface)。
  • 输出(Symfony\Component\Console\Output\OutputInterface)。
  • 样式(Symfony\Component\Console\Style\SymfonyStyle)。
  • 连接(Aes3xs\Tasker\Connection\Connection)。
  • 记录器(Monolog\Logger)。
  • 运行器(Aes3xs\Tasker\Runner\Runner)。

还有一些辅助工具。

  • shell(Aes3xs\Tasker\Service\Shell)。
  • composer(Aes3xs\Tasker\Service\Composer)。
  • git(Aes3xs\Tasker\Service\Git)。
  • 发布者(Aes3xs\Tasker\Service\Releaser)。
  • symfony(Aes3xs\Tasker\Service\Symfony)。
<?php

class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    /**
     * Can be obtained by:
     * dynamicRecipeResource
     * dynamic_recipe_resource
     */
    public function getDynamicRecipeResource($dependency)
    {
        return $dependency * 10;
    }
    
    /**
     * Can be obtained by:
     * SomeClass
     * recipePropertyObject
     * recipe_property_object
     */
    public $recipePropertyObject; // Contains \SomeClass
    
    /**
     * Can be obtained by:
     * recipeProperty
     * recipe_property
     */
    public $recipeProperty;
    
    /**
     * Can be obtained by:
     * recipePropertyCallback
     * recipe_property_callback
     */
    public $recipePropertyCallback;
    
    public function setupRecipeCallbackProperty()
    {
        // Callback arguments resolving same way as actions
        $this->recipePropertyCallback = function ($dependency) {
            return $dependency * 10;
        };
    }
    
    protected function configure()
    {
        /**
         * Can be obtained by:
         * inputArgument
         * input_argument
         */
        $this->addArgument('input_argument');
        
        /**
         * Can be obtained by:
         * inputOption
         * input_option
         */
        $this->addOption('input_option');
    }
    
    /**
     * Can be obtained by class or name
     */
    public function containerServicesAction(
        \Symfony\Component\Console\Input\InputInterface $input,
        \Symfony\Component\Console\Output\OutputInterface $output,
        \Symfony\Component\Console\Style\SymfonyStyle $style,
        \Aes3xs\Tasker\Connection\Connection $connection,
        \Monolog\Logger $logger,
        \Aes3xs\Tasker\Runner\Runner $runner,
        \Aes3xs\Tasker\Service\Shell $shell,
        \Aes3xs\Tasker\Service\Composer $composer,
        \Aes3xs\Tasker\Service\Git $git,
        \Aes3xs\Tasker\Service\Releaser $releaser,
        \Aes3xs\Tasker\Service\Symfony $symfony
    ) {
        // Do smth
    }
}

连接。

简单地在使用之前设置连接参数。
它将在第一次调用时自动初始化。目前不提供重用或重新连接。

本地连接相当简单。要使用它,请将主机留为 null。

远程连接提供了三种认证类型。

  • 登录/密码,使用 setPassword('password')
  • 公钥(可选密码),使用 setPublicKey($path or key itself)
  • 代理转发,使用 setForwarding(true)

基于 PhpSecLib 的远程连接。

实现了 Ssh 扩展,但在转发损坏的情况下已禁用。
当您手动使用 ssh 转发时,存在环境变量 $SSH_AUTH_SOCK,其中包含代理套接字的路径。因此,您可以继续使用转发来连接到另一个服务器。
使用 php ssh 扩展此变量不存在。这是一个非常必要的功能,因此目前我切换回 PhpSecLib。

<?php
  
class TestRecipe extends \Aes3xs\Tasker\AbstractRecipe
{
    protected function connect(\Aes3xs\Tasker\Connection\Connection $connection)
    {
        // By default connection is local
        // Local means host is null, 127.0.0.1 or localhost
        
        // Remote connection
        $connection
            ->getParameters()
            ->setHost('192.168.1.1') // Default port 22
            ->setLogin('root')
            ->setPassword('password');
        
        $connection->exec('echo hello');
    }
}

服务。

Shell。

Shell 是基于连接构建的。因此,如果您已经连接到远程(或本地)服务器,您也可以使用 Shell。
它包含大多数可用的 shell 命令。

  • exec
  • ln
  • chmod
  • chown
  • rm
  • mkdir
  • touch
  • readlink
  • realpath
  • dirname
  • ls
  • which

辅助方法

  • exists()
  • isFile()
  • isDir()
  • isLink()
  • isWritable()
  • isReadable()
  • write()
  • read()
  • copy()
  • copyPaths()
  • linkPaths()
  • checkWritable()
  • checkReadable()

如果您想以另一个用户身份运行所有命令,请使用 setUser() 进行配置。因此,所有命令都将使用 sudo -EHu USER bash -c "COMMAND" 预先运行。SSH 代理转发也将可用。

如果您想从特定目录运行所有命令,请使用 setCwd() 进行配置。

这些选项仅对 Shell 服务本身有效,而不是连接。
其他服务(发布者、Git、Composer、Symfony)也使用 Shell,因此请考虑这一点。

发布者

发布者管理发布。它准备目录结构以存储您的发布,并在部署或回滚时链接它们。
首先调用setDeployPath()来指向包含所有相关内容的根目录。

/var/www/project <- deploy_path
    │ 
    ├─ releases
    │   ├─ 20180101221100 (YmdHis format)
    │   ├─ 20180101221101
    │   ├─ 20180101221102
    │   ├─ 20180101221103
    │   ├─ 20180101221104 (Symfony example) <- current_path
    │   │   ├─ app
    │   │   │   └─ config
    │   │   │       └─ ~parameters.yml (symlink in shared)
    │   │   ├─ var
    │   │   │   └─ ~logs (symlink in shared)
    │   │   └─ release.lock (exists only in completed releases)
    │   │
    │   └─ 20180101221105 (deploy in progress...) <- release_path
    │     
    ├─ ~current (symlink to 20180101221104 for example)
    │
    ├─ shared
    │   ├─ app
    │   │   └─ config
    │   │       └─ parameters.yml
    │   └─ var
    │       └─ logs
    │           └─ ...
    │           
    └─ deploy.lock

然后调用prepare()来构建目录结构。
使用lock()、unlock()和isLocked()来控制deploy.lock文件,使用它来防止同时部署。
调用create()来创建新发布,调用release()来创建当前链接的符号链接并添加release.lock文件,调用link()来创建特定现有发布的符号链接,调用rollback()来创建上一个发布的符号链接,调用cleanup()来删除未使用的发布。使用getReleaseList()获取所有可用的发布,它只显示已完成的发布,忽略损坏和意外的目录和文件。
使用updateReleaseShares()来更新共享文件和目录。共享在所有发布中都是相同的,并且它们被单独存储。
使用getCurrentReleasePath()来获取当前链接的发布路径。
使用getCurrentReleaseName()来获取当前发布的名称(目录名明显)。

Git

使用私有仓库的首选方式是代理转发。但您也可以设置setKeyPath()来使用您的公钥。
方法

  • checkout()
  • cloneAt()
  • log()
  • fetch()
  • getBranches()
  • getCurrentBranch()

Composer

方法

  • install()
  • update()
  • download()用于下载phar存档

Symfony

首先设置控制台路径setConsolePath(),通常是release_path/bin/console

  • setEnv()
  • setDebug()
  • setInteractive()
  • runCommand()

将参数和选项传递给runCommand()。选项是一个关联数组,键是选项名称(如果值为null,则选项被视为标志)。