Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

golang.org/x/oauth2で色々な認可フローや方言に対応する

この記事はQiitaで公開されていました

OAuth 2.0クライアントの実装

golang.org/x/oauth2を使ってトークンを発行する際に、複数の認可フローや方言があるけれど、どうやって実装するんだろう?と思ったことを調べました。OAuth 2.0自体については、OAuth 2.0の仕組みと認証方法が、どのようなレスポンスが返却されるのか記載されているので理解しやすいと思います。

アクセストークンの発行方法

OAuth 2.0には、リダイレクトを伴うものだけではなく、いくつかの認可フローが存在します。各フローの説明は、色々な OAuth のフローと doorkeeper gem での実装がわかりやすいと思います。

  • Authorization Code
  • Client Credentials
  • Resource Owner Password Credentials
  • Implicit Grant

以下で、golang.org/x/oauth2を使うと、それぞれの認可フローではどのようにhttp.Clientを取得するのか、をサンプルとして紹介します。

Authorization Code

ブラウザでリダイレクトを行うタイプのトークン取得方法です。3-legged OAuth 2.0と呼ばれます。この認可フローでは、grant_typeauthorization_codeです。この場合、さらにオンラインとオフラインアクセスで微妙に異なります。以下はオフラインアクセスの例です。

conf := oauth2.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://provider.com/o/oauth2/auth",
        TokenURL: "https://provider.com/o/oauth2/token",
    },
}
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)

// ...urlをブラウザで開いて認可コードを取得...

authCode := "AUTH_CODE"
token, err := conf.Exchange(context.Background(), authCode)
if err != nil {
    log.Fatal(err)
}
client := conf.Client(context.Background(), token)

余談ですが、ユーザにコピペさせずにoauth2.Config.RedirectURLで工夫してアクセストークンを取得する方法もあるそうです。

Client Credentials

少し簡略化したトークン取得方法です。2-legged OAuth 2.0と呼ばれます。このためには、golang.org/x/oauth2/clientcredentialsという別のパッケージが必要です。この認可フローでは、grant_typeclient_credentialsです。

conf := clientcredentials.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    TokenURL:     "https://provider.com/o/oauth2/token",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
}
client := conf.Client(context.Background())

上記の例では直接http.Clientを取得していますが、clientcredentials.Config.Token(ctx)を使うとトークンも取得できます。

Resource Owner Password Credentials

ユーザ名とパスワードを使ったトークンの取得方法です。この認可フローでは、grant_typepasswordです。

conf := oauth2.Config{
    ClientID:     "YOUR_CLIENT_ID",
    ClientSecret: "YOUR_CLIENT_SECRET",
    Scopes:       []string{"SCOPE1", "SCOPE2"},
    Endpoint: oauth2.Endpoint{
        TokenURL: "https://provider.com/o/oauth2/token",
    },
}
token, err := conf.PasswordCredentialsToken(ctx, "USER", "PASSWORD")
if err != nil {
    log.Fatal(err)
}
client := conf.Client(ctx, token)

Authorization Codeと異なりoauth2.AuthCodeURL()を呼び出さないため、EndpointAuthURLは不要です。

Implicit Grant

これはよくわかりません。

任意のヘッダを追加したい

認可サーバの仕様によっては、HTTPヘッダが必須になる場合があります。golang.org/x/oauth2にはカスタムしたHTTPリクエストを認証フローで使わせる方法があるので、これを使って行います。具体的には、context.Contextの値に、oauth2.HTTPClientというキーでカスタムしたhttp.Clientをセットしておくという形になります。

type oauthTransport struct{}

func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    r.Header.Set("User-Agent", "MyApp/1.0.0")
    return http.DefaultTransport.RoundTrip(r)
}

