PullRequestでRundeckジョブを作成しGUIからSpringBatchを実行可能にする。

#背景
現在携わっているシステムはWEB画面系は Pull Request をJenkinsでビルドして開発機のtomcatにデプロイ→SlackでURL通知→即レビュー、という流れができており、重宝しています。
ところがバッチ系(SpringBatch)はさすがにそれは難しい。 `java ...` とかコマンド打つの面倒くさいしそもそもCUI怪しい人もいるので。

でもビルド結果をコンソールではなくRundeckから実行できればコマンド打てない人でもレビューできるなと思いやってみました。

完全なハンズオンにはできないですが、迷っているorハマっている方の一助になれば幸いです。

# 前提
Maven/Jenkins/Rundeckをまたいだ作業になるので、下記の前提をクリアしている方がいいです。
　・Maven がそこそこ使える
　・GitHub Hook で Jenkins PR-HEAD ビルド走らせる環境がある.
　　http://qiita.com/quattro_4/items/6b1962909ce868f12e5a
　・Jenkinsfile が怖くない. Groovy の Sandbox も対処できる。
　　http://qiita.com/miyasakura_/items/9d9a8873c333cb9e9f43
　　http://qiita.com/yyYank/items/e591750c67f3a0bef221

　・Rundeck で Job 動かせる。Rundeckと実行環境が別の場合はSSH経由で実行できる環境がある。
　　http://qiita.com/komeda-shinji/items/76c1c30f37ab5f81c441
　　実行時のパーミッションなど考慮すると jenkins ユーザがよいですよ。
　　http://qiita.com/toyottoyo/items/0ceb96c1454bc16d20ed

# 環境
　Rundeck 2.9.2-1 (Windows Server) on Azure
　Jenkins ver. 2.52 (Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-75-generic x86_64) on Azure
　GitHub
　spring-batch 3.0.7.RELEASE

# 作業項目
## Rundeckのインストールとプロジェクト作成
　上記リンク参照
## JenkinsとGitHubの設定
 ・PRビルドできるように設定をする。
　上記リンク参照

## SpringBatchのexecutable-jar化
この記事のメインその１。

現在の環境がローカルWindows 検証機ubuntu、ステージング・本番Windowsといういびつな構成のため、shやbatでラップしようとすると単純に２倍の手間が掛かりそうなので、実行可能なjarを作り

Jar内の任意のjobを
`> java -jar path/to/jar/somebuild.jar some-job-context.xml some-job param1=value1 ...`
というように実行できるようにしました。

### 以降、サンプル用の諸元もろもろ
 ・groupId:**jp.co.icsoft**
 ・artifactId:**ics-bat**
 ・version:**0.1-SNAPSHOT**
 ・spring-batch job-context: **src/main/resources/META-INF/job/some-job-context.xml**
 ・spring-batch job: **somejob**
 ・Rundesc URL: **http://yourrundeck:4440/**
 ・Rundeck ジョブ名: **ics-bat**
 ・Rundeck ジョブ実行ノード: **ubuntu-node**
　
### pom.xmlに shade plugin と resource filtering 適用

```xml:pom.xml抜粋
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  <groupId>jp.co.icsoft</groupId>
  <artifactId>ics-bat</artifactId>
  <version>0.1-SNAPSHOT</version>
...
<build>
   <resources>
      <!-- for rundeck-job.xml -->
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
    </resources>
...
<plugins>
...
<plugin>
       <artifactId>maven-shade-plugin</artifactId>
        <version>3.0.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <finalName>${project.artifactId}-${project.version}</finalName>
              <transformers>
                <transformer
                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>org.springframework.batch.core.launch.support.CommandLineJobRunner
                  </mainClass>
                </transformer>
                <transformer
                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.handlers</resource>
                </transformer>
                <transformer
                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                  <resource>META-INF/spring.schemas</resource>
                </transformer>
              </transformers>
              <shadedArtifactAttached>true</shadedArtifactAttached>
              <!-- filename will be '<project.artifact>-<project.version>-executable.jar'-->
              <shadedClassifierName>executable</shadedClassifierName>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
                <!-- filtering only specific (illegal!) jar if you need -->
                <filter>
                  <artifact>org.bouncycastle:*</artifact>
                  <excludes>
                    <exclude>org/bouncycastle/jce/**/*</exclude>
                  </excludes>
                </filter>
              </filters>
            </configuration>
          </execution>
        </executions>
      </plugin>
</plugins>
...
</build>
...
</project>
```
### Spring の Namespace Handler を置く
　spring.schemas
　spring.handlers
を`src/resources/META-INF`に置きましょう。
自分は本家から拾ってきましたよ。
https://github.com/spring-projects/spring-framework/tree/master/spring-beans/src/main/resources/META-INF
### チェック
ここで`mvn package` してみましょう。
`ics-bat-0.1-SNAPSHOT-executable.jar` ができており、同じディレクトリにいる
`ics-bat-0.1-SNAPSHOT.jar`
より、dependency など取り込んだ分容量が大きくなっていればうまくいってます。

それができたら

```bash
> java -jar /home/ore/ics-bat/target/ics-bat-0.1-SNAPSHOT-executable.jar classpath:META-INF/job/some-job-context.jar somejob -next
```
がローカルで動く(少なくともspring-batchが起動する)ようになってるとよいです。

### Rundeck用のjobテンプレートを置く

この記事のメインその２、自分が調べた範囲ですが、あまりやってる例を見ない。

のちの Jenkinfile でゴリゴリしてもいいのかもしれませんが、開発者に step 追記してもらいたかったりもするので、import させる xml のテンプレをあらかじめ配置しておきます。

```xml:src/main/resources/rundeck-job.xml
<joblist>
  <job>
    <description>for ${env.BRANCH_NAME}</description>
    <dispatch>
      <excludePrecedence>true</excludePrecedence>
      <keepgoing>false</keepgoing>
      <rankOrder>ascending</rankOrder>
      <successOnEmptyNodeFilter>false</successOnEmptyNodeFilter>
      <threadcount>1</threadcount>
    </dispatch>
    <executionEnabled>true</executionEnabled>
    <loglevel>DEBUG</loglevel>
    <name>ics-bat-${env.BRANCH_NAME}"</name>
    <nodeFilterEditable>false</nodeFilterEditable>
    <nodefilters>
      <filter>hostname: ubuntu-node</filter>
    </nodefilters>
    <nodesSelectedByDefault>true</nodesSelectedByDefault>
    <scheduleEnabled>true</scheduleEnabled>
    <sequence keepgoing='false' strategy='node-first'>
      <command>
        <!-- ここに開発担当者が別のcommandを追加可能 -->
        <description>step1</description>
        <exec>java -jar ${env.WORKSPACE}/target/${pom.artifactId}-${pom.version}-executable.jar classpath:META-INF/job/some-job-context.xml somejob -next
        </exec>
      </command>
    </sequence>
  </job>
</joblist>
```
### Jenkinsfile置く
この記事のメインその３、JenkinsfileでRundeckのapi叩くのあまり見ないです。
ちょっと恥ずかしいですが、こんな感じです。

```groovy:Jenkinsfile
#!groovy

RUNDECK_URL = "http://yourrundeck:4440"
RUNDECK_IMPORT_URL = "api/20/jobs/import"
RUNDECK_TOKEN = "XXXXX"

def rundeckPermaLink = ""
node {
    step([$class: 'GitHubSetCommitStatusBuilder'])
    echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
    def pom
    stage('Checkout') {
        checkout scm
    }
    stage('Build and test') {
        env.PATH = "${tool 'maven-3.3.9'}/bin;${env.PATH}"
        pom = readMavenPom file: 'pom.xml'
        def version = pom.version.replace("-SNAPSHOT", ".${currentBuild.number}")
        //deciding develop OR pull request
        def mvnGoals = " clean"
        mvnGoals += (env.BRANCH_NAME.equals("develop") ? " deploy" : " package -Dpullrequest=${env.BRANCH_NAME}")
        mvnGoals += " -P develop"
        // directory to deploy.
        mvnGoals += " -DfileRootDir=${env.WORKSPACE}/target/temp" //この辺はお好みでどうぞ
        mvnGoals += " -DbranchName=${env.BRANCH_NAME}"
        try {
            echo "Build and Test ${env.BRANCH_NAME}"
            mvn "${mvnGoals}"
            currentBuild.result = 'SUCCESS'
        } catch (err) {
            echo err.message
            currentBuild.result = 'FAILURE'
        }
        echo "RESULT = ${currentBuild.result}"
    }
    stage('Rundeck') {
        echo "WORKSPACE:${env.WORKSPACE}"
        try {
            
            rundeckPermaLink = postJobToRundeck()
        } catch (err) {
            echo err.message
            currentBuild.result = 'FAILURE'
        }
    }
    stage('Notify') {
        notifySlack("${pom.artifactId}-${pom.version} on ${env.BRANCH_NAME} : ${currentBuild.result} : ${rundeckPermaLink}",
                "esys-bat-sts")
        notifyO365Teams("${pom.artifactId}-${pom.version} on ${env.BRANCH_NAME} : ${currentBuild.result}",
                "esys-bat-sts")
        step([$class: 'GitHubCommitNotifier', resultOnFailure: 'FAILURE'])

        def color = isOK()? 'GREEN' : 'RED'
    }
}

def mvn(String goals) {
    def mvnHome = tool "maven-3.3.9"
    def javaHome = tool "java-1.8.0-openjdk-amd64"
    withEnv(["PATH+MAVEN=${mvnHome}/bin"]) {
        wrap([$class: 'ConfigFileBuildWrapper', managedFiles: [
                [fileId: 'XXXXX', targetLocation: "settings.xml", variable: '']
        ]]) {
            sh "mvn -s settings.xml -B ${goals}"
        }
    }
}

def isOK() {
    return "SUCCESS".equals(currentBuild.result)
}

@NonCPS
def echoVariables(val) {
    val.properties.each { it ->
        echo "$it.key -> $it.value"
    }
}

def notifyO365Teams(text, deployedUrl) {
    office365ConnectorSend message: "${text} (${env.BUILD_URL})",
            status: currentBuild.result,
            webhookUrl: 'https://outlook.office.com/webhook/XXXXXXX'
}

def notifySlack(text, deployedUrl) {
    slackSend color: isOK() ? '#0000FF' : '#FF0000',
            failOnError: true,
            message: "${text} (${env.BUILD_URL})",
            teamDomain: 'yourteam',
            tokenCredentialId: 'Slack'
}

def String postJobToRundeck() {
    //URLBuilderなどで爽やかに書きたいところ
    String params = "dupeOption=update&project=ics-bat&format=xml&xmlBatch="
    String jobXml = readFile("${env.WORKSPACE}/target/classes/rundeck-job.xml")
    byte[] postData = (params + jobXml).getBytes("UTF-8")
    echo(jobXml)
    def req = new URL(RUNDECK_URL + "/" + RUNDECK_IMPORT_URL).openConnection();
    req.setRequestMethod("POST")
    req.setDoOutput(true)
    req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
    req.setRequestProperty("Content-Length", String.valueOf(postData.length))
    req.setRequestProperty("X-Rundeck-Auth-Token", RUNDECK_TOKEN)
    req.getOutputStream().write(postData)
    def resCd = req.getResponseCode()
    if (resCd.equals(200)) {
        String resXml = req.getInputStream().getText()
        echo(resXml)
        //Sandbox の制限で Closure 使えないので荒々しい書きぶり
        return resXml.find(/<permalink>(.*)<\/permalink>/).replaceAll('<permalink>|</permalink>', '');
    }
}
```
いやーん恥ずかしい。
適宜、in process script approvalなどで、sandboxが抑止してるメソッド解放してあげてください。
Sandobox自体を回避した方がいいくらい怒られます。

## いきなり結論
これでビルドが完了すると、Rundeckの`ics-bat`プロジェクトに`ics-bat-PR-1`というジョブができて実行できるようになっているはずです。

リアクションがあれば、時間を見て追記していく心づもりではいますのでよろしうお願いいたします。