GeekFactory

int128.hatenablog.com

Jenkinsで自分でビルドしたGitコマンドを使う

Jenkins Agentで自分でビルドしたGitコマンドを使う方法を説明します。新しいバージョンのGitを使いたい場合に有用です。

方針

JenkinsにはGitやAntなどの外部ツールを管理する機能があります。 外部ツールが必要になった場合に自動的にインストールスクリプトを実行することもできます。

設定方法

JenkinsのGlobal Tool ConfigurationにあるGit installationで、以下を設定します。

  • Name:適当な名前(例えば compiled-git)
  • 自動インストール:チェック
  • コマンド実行
    • ラベル:自動インストールを行うノードを限定する場合はラベルを指定します。
    • ツールホーム: bin/git

コマンドは以下を指定します。

GIT_VERSION=2.11.0
GIT_HOME="`pwd`"
echo "Using $GIT_HOME/bin/git"
if [ ! -x bin/git ]; then
  curl -LO "https://github.com/git/git/archive/v${GIT_VERSION}.tar.gz"
  tar -zxf "v${GIT_VERSION}.tar.gz"
  cd "git-${GIT_VERSION}"
  make configure
  ./configure --prefix="$GIT_HOME"
  make install
fi

これで設定は完了です。

実行

Jenkins Slaveにあらかじめ開発用パッケージを入れておく必要があります。RHEL系の場合は以下になります。

yum -y groupinstall 'Development Tools'
yum -y install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-ExtUtils-MakeMaker

Gitを利用するジョブを実行してみましょう。初回のみGitのダウンロードとコンパイルが実行されるはずです。

CentOSに標準で入っているGitが古すぎてJenkinsのgit cloneが失敗する場合に使いました。Git以外にも応用できるので、いろんな場面で役に立つと思います!

改訂新版Jenkins実践入門 ――ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

改訂新版Jenkins実践入門 ――ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

Spring BootアプリのテストをSpockで書く

Spring BootアプリケーションのテストをSpockで書く方法を説明します。最近のバージョンを対象にしています。

  • Spring Boot 1.4
  • Spock 1.1-rc-3
  • Groovy 2.4

本稿では以下のテストレベルを対象とします。

まずは、build.gradleに依存関係を追加しておきます。

dependencies {
    testCompile 'org.springframework.boot:spring-boot-starter-test'
    testCompile 'org.spockframework:spock-core:1.1-groovy-2.4-rc-3'
    testCompile 'org.spockframework:spock-spring:1.1-groovy-2.4-rc-3'
    testRuntime 'cglib:cglib-nodep:3.2.4'
}

コンポーネントテスト

このテストレベルでは、テスト対象の依存コンポーネントをモックに差し替えた状態でテストを行います。

SpockのSpecificationでDIコンテナを利用するには、以下のように@SpringBootTestアノテーションを付加します。

@SpringBootTest(webEnvironment = NONE)
class HelloServiceSpec extends Specification {
}

これにより@AutowiredでSpringがコンポーネントを注入してくれるようになります。Web Environmentは不要なのでNONEを指定しています。

モックを利用するには@TestConfigurationを定義します。以下のようにDetachedMockFactory#Mock()メソッドでモックオブジェクトを注入することを宣言します。SpockのMock()記法は使えないので注意が必要です。

    // Specificationのインナークラス
    @TestConfiguration
    static class MockConfig {
        final detachedMockFactory = new DetachedMockFactory()

        @Bean
        ExternalApiClient externalApiClient() {
            detachedMockFactory.Mock(ExternalApiClient)
        }
    }

あとは、Spockのお作法に従ってgivenブロックの中でモックのインタラクションを宣言します。

        given:
        1 * client.getDefault() >> new Hello('world')

E2Eテスト

このテストレベルでは、テスト対象のAPIに対してHTTPリクエストを投げてレスポンスを検証します。

@SpringBootTestアノテーションwebEnvironment = RANDOM_PORTを指定することで、実際にアプリケーションサーバが起動した状態でテストを実行できます。

@SpringBootTest(webEnvironment = RANDOM_PORT)

テスト対象にRESTリクエストを送るにはTestRestTemplateクラスを利用します。

    @Autowired
    TestRestTemplate restTemplate

RestTemplateのお作法に従ってリクエストを送り、レスポンスを検証します。

        when:
        def entity = restTemplate.getForEntity('/hello', Hello)

        then:
        entity.statusCode == HttpStatus.OK
        entity.body.name == 'world'

この場合も`DetachedMockFactory#Mock()で依存コンポーネントをモックに差し替えることが可能です。

まとめ

@SpringBootTestアノテーションでDIコンテナを使ってテストを実行する方法を説明しました。また、DetachedMockFactory#Mock()メソッドで依存コンポーネントをモックに差し替える方法を説明しました。TestRestTemplateクラスを使うとテスト対象APIにRESTリクエストを送信できます。

GitHubにサンプルプロジェクトを置いているので参考にどうぞ。

github.com

FeignでOAuth 2.0クライアントを使う

RESTクライアントのSpring Cloud Netflix FeignでOAuth 2.0を使う方法を説明します。

  • @FeignClientconfigurationOAuth2FeignRequestInterceptorを設定する。
  • OAuth2FeignRequestInterceptorにクライアントIDやクライアントクレデンシャルを渡す。

具体的な設定

@EnableOAuth2Clientアノテーションを付けます。

@EnableOAuth2Client
@EnableFeignClients
@SpringBootApplication
class App {
    static void main(String[] args) {
        SpringApplication.run(App, args)
    }
}

@FeignClientアノテーションにConfigurationクラスを指定します。

@FeignClient(name = 'hello', url = '${hello.service.url}', configuration = HelloClientConfiguration)
interface HelloClient {
}

ConfigurationクラスでOAuth2FeignRequestInterceptorを設定します。OAuth2FeignRequestInterceptorクラスはspring-cloud-securityに含まれるクラスで、OAuth 2.0アクセストークンの取得や利用をまるっとお任せできます。

@Configuration
class HelloClientConfiguration {
    @Value('${security.oauth2.hello.access-token-uri}')
    private String accessTokenUri

    @Value('${security.oauth2.hello.client-id}')
    private String clientId

    @Value('${security.oauth2.hello.client-secret}')
    private String clientSecret

    @Value('${security.oauth2.hello.scope}')
    private String scope

    @Bean
    RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext) {
        new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource())
    }

    OAuth2ProtectedResourceDetails resource() {
        def details = new ClientCredentialsResourceDetails()
        details.accessTokenUri = accessTokenUri
        details.clientId = clientId
        details.clientSecret = clientSecret
        details.scope = [scope]
        details
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        Logger.Level.BASIC
    }
}

application.ymlを設定します。

logging.level:
  # アクセストークンリクエストのログを有効にします
  org.springframework.web.client: DEBUG
  # Feignのリクエスト/レスポンスログを有効にします
  example.client.HelloClient: DEBUG

hello.service:
  url: http://localhost:8081

# Hystrixを無効化またはSEMAPHOREに変更する必要がある(後述)
feign.hystrix.enabled: false

security:
  oauth2:
    hello:
      access-token-uri: http://localhost:8081/oauth/token
      client-id: theId
      client-secret: theSecret
      scope: theScope
    # (参考) Twitter APIの場合はこんな感じ
    twitter:
      access-token-uri: https://api.twitter.com/oauth2/token

既知の問題

アクセストークンはセッション単位で管理する必要があるため、OAuth2ClientContextはセッションスコープで管理されます(OAuth2ClientContextConfiguration)。これにより、以下の問題があります。

まず、複数のFeignClientを利用すると同一のアクセストークンを利用してしまう問題があります。例えば、TwitterClientとFacebookClientがあって先にTwitterClientを実行した場合、OAuth2ClientContextにTwitter APIのアクセストークンが保存されてしまうため、Facebook APIはエラーになります。自分でセッションスコープのBeanを定義すれば解決しそうですが、まだ試していません。解決策を知っていたら教えてください。

また、FeignとHystrixを組み合わせて使えない問題があります。これはHystrixが別スレッドでHTTPリクエストを実行するため、セッションスコープのOAuth2ClientContextを参照できないエラーが発生するためです。Hystrixを無効化するか、SEMAPHOREモードに切り替える必要があります。(参考: Spring @FeignClient with OAuth2FeignRequestInterceptor not working - Stack Overflow

実行時にOAuth2ProtectedResourceDetailsを切り替える

上記の実装ではDIコンテナの起動時にOAuth2ProtectedResourceDetailsが生成されるため、複数のOAuth2ProtectedResourceDetailsを動的に切り替えるには工夫が必要です。例えば、ログイン状態に応じてClient Credentials GrantとResource Owner Password Grantを使い分けたいといった場合があります。

実行時にOAuth2ProtectedResourceDetailsを切り替えたい場合、FeignのRequestInterceptorを自分で実装します。

@Slf4j
class MultiGrantOAuth2FeignRequestInterceptor implements RequestInterceptor {
    //@Valueは省略

    @Inject
    private OAuth2ClientContext oAuth2ClientContext

    @Override
    void apply(RequestTemplate template) {
        new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource()).apply(template)
    }

    OAuth2ProtectedResourceDetails resource() {
        def authentication = SecurityContextHolder.context.authentication
        if (!authentication || authentication instanceof AnonymousAuthenticationToken) {
            // 未ログインの場合はClient Credentials Grantを使う
            def details = new ClientCredentialsResourceDetails()
            details.accessTokenUri = accessTokenUri
            details.clientId = clientId
            details.clientSecret = clientSecret
            details.scope = [scope]
            log.debug("ClientCredentialsResourceDetails")
            details
        } else {
            // ログイン済みの場合はResource Owner Password Grantを使う
            def details = new ResourceOwnerPasswordResourceDetails()
            details.accessTokenUri = accessTokenUri
            details.clientId = clientId
            details.clientSecret = clientSecret
            details.scope = [scope]
            // ユーザ情報からユーザ名を取得
            details.username = (authentication.principal as UserDetails).username
            details.password = 'theResourceOwnerPassword'  // TODO: 固定値ではなく動的に設定
            log.debug("ResourceOwnerPasswordResourceDetails: username=${details.username}")
            details
        }
    }
}

とりあえず、簡単に試すには以下のようなWebSecurityConfigを用意するとよいでしょう。

@Configuration
@EnableWebSecurity
class AppSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        // ログイン後にセッションを再作成する
        http.sessionManagement().sessionFixation().newSession()

        // 未ログインでもアクセスできるようにする
        http.authorizeRequests().anyRequest().permitAll()

        // フォームログインを有効にする
        http.formLogin()
    }
}

注意点ですが、未ログイン時に取得したアクセストークンはClient Credentials Grantのものなので、ログイン後にResource Owner Password Grantのものを再取得する必要があります。そのため、sessionFixation().newSession()でログイン後にセッションを再作成するように設定しています。

この実装では以下のような挙動になります。

  1. http://localhost:8082/hello にアクセスすると、Client CredentialsのクライアントIDが表示される
  2. http://localhost:8082/login にアクセスしてフォームでログインする
  3. http://localhost:8082/hello にアクセスすると、リソースオーナーのユーザ名が表示される

下記にサンプルがあるので参考までにどうそ。

github.com