func main() {
    c := &http.Client{Transport: &oauthTransport{}}
    ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
    conf := &oauth2.Config{...}
    token, err := config.PasswordCredentialsToken(ctx, "USER", "PASSWORD")

任意のパラメータを追加したい

試していませんが、認可コード取得時ならoauth2.SetAuthURLParam()が使えると思います。

Authorizationヘッダではなくペイロードを使う

OAuth 2.0でクライアントシークレットを認可サーバへ渡す方法は2通り存在します。一つはBasic認証のヘッダで渡す方法。もう一つはリクエストのペイロードで渡す方法です。golang.org/x/oauth2は、通常はBasic認証の方法を使いますが、一部のペイロードを要求する既知のサービスはペイロードとして渡すようになっています。

独自または無名の認可サーバは、Basic認証で動作してしまうため、これが不都合な場合はoauth2.RegisterBrokenAuthHeaderProvider(tokenURL)ペイロードとして渡されるようにURLを登録する必要があります。これは前方一致でマッチします。

func init() {
    oauth2.RegisterBrokenAuthHeaderProvider("https://provider.com/")
}

JSONの形式が微妙に異なる場合に対応する

golang.org/x/oauth2パッケージは、トークン取得時のレスポンスに含まれるペイロードがフォームエンコードまたは以下のような形式のJSONであることを要求します。

{
  "access_token": "xxx",
  "token_type": "bearer",
  "refresh_token": "xxx",
  "expires_in": 3600
}

そのため、以下のようなJSONは、そのままでは扱うことができません。

{
  "auth_param": {
    "access_token": "xxx",
    "token_type": "bearer",
    "refresh_token": "xxx",
    "expires_in": 3600
  }
}

この場合、上で書いたヘッダを追加する方法と同じように、http.RoundTripperで加工したものを返してあげる必要があります。

type oauthTransport struct{}

type tokenJSON struct {
    Auth json.RawMessage `json:"auth_param"`
}

func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    resp, err := http.DefaultTransport.RoundTrip(r)
    if err != nil {
        return resp, err
    }
    var v tokenJSON
    decoder := json.NewDecoder(resp.Body)
    if err := decoder.Decode(&v); err != nil {
        return resp, err
    }
    resp.Body.Close()
    resp.Body = ioutil.NopCloser(bytes.NewReader(v.Auth))
    resp.ContentLength = int64(len(v.Auth))
    return resp, err
}

json.RawMessageは値を[]byteのまま扱う型です。JSONの一部だけをそのまま扱いたい場合にとても便利です。

macOSでNixpkgsのアップグレード

この記事はQiitaで公開されていました

TL;DR

NixpkgsのmacOSインストーラはマルチユーザインストーラなので、rootユーザにインストールされているnixパッケージのアップデートが必要。

$ sudo -i nix-env -q
nix-2.0
nss-cacert-3.35
$ nix-env --version
nix-env (Nix) 2.0

モチベーション

最近nix-channel --updateした環境では、nix-env -qaするとエラーになってしまう。

$ nix-env -qa ct
error: undefined variable ‘placeholder’ at /nix/store/f81z(snip)ba37/nixpkgs/pkgs/development/libraries/pipewire/default.nix:37:46

これは、Nix 2.0でplaceholder変数が導入されたけれど、Nix 1.11以前には存在していないため発生するエラーなので、先にNix自体をアップグレードする必要がある。しかしbrew upgradeのようなそれ自身をアップグレードするコマンドは用意されていない。

現在、macOS版のNixpkgsはマルチユーザ用にインストールされているので、Nixpkgs自体をアップグレードするには、rootnixパッケージを更新する必要がある。

$ sudo -i su -
root# nix-channel --update
root# nix-env -iA nixpkgs.nix
root# launchctl stop org.nixos.nix-daemon
root# launchctl start org.nixos.nix-daemon

sudo -i nix-channel --updateでも良さそうに思えるが、

error: unable to download ‘https://d3g5gsiof5omrk.cloudfront.net/nixpkgs/nixpkgs-18.09pre133932.ee28e35ba37/nixexprs.tar.xz’: Problem with the SSL CA cert (path? access rights?) (77)

というエラーでうまく動作しなかった。

Go & Versioning(vgo)を読んで大きな変更が入ったなと思った

この記事はQiitaで公開されていました

このQiita記事は、Go & Versioningで掲載された一連の記事を読んで、自分なりのまとめと感想です。私の周りはあまり騒いでないけど、これ感覚的なものが大きく変わるなあ、と思ったので、主に表面上に現れる変更をまとめました。

これは、Go 1.11で試験的な導入、Go 1.12で正式サポートとなる予定の機能に関する話です。

@nekketsuuuさんが原文の和訳をされています。

何が変わるのか

バージョン管理機能の導入

goコマンドにバージョン管理の機能が追加されます。バージョンは常にセマンティックバージョニングで表します。今もdepコマンドが(goとは別に)存在しますが、バージョン管理機能が追加されたgoコマンド(以下vgoと表記)は、depglideなどの依存管理ツールとは別のアプローチでバージョンを管理します。とはいえ、目的はどちらもバージョン管理なので、vgoを使う場合はdepを使いません。また、vgovendorディレクトリを使わないためvendorも不要です。

モジュールという単位の追加

バージョン管理機能に伴って、Goのパッケージにモジュールという単位が追加されます。モジュールは複数のパッケージをまとめたもので、go.modというファイルで管理します。Go & Versioningでは以下の例が記されていました。

// My hello, world

module "rsc.io/hello"

require (
    "golang.org/x/text" v0.0.0-20180208041248-4e4a3210bb54
    "rsc.io/quote" v1.5.2
)

モジュールは、vgoでバージョン管理を行う基本的な単位となります。1つのリポジトリが1つのモジュールに該当し、タグ(git tag)を使ってモジュールにバージョンを与えます。従って、バージョン管理を行う単位はモジュールです。例えば、上のgo.modでリストされているgolang.org/x/textrsc.io/quoteもモジュールです。今まではパッケージという扱いでしたが、モジュールに変わります。

モジュールのアップデートはvgo get

モジュールはgo.modファイルでバージョンを指定しますが、これを人が維持するのは大変です。vgo getで必要なモジュールを追加したり、vgo get -uでアップデートを行ったりするようです。

たくさんのExampleが書かれているのでA Tour of Versioned Goを眺めてみてください。これだけで雰囲気はつかめると思います。

破壊的変更を加える場合はimport pathを変更する

vgoで扱うパッケージは、全て後方互換性を持たなければなりません。例えばAというパッケージの作者は、メジャーバージョンが同じ間は後方互換性を維持する必要があります。この制約によって、v1.1.0を参照しているプログラムは、v1.2.1が使われた場合でも同じように動作することが保証されます。もしマイナーバージョンのアップデートでビルドが壊れた場合、Av1.2.2で過去の互換性を取り戻すべきです。例えば、関数の動作を変更する場合は、仕様変更ではなく名前を変えて新しい関数として追加しましょう。

とはいえ、どうしても破壊的な変更が避けられないケースは存在します。その場合はimport pathを変更することで別のパッケージとして作成してください。例えば、lufia.org/pkg/xに破壊的変更を加える場合、新しいバージョンはlufia.org/pkg/x/v2のようにメジャーバージョンを含むimport pathにしましょう。そうするとv1v2で重複する部分がソースコードの二重管理になりがちですが、それはtypealias等を使って頑張ってください。

最初からimport pathをlufia.org/pkg/x/v1のようにするべきかについては、最終的にどうなるかは分かりませんが、個人的には「最初はバージョンを含めない」でいいと思います。

$GOPATHが不要になる

これまで$GOPATHはGoワークスペースのルートとして必要でしたが、vgoではgo.modによってモジュールのURLやバージョンが明確に特定できるため、$GOPATHがなくてもソースコードの取得やビルドの再現性には困りません。参照するモジュールのマイナーバージョンは上がるかもしれませんが、マイナーバージョンの変更はモジュール作者の努力によって互換性が維持されるため、最終的な動作は変わらないことが保証できます。

これによって、任意のディレクトリでGoのコードを書くことが可能となります。今までのように、$GOPATH配下にワークスペースを構築する必要は無くなります。

ただこれ疑問なのは、今までの$GOPATH直下には

$ ls $GOPATH
bin pkg src

が存在していて、まあsrcpkgは無くてもいいかなと思うのですが、go getでインストールする場所としての$GOPATHはどうなるんだろう?と思いました。

何が変わらないのか

今までのコードはそのままビルド可能

vgoだからといって今までのコードが壊れることはありません。$GOPATHvendorも、不要になるだけで、そのまま使えます。go.modがないリポジトリもそのままビルドできるはずです。

depvgoが普及するまでは残る

長期的にみればdepは無くなるのかもしれませんが、少なくともvgoがリリースされて、十分に普及するまでは継続することが書かれていました。今すぐ何かが変わることはないので、そのまま使い続けても問題ないと思います。

今後の話

vgoは、この記事の最初にも書きましたけれど、Go 1.11で試験的な導入、Go 1.12で正式サポートとなる予定です。今はまだGo 1.10がリリースされたばかりなので何もしなくても良いと思いますが、半年後にGo 1.11がリリースされたら、特にパッケージ作者は以下のことに注意しましょう。

  • go.modを作成しましょう
  • セマンティックバージョニングでタグを打ちましょう
  • import pathが同一である限り後方互換性を維持しましょう

golang.org/x/sync/semaphoreを使ってゴルーチンの同時実行数を制御する

この記事はQiitaで公開されていました

特定の処理をゴルーチンで並列実行したいけれど、サーバの負荷等を考慮して、同時実行数の上限を設定したい話です。元ネタの記事では、チャネルやsync.Poolを使って実現していて、すでに十分シンプルなのですが、x/sync/semaphoreを使う方法も便利だったので紹介します。

見た目はほぼ、チャネルを使った実装と同じですが、s.Acquire(ctx, n)nの値で重みをつけることができます。なので、Aという処理が動いているときは他の処理を行わない、けれどBなら3個まで同時に動いても良い、といった対応をチャネルで行うと面倒ですが、semaphore.Weightedなら重みを変更するだけで実現できるので便利だと思いました。

元ネタ

実装例

以下の例は、同時実行数が3つに制限された状態でdoSomething(u)を並列実行します。サンプルコード自体はmattnさんのものをほぼそのまま流用しました。

全ての処理完了を待つためにsync.WaitGroupを使っていますが、semaphore.Weightedには全く関係ありません。

2024-04-10追記 sync.WaitGroup を使わなくても、最後にs.Acquire(Limit)すると完了を待てるので、そっちのほうがシンプルですね。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func doSomething(u string) {
    fmt.Println(u)
    time.Sleep(2 * time.Second)
}

const (
    Limit  = 3 // 同時実行数の上限
    Weight = 1 // 1処理あたりの実行コスト
)

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.net/foo",
        "http://www.example.net/bar",
        "http://www.example.net/baz",
    }
    s := semaphore.NewWeighted(Limit)
    var w sync.WaitGroup
    for _, u := range urls {
        w.Add(1)
        s.Acquire(context.Background(), Weight)
        go func(u string) {
            doSomething(u)
            s.Release(Weight)
            w.Done()
        }(u)
    }
    w.Wait()
}

説明

まずは、semaphore.NewWeighted(lim)lim個のリソースをもつsemaphore.Weightedを作成します。s.Acquire(ctx, n)は、全体のリソース(lim)からn個消費しますが、Weightedのリソースが足りない場合は、s.Acquire(ctx, n)の呼び出しは他のゴルーチンからs.Release(n)されるまでブロックします。そのため、同時にlim個以上の処理が動くことはありません。

処理が終わった後s.Release(n)を使うと、n個のリソースをWeightedへ戻します。ブロックしていたs.Acquire(ctx, n)があれば、ブロックが解除されて続きの処理を行います。

また、s.TryAcquire(n)というメソッドも用意されていて、こちらはブロックしません。代わりに、リソースが取得できたらtrueを返します。

Jenkinsfileの書き方

この記事はQiitaで公開されていました

ほとんど情報がなかったので調べながら書いたメモ。基本的に、公式情報が最もまとまっている。

また、古い情報らしいけど、現状こちらにしか書かれていない項目もある。

Declarative Pipelineの1.2.8から、ジェネレータが追加されたようです。

基本

現状のJenkinsfileは2通り書き方があって、pipelineがルートになっている場合はDeclarative Pipelineという。この場合は、Groovyスクリプトを直接書くことができず、Groovyを書きたければscriptディレクティブを使う必要がある。この記事では主にこちらを扱う。

pipelineから始まらない場合、Scripted Pipelineといって、この場合は直接Groovyスクリプトも書けるし、node()stage()などの、Pipeline Stepsメソッドも書くことができる。便利そうにみえるけど自由度が高すぎて職人コードになりがち。

Declarative Pipelineの書き方

Jenkinsfileは特定の条件ごとにディレクティブを書いて実装する。ディレクティブの出現順は以下の順番になっている。例えばstagesのすぐ下にagentを書くことはできない。

pipeline {
    // 1行コメント
    /*
    * 複数行コメント
    */
    agent { ... }
    environment { ... }
    options {
        buildDiscarder(...)
        checkoutToSubdirectory(...)
        disableConcurrentBuilds(...)
        disableResume()
        newContainerPerStage(...)
        overrideIndexTriggers(...)
        preserveStashes(...)
        quietPeriod(...)
        retry(...)
        skipDefaultCheckout(...)
        skipStagesAfterUnstable(...)
        timeout(...)
        timestamps()
        parallelsAlwaysFailFast(...)
    }
    parameters {
        string(...)
        booleanParam(...)
        choice(...)
        file(...)
        text(...)
        password(...)
        run(...) // ??
    }
    tools {
        maven '...'
        jdk '...'
        gradle '...'
    }
    triggers {
        cron(...)
        pollSCM(...)
    }
    stages {
        stage {
            agent { ... }
            environment { ... }
            tools { ... }
            options {
                skipDefaultCheckout(...)
                timeout(...)
                retry(...)
                timestamps()
            }
            when { ... }
            steps {
                //Pipeline Steps Reference参照
                echo 'help'
                sh 'ls'
                script {
                    // 任意のGroovyスクリプト
                }
            }
        }
    }
    steps {
        //Pipeline Steps Reference参照
    }
    post {
        always { ... }
        success { ... }
        failure { ... }
        ...
    }
}

stepsだけの場合は、直接stepsの中に書くものを記述できる。

echo 'help'
sh 'ls'

node()のようにstepsディレクティブの中で子ブロックが現れる場合、そのブロックの中はstepsディレクティブと同じものが書ける。

steps {
    node('slave1'){
        sh 'ls'
        dir('output'){
            sh 'ls'
        }
    }
}

個別の事例

stageの書き方

stage()を使うと、進捗を分けて表示することができるが、インターネットでは2種類の書き方がある。stage単体で書かれているものは古く、推奨されない。

stage 'first'
sh 'ls'

stage 'second'
sh 'pwd'

今はブロックを取るようになっていて、こちらが推奨される。

stage('first'){
    sh 'ls'
}
stage('second'){
    sh 'pwd'
}

ビルドするノードを制限する

agentディレクティブでラベルを指定する。

agent {
    label 'slave1'
}

または、stepsディレクティブでnodeブロックを使う。

steps {
    node('slave1'){
    }
}

環境変数をセットする

environmentディレクティブが使える場合はその中で書く。ここでは、定数またはcredentials()しか使えない。また、credentials()SSHユーザ名と秘密鍵を扱えない。

environment {
    GOPATH = '/home/jenkins/go'

    // Jenkinsの資格情報に登録されている値を取り出す
    TOKEN = credentials('credential_id')
}

credentials()で取り出したものは、シークレットテキストの場合はそのまま使える。ユーザ名とパスワードの場合、TOKENはユーザ名とパスワードを:で区切った文字列になっている。個別に使いたい場合は、TOKEN_USRTOKEN_PSWのようにサフィックスを付けて扱うと良い。

または、stepsディレクティブの中でwithEnvブロックやwithCredentialsブロックを使う。withEnvの中でだけ環境変数が有効になる。

steps {
    withEnv(["GOPATH=${env.WORKSPACE}"]) {
    }
    withCredentials(bindings: [
        // シークレットテキスト
        string(credentialsId: 'credential_id', variable: 'TOKEN'),

        // ユーザ名とパスワード
        usernamePassword(credentialsId: 'credential_id',
         passwordVariable:'PASS',
         usernameVariable:'USER'),

        // ファイル
        file(credentialsId: 'credential_id', variable: 'FILE'),

        // SSH秘密鍵: passphraseVariable, usernameVariableは省略可
        sshUserPrivateKey(credentialsId: 'credential_id',
         keyFileVariable: 'KEY_FILE',
         passphraseVariable: '',
         usernameVariable: '')
    ]){
    }
}

withCredentials()の詳細はCredentials Binding Pluginを読むと良い。

Groovyを使ってもっと細かく制御したい場合。stepsディレクティブに直接Groovyは書けないのでscriptディレクティブを使う。

steps {
    script {
        // ここからGroovy
        env.GOPATH = env.WORKSPACE
    }
}

ツールをインストールする

Jenkinsの管理メニューにGlobal Tools Configuration があり、そこで事前にMavenやGoコンパイラなどのバージョンと名前を定義しておくと、toolsディレクティブで、自動でインストールすることができる。

pipeline {
    tools {
        go 'go1.10'
    }
}

Pipeline Syntaxで自動生成したコードはtool name: 'go1.9', type: 'go'のように出力されるが、このままDeclarative Pipelineとして記述するとエラーになる。$type '$name'のように置き換えて書く必要がある。

パラメータ付きビルドにする

parametersに何か書くと、パラメータ付きビルドとして扱われる。params.nameで参照する。

pipeline {
    parameters {
        string(name: 'TARGET', defaultValue: 'Tests', description: '説明')
        choice(name: 'STAGE', choices: 'staging\nproduction', description: '説明')
    }
    stages {
        stage('build'){
            sh "./make.bash ${params.TARGET}"
        }
    }
}

実行条件を設定する

stage()ブロックでwhenディレクティブを使う。以下の例は、ブランチ名がrelease-*にマッチした場合のみstageを実行する。

stage('stage'){
    when {
        branch 'release-*'
    }
}

他にも、環境変数の値を条件としたり、全て一致またはどれかが一致などの条件を作ることができる。

複数行のシェルスクリプトを実行

'''または"""で囲むと複数行のテキストを記述できる。

steps {
    sh '''
  date
  sleep 10
  date
  '''
}

'''の場合は変数展開などをせずにそのままシェルへ渡す。"""なら${env.PATH}のような展開を行う。

GitHub Branch Source Plugin環境でスレーブを使ってビルドする

基本的には何もしなくてもプラグインがcloneするが、デフォルトでは(おそらく)Jenkinsマスターのワークスペースへcloneされる。このため、ビルドノードがマスター以外の場合、ビルドするソースコードがスレーブのワークスペースにない状態となる。

stepsディレクティブでcheckoutを使うとslaveのワークスペースへcloneが行われる。

steps {
    checkout scm
}

または最初から、agentディレクティブでラベルを指定しておくと、対象となったスレーブでcloneされる。

pipeline {
    agent {
        label 'slave1'
    }
    steps {
        sh 'ls'
    }
}

特定のディレクトリへcloneする

optionsディレクティブで指定する。

options {
    checkoutToSubdirectory 'src/v2'
}

または、dir()を使うとカレントディレクトリを変更できるので、移動した後でcheckout scmを実行すればいい。移動するディレクトリが存在しない場合は自動的に作成される。

stage('chekout'){
    steps {
        dir("src/v2"){
            checkout scm
        }
    }
}

GitHub Branch Source Pluginでcheckoutの動作を変更する

How to Customize Checkout for Pipeline Multibranchによると、checkout scmは、

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions,
 userRemoteConfigs: scm.userRemoteConfigs
])

の省略形らしい。Shallow cloneとかcloneするディレクトリを変更する場合は、scm.extensionsに配列でオプションを追加する。

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions + [
        [ $class: 'CloneOption', noTags: false ],
        [ $class: 'RelativeTargetDirectory',
       relativeTargetDir: "src/v2"
        ],
    ],
 userRemoteConfigs: scm.userRemoteConfigs
])

Gitサブモジュールを使う

checkout scmSubmoduleOptionをセットします。

checkout([
 $class: 'GitSCM',
 branches: scm.branches,
 extensions: scm.extensions + [
        [ $class: 'SubmoduleOption',
       disableSubmodules: false,
       parentCredentials: true,
       recursiveSubmodules: true,
       reference: '',
       trackingSubmodules: false
        ],
    ],
 userRemoteConfigs: scm.userRemoteConfigs
])

ただし、GitHub Branch Source Pluginを使う場合、parentCredentialsGitHub APIトークンが使われているため、サブモジュールの参照もHTTPで行う必要があります。

checkoutの前にワークスペースをクリアする

checkout scmの前にdeleteDirを使う。

stage('clean'){
    steps {
        deleteDir()
    }
}
stage('checkout'){
    steps {
        checkout scm
    }
}

Scripts not permitted to use methodエラーで動作しない

Jenkinsfileのコードを実行した時、

RejectedAccessException: Scripts not permitted to use method (メソッド名)

というエラーで停止する場合がある。これは、外部のコードを無条件に実行すると危険なので、Script Security Pluginによってsandbox実行されているため、らしい。

外部のコードを制限するための機能なので、Jenkinsfileで回避できるものではない。エラーが発生した後であれば、Jenkinsの管理画面にエラーとなったメソッドの許可を求めるログが出ているので、そこでApprovalボタンを押せば次からはエラーを回避できる。このファイルは$JENKINS_HOME/scriptApproval.xmlに置かれているので、これをコピーしてもいい。

成果物を保存する

postセクションでArchive Artifact Pluginを使えばよい。

pipeline {
    post {
        success {
            archiveArtifacts artifacts: bin/*, fingerprint: true
       }
   }
}

成果物の保存数を制限する

いろいろ書き方はあるが、おそらくoptionsディレクティブを使うのが簡単。

pipeline {
    options {
        buildDiscarder(logRotator(numToKeepStr: '5', daysToKeepStr: '7', artifactNumToKeepStr: '5'))
    }
}

並列ビルドする

stepsparallelを使う。

steps {
    parallel(
     linux: {
            sh './make.bash -t linux_amd64'
        },
     windows: {
            sh './make.bash -t windows_amd64'
        }
    )
}

この例では、./make.bash -t linux_amd64./make.bash -t windows_amd64が並列実行される。

他のジョブをビルドする

buildを使う。

引数のパラメータはJenkinsを置いているフォルダまでのパスを渡す。相対パスの場合は呼び出し元ジョブのディレクトリがカレントになり、絶対パスの場合は$JENKINS_HOME/jobsがルートになる。

steps {
    build '../jobName1'
}

マルチブランチ構成のプロジェクトを呼び出す場合は、内部の階層がブランチごとに切られているので、ブランチ名も必要。

steps {
    build '../jobName1/master'
}

GitHub Branch Sourceが管理しているジョブはマルチブランチに近くて、Organizationの下にジョブが作られるので、次のようになる。

steps {
    build '/organizationName/jobs/jobName/master'
}

Jenkinsが管理しているCredentialでssh接続したい

ssh-agentプラグインをインストールするとsshagentブロックが利用可能になる。このブロックの中に記述したコマンドは、Jenkinsが管理している秘密鍵が追加されたssh-agentが動作してる状態で実行される。なのでJenkinsのアカウントでgit pushしたい場合は、以下のように書く。

steps {
    // jenkins_credential_id_strという名前のCredentialを読み込む
    sshagent(['jenkins_credential_id_str']){
        sh 'git push'
    }
}

bashのあまり知られていないけど便利な話

この記事はQiitaで公開されていました

~*を展開する場合はクオートしない

クオートされた文字列の中にある~はそのまま文字として扱われる。扱いは*と同じようにみえる。

$ echo ~/bin
/Users/kadota/bin
$ echo "~/bin"
~/bin

$ echo *.txt
hello.txt
$ echo "*.txt"
*.txt

$*$@の違い

基本的には同じ動きをするが、クオートの中で使うと文字区切りの扱いが変わる。

# star.sh
for i in $*; do echo "'$i'"; done

# star-quote.sh
for i in "$*"; do echo "'$i'"; done

# atom.sh
for i in $@; do echo "'$i'"; done

# atom-quote.sh
for i in "$@"; do echo "'$i'"; done

スペース($IFS)を含む場合の例。

$ bash star.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash atom.sh 1 "2 3" 4
'1'
'2'
'3'
'4'
$ bash star-quote.sh 1 "2 3" 4
'1 2 3 4'
$ bash atom-quote.sh 1 "2 3" 4
'1'
'2 3'
'4'

間接参照

rcでは変数の値を変数名として使うと便利だけど

% a=foo
% foo=1
% echo $$a
1

bashはそのまま実行すると、$$が先に解決されてPIDになってしまう。

$ a=foo
$ foo=1
$ echo $$a
1136a

正しくはこちら。

$ echo ${!a}

[[[test

シェルで分岐をする場合によく使うこれ。[testと同じコマンドなのでどちらを使ってもいい。

# if test $a -gt 1でも同じ

if [ $a -gt 1 ]
then
    echo $a greater than 1
else
    echo $a less than or equal to 1
fi

だけどこの2つはシェルとは別のコマンドなので、空白文字や空文字の場合に注意しないといけなくてめんどくさい。

$ a=""
$ [ $a = "" ] # unary operater expectedエラー
$ [ "$a" = "" ] # ok
$ a="1 2"
$ [ $a = "" ] # too many argumentsエラー
$ [ "$a" = "" ] # ok

bashを使うのであれば、今は常に[[を使うのが良いと思う。

$ a=""
$ [[ $a = "" ]]
$ a="1 2"
$ [[ $a = "" ]]

正規表現でマッチ

[[=~演算子を使うと正規表現が使える。

text='a b c'
if [[ $text =~ ^a.*$ ]]
then
    echo match
fi

ここでは、安易に正規表現をクオートしてはいけない。クオートすると、正規表現で特別な扱いされる文字が全てバックスラッシュでエスケープされる。シングルクオートとダブルクオートでどちらも同じ。

[[ $text =~ '^a.*$' ]] # \^a\.\*\$と同等

正規表現の途中でスペース等を含ませたい場合は、その部分だけクオートするとよい。

[[ $text =~ ^'a b'.*$ ]]

実際にどのようなエスケープがされるのかは、set -xしておくとエスケープされた後の正規表現を見ることができる。

if&&||

ifの代わりに、&&||が使える。エラー処理はこちらを使った方が読みやすい。

$ test -f file || echo 'file not exist' >&2 && exit 1

配列

簡単な使い方。

# 初期化
a=()

# 末尾に追加
a=("${a[@]}" test)

# 先頭に追加
a=(test "${a[@]}")

# 内容を列挙
echo "${a[@]}"

単純にecho $aすると、先頭の要素しか対象にならない。

配列に特定の要素が存在するか

専用の機能は無いが、[[コマンドの=~演算子が使える。

a=(cafe beef)
if [[ " ${a[@]} " =~ " cafe " ]]
then
    echo found
fi

=~正規表現でマッチさせる演算子なので、スペースで要素の区切りを表現してあげなければならない。

計算する

$((10 + 1))のように書くと外部コマンドなく計算ができる。

$ N=10
$ echo $((10 + $N))
20
$ echo $((1 * 3 - 1))
2

数字の前に基数を書ける。

$ echo $((16#0F + 8#10))
23

((expr))だけの場合は評価をするだけなので、条件式に使うと便利。

$ ((10 > 8#10)) && echo ok
ok

trap

trap は、SIGINT, SIGHUP などの他に以下の擬似シグナルが存在し、それらにハンドラを割り当てられる。

擬似シグナル 意味
ERR コマンド実行結果がエラーなら都度実行される
EXIT コマンドを終了したときに実行される
DEBUG コマンド実行のたびに都度実行される

一時ファイルを必ず削除したい場合はEXITに登録しておくと良い。利用可能なシグナルはtrap -lで表示できる。

変数展開

bashは通常のshと比べてとても多くの展開式が使える。

変数置換

古くからある展開式。

書き方 意味
${v:-text} vが未定義ならtextに置換
${v:=text} vが未定義ならtextをセットして置換
${v:?text} vが未定義ならtextを表示して終了
${v:+text} vが 定義されていたら textに置換

変数の少し変わった扱い方。

書き方 意味
${#v} 文字列の長さ、または配列の要素数に展開
${!v} vの値を変数名として展開

文字列置換

書き方 意味
${v%glob} vの末尾から最短マッチした部分を削除
${v%%glob} vの末尾から最長マッチした部分を削除
${v#glob} vの先頭から最短マッチした部分を削除
${v##glob} vの先頭から最長マッチした部分を削除
${v/s1/s2} vに含まれるs1を1つだけs2に置換
${v//s1/s2} vに含まれるs1を全てs2に置換

bash 4以降から利用可能な式

便利な式だけど、macOS High Sierraはまだ3系なので利用箇所には注意。

書き方 意味
${v^} 先頭の文字を大文字にして展開
${v^^} 全ての文字を大文字にして展開
${v,} 先頭の文字を小文字にして展開
${v,,} 全ての文字を小文字にして展開
${v~} 先頭文字の大文字小文字を反転
${v~~} 全ての文字で大文字小文字を反転

オプションを扱う

色々方法はあるけどgetoptsがいちばん面倒がなくて良いと思う。以下のコードでは、-fに引数がない・不明なオプションが与えられた時に、getoptsがエラーを出してくれる。

while getopts xf: OPT
do
    case $OPT in
    f)    filename="$OPTARG" ;;
    x)    flagx=1 ;;
    esac
done
shift $((OPTIND - 1))

だけど不明なオプションでもエラーとしたくない場合は少しめんどくさい。getopts最初の引数先頭に:をつけるとエラーにならない代わりに、特殊なオプション文字として:または?のどちらかが入っている。

文字 意味
? 不明なオプション文字が使われた
: 引数が必要なオプションだけど無かった

このように使う。

while getopts :xf: OPT
do
    case $OPT in
    :)  echo usage: $(basename $0) [-x] [-f file] >&2
        exit 1 ;;
    x)  flagx=1 ;;
    \?) x=$((OPTIND - 1))
        echo unknown option: ${!x} >&2
        exit 1 ;;
    esac
done
shift $((OPTIND - 1))

不明なオプションだった場合、具体的にどの文字が使われたのか調べる方法は特に用意されてなさそうだったので、上記のように自分で計算する。

先頭のアレ

シバン(shebang)というらしい。

#!/usr/bin/env bash

bashを使う場合、環境によってパスが違うかもしれないのでenvでラップするのをよく見かける。

Windows 10でBluetoothデバイスのPINコードが表示されない

Bluetoothバイスを追加するときに、PINコードを入力する必要があるけど、なぜか、本来は画面に表示されるべきPINコードがまったく表示されず、ペアリングができなくなる場合があった。

PINコード入力画面

バイス側のペアリング情報を削除しても変わらない。

この場合、設定アプリの「Bluetoothとその他デバイス」の画面下のほうにある「デバイスとプリンター」リンクを選び、コントロールパネル側の「デバイスとプリンター」画面を開く。次に、右クリックメニュー→デバイスとプリンターの追加を選んで作業を進める。途中でPINコードの入力が促されるので、テキストボックスの下にある「または、パスコードを接続先のデバイスで入力してください」リンクを開いて、そこから入力するといい。

この記事が参考になった。

nadegata.info