GeekFactory

int128.hatenablog.com

golang.org/x/tools/go/packages による構文解析と型解析

golang.org/x/tools/go/packages を利用すると,抽象構文木や型情報を利用したコードが簡単に書けるので調べてみました.日本語の情報があまりないようです.

抽象構文木を表示する

Goのソースコードを読み込むには,packages.Load 関数を利用します.packages.Load 関数の引数にはどんな情報を解析してほしいかという設定を渡します.抽象構文木(AST)を取得するには下記のコードのようにフラグを渡します.ドキュメントを読むと packages.NeedSyntax だけで良さそうなのですが,実際に実行してみると複数のフラグが必要でした.

Load 関数の引数にはパッケージ名の配列を渡します../helloworld, os/exec, ./... のような表記が使えます.

package main

import (
    "go/ast"
    "log"
    "os"

    "golang.org/x/tools/go/packages"
)

func main() {
    config := &packages.Config{
        Mode: packages.NeedCompiledGoFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo,
    }
    pkgs, err := packages.Load(config, os.Args[1:]...)
    if err != nil {
        log.Fatalf("could not load packages: %s", err)
    }
    if packages.PrintErrors(pkgs) > 0 {
        log.Fatalf("error occurred")
    }
    for _, pkg := range pkgs {
        if err := ast.Print(pkg.Fset, pkg); err != nil {
            log.Printf("could not print the AST: %s", err)
        }
    }
}

packages.Load 関数はGoのソースファイルの配列を返します.例えば, hello.goworld.go を渡した場合はサイズ2の配列が返ってきます.配列の要素(packages.Package)にはファイル名,抽象構文木,型情報などが含まれています.

先ほどのコードでは ast.Print 関数を利用して抽象構文木をダンプしています.デバッグに便利です.

例えば,以下のソースコードhello.go):

package testdata

import (
    "github.com/pkg/errors"
)

// Hello says hello world!
func Hello() error {
    return errors.New("hello world")
}

に対応する抽象構文木は以下のようになります:

     0  *packages.Package {
     1  .  ID: "github.com/int128/hello-go-parser/testdata"
     2  .  Name: ""
     3  .  PkgPath: ""
     4  .  CompiledGoFiles: []string (len = 2) {
     5  .  .  0: "/src/hello-go-parser/testdata/hello.go"
     6  .  .  1: "/src/hello-go-parser/testdata/world.go"
     7  .  }
     8  .  ExportFile: ""
     9  .  Types: *types.Package {}
    10  .  Fset: *token.FileSet {}
    11  .  IllTyped: false
    12  .  Syntax: []*ast.File (len = 2) {
    13  .  .  0: *ast.File {
    14  .  .  .  Package: /src/hello-go-parser/testdata/hello.go:1:1
    15  .  .  .  Name: *ast.Ident {
    16  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:1:9
    17  .  .  .  .  Name: "testdata"
    18  .  .  .  }
    19  .  .  .  Decls: []ast.Decl (len = 2) {
    20  .  .  .  .  0: *ast.GenDecl {
    21  .  .  .  .  .  TokPos: /src/hello-go-parser/testdata/hello.go:3:1
    22  .  .  .  .  .  Tok: import
    23  .  .  .  .  .  Lparen: /src/hello-go-parser/testdata/hello.go:3:8
    24  .  .  .  .  .  Specs: []ast.Spec (len = 1) {
    25  .  .  .  .  .  .  0: *ast.ImportSpec {
    26  .  .  .  .  .  .  .  Path: *ast.BasicLit {
    27  .  .  .  .  .  .  .  .  ValuePos: /src/hello-go-parser/testdata/hello.go:4:2
    28  .  .  .  .  .  .  .  .  Kind: STRING
    29  .  .  .  .  .  .  .  .  Value: "\"github.com/pkg/errors\""
    30  .  .  .  .  .  .  .  }
    31  .  .  .  .  .  .  .  EndPos: -
    32  .  .  .  .  .  .  }
    33  .  .  .  .  .  }
    34  .  .  .  .  .  Rparen: /src/hello-go-parser/testdata/hello.go:5:1
    35  .  .  .  .  }
    36  .  .  .  .  1: *ast.FuncDecl {
    37  .  .  .  .  .  Doc: *ast.CommentGroup {
    38  .  .  .  .  .  .  List: []*ast.Comment (len = 1) {
    39  .  .  .  .  .  .  .  0: *ast.Comment {
    40  .  .  .  .  .  .  .  .  Slash: /src/hello-go-parser/testdata/hello.go:7:1
    41  .  .  .  .  .  .  .  .  Text: "// Hello says hello world!"
    42  .  .  .  .  .  .  .  }
    43  .  .  .  .  .  .  }
    44  .  .  .  .  .  }
    45  .  .  .  .  .  Name: *ast.Ident {
    46  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:8:6
    47  .  .  .  .  .  .  Name: "Hello"
    48  .  .  .  .  .  .  Obj: *ast.Object {
    49  .  .  .  .  .  .  .  Kind: func
    50  .  .  .  .  .  .  .  Name: "Hello"
    51  .  .  .  .  .  .  .  Decl: *(obj @ 36)
    52  .  .  .  .  .  .  }
    53  .  .  .  .  .  }
    54  .  .  .  .  .  Type: *ast.FuncType {
    55  .  .  .  .  .  .  Func: /src/hello-go-parser/testdata/hello.go:8:1
    56  .  .  .  .  .  .  Params: *ast.FieldList {
    57  .  .  .  .  .  .  .  Opening: /src/hello-go-parser/testdata/hello.go:8:11
    58  .  .  .  .  .  .  .  Closing: /src/hello-go-parser/testdata/hello.go:8:12
    59  .  .  .  .  .  .  }
    60  .  .  .  .  .  .  Results: *ast.FieldList {
    61  .  .  .  .  .  .  .  Opening: -
    62  .  .  .  .  .  .  .  List: []*ast.Field (len = 1) {
    63  .  .  .  .  .  .  .  .  0: *ast.Field {
    64  .  .  .  .  .  .  .  .  .  Type: *ast.Ident {
    65  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:8:14
    66  .  .  .  .  .  .  .  .  .  .  Name: "error"
    67  .  .  .  .  .  .  .  .  .  }
    68  .  .  .  .  .  .  .  .  }
    69  .  .  .  .  .  .  .  }
    70  .  .  .  .  .  .  .  Closing: -
    71  .  .  .  .  .  .  }
    72  .  .  .  .  .  }
    73  .  .  .  .  .  Body: *ast.BlockStmt {
    74  .  .  .  .  .  .  Lbrace: /src/hello-go-parser/testdata/hello.go:8:20
    75  .  .  .  .  .  .  List: []ast.Stmt (len = 1) {
    76  .  .  .  .  .  .  .  0: *ast.ReturnStmt {
    77  .  .  .  .  .  .  .  .  Return: /src/hello-go-parser/testdata/hello.go:9:2
    78  .  .  .  .  .  .  .  .  Results: []ast.Expr (len = 1) {
    79  .  .  .  .  .  .  .  .  .  0: *ast.CallExpr {
    80  .  .  .  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    81  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
    82  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:9:9
    83  .  .  .  .  .  .  .  .  .  .  .  .  Name: "errors"
    84  .  .  .  .  .  .  .  .  .  .  .  }
    85  .  .  .  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    86  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: /src/hello-go-parser/testdata/hello.go:9:16
    87  .  .  .  .  .  .  .  .  .  .  .  .  Name: "New"
    88  .  .  .  .  .  .  .  .  .  .  .  }
    89  .  .  .  .  .  .  .  .  .  .  }
    90  .  .  .  .  .  .  .  .  .  .  Lparen: /src/hello-go-parser/testdata/hello.go:9:19
    91  .  .  .  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    92  .  .  .  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    93  .  .  .  .  .  .  .  .  .  .  .  .  ValuePos: /src/hello-go-parser/testdata/hello.go:9:20
    94  .  .  .  .  .  .  .  .  .  .  .  .  Kind: STRING
    95  .  .  .  .  .  .  .  .  .  .  .  .  Value: "\"hello world\""
    96  .  .  .  .  .  .  .  .  .  .  .  }
    97  .  .  .  .  .  .  .  .  .  .  }
    98  .  .  .  .  .  .  .  .  .  .  Ellipsis: -
    99  .  .  .  .  .  .  .  .  .  .  Rparen: /src/hello-go-parser/testdata/hello.go:9:33
   100  .  .  .  .  .  .  .  .  .  }
   101  .  .  .  .  .  .  .  .  }
   102  .  .  .  .  .  .  .  }
   103  .  .  .  .  .  .  }
   104  .  .  .  .  .  .  Rbrace: /src/hello-go-parser/testdata/hello.go:10:1
   105  .  .  .  .  .  }
   106  .  .  .  .  }
   107  .  .  .  }
   108  .  .  .  Scope: *ast.Scope {
   109  .  .  .  .  Objects: map[string]*ast.Object (len = 1) {
   110  .  .  .  .  .  "Hello": *(obj @ 48)
   111  .  .  .  .  }
   112  .  .  .  }
   113  .  .  .  Imports: []*ast.ImportSpec (len = 1) {
   114  .  .  .  .  0: *(obj @ 25)
   115  .  .  .  }
   116  .  .  .  Unresolved: []*ast.Ident (len = 2) {
   117  .  .  .  .  0: *(obj @ 64)
   118  .  .  .  .  1: *(obj @ 81)
   119  .  .  .  }
   120  .  .  .  Comments: []*ast.CommentGroup (len = 1) {
   121  .  .  .  .  0: *(obj @ 37)
   122  .  .  .  }
   123  .  .  }
   124  .  .  1: *ast.File {
(中略)
   312  .  .  }
   313  .  }
   314  }

抽象構文木を探索する

抽象構文木を探索するには ast.Inspect を利用します.コールバック関数に抽象構文木のノードが渡されるので,ノードの型をチェックして必要な処理を行います.

例えば,以下を実行するとソースコードに含まれる import 文を抽出できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.ImportSpec:
                    log.Printf("import %s as %s", node.Path.Value, node.Name)
                }
                return true
            })
        }
    }
2019/09/03 18:06:32 import "github.com/pkg/errors" as <nil>

また,以下を実行すると関数やメソッドの呼び出しを抽出できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.CallExpr:
                    switch fun := node.Fun.(type) {
                    // foo.bar() 形式の呼び出し
                    case *ast.SelectorExpr:
                        log.Printf("call %s.%s with %d arg(s)", fun.X, fun.Sel, len(node.Args))
                    // foo() 形式の呼び出し
                    default:
                        log.Printf("call %s with %d arg(s)", fun, len(node.Args))
                    }
                }
                return true
            })
        }
    }
2019/09/03 19:09:59 call errors.New with 1 arg(s)

型情報を利用する

ast.Inspect 関数で得られるノードには型情報が含まれないため,関数やメソッドの呼び出し対象を判断するのが難しい問題があります.関数やメソッドの呼び出し対象は以下の種類があります.

import "fmt"

// 同じパッケージの関数
hello()

// 外部のパッケージの関数
fmt.Printf("hello world")

// メソッド
var x Hello
x.Hello()

そこで, packages.Load 関数が返す TypesInfo を利用すると,ノードの型を取得できます.例えば,以下を実行すると外部パッケージの関数に対する呼び出しを取得できます.

   for _, pkg := range pkgs {
        for _, syntax := range pkg.Syntax {
            ast.Inspect(syntax, func(node ast.Node) bool {
                switch node := node.(type) {
                case *ast.CallExpr:
                    switch fun := node.Fun.(type) {
                    // foo.bar() 形式の呼び出し
                    case *ast.SelectorExpr:
                        switch x := fun.X.(type) {
                        case *ast.Ident:
                            switch o := pkg.TypesInfo.ObjectOf(x).(type) {
                            // fooがパッケージの場合
                            case *types.PkgName:
                                // パッケージ名を取得
                                path := o.Imported().Path()
                                log.Printf("call %s.%s with %d arg(s)", path, fun.Sel, len(node.Args))
                            }
                        }
                    }
                }
                return true
            })
        }
    }
2019/09/03 19:53:03 call github.com/pkg/errors.New with 1 arg(s)

ここでは関数呼び出しのパッケージ名を取得しましたが,変数の型なども取得できます.

まとめ

golang.org/x/tools/go/packages を利用すると,抽象構文木や型情報を利用するコードが簡単に書けます.

EKSのGitLab RunnerでTerraformをCI/CDする

AWS EKSでGitLab Runnerを実行して,GitLab RunnerでTerraformを実行する方法を紹介します.

以下の流れで作業を行います.

  1. EKS workerにAssumeRoleのIAMポリシーをアタッチする.
  2. stable/kube2iamをデプロイする.
  3. GitLab RunnerのIAMロールを作成する.
  4. GitLab Runner Helm Chartをデプロイする.
  5. リポジトリ.gitlab-ci.yml を追加して,ビルドを実行する.

ここでは以下のツールを利用している前提とします.

  • GitLab
  • Terraform
  • helmfile

EKS workerのIAMポリシーの追加

kube2iamがAssumeRoleを実行できるように,EKS workerインスタンスにIAMポリシーをアタッチします. Terraformのeksモジュールを利用している場合は以下を追加します.

resource "aws_iam_policy" "worker_kube2iam" {
  name        = "hello-eks-worker-kube2iam"
  description = "kube2iam on EKS workers"
  policy      = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "sts:AssumeRole"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}

module "eks" {
  workers_additional_policies = [
    aws_iam_policy.worker_kube2iam.arn
  ]
}

kube2iamのデプロイ

stable/kube2iamKubernetesにデプロイします. helmfileを利用している場合は以下を追加します.

releases:
  # https://github.com/helm/charts/tree/master/stable/kube2iam
  - name: kube2iam
    namespace: kube-system
    chart: stable/kube2iam
    values:
      - host:
          iptables: true
          interface: eni+
        extraArgs:
          # https://github.com/jtblin/kube2iam#base-arn-auto-discovery
          auto-discover-base-arn: ""
        rbac:
          create: true

GitLab RunnerのIAMロールの作成

GitLab RunnerのPodにアタッチするIAMロールを作成します. ここではコードを簡単にするためPowerUserAccessポリシーをアタッチしていますが,実際にはGitLab Runner専用のIAMポリシーを書く方がよいでしょう.

data "aws_iam_policy_document" "kube2iam_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    principals {
      type        = "AWS"
      identifiers = [module.eks.worker_iam_role_arn]
    }
  }
}

resource "aws_iam_role" "gitlab_runner_terraform" {
  name               = "hello-gitlab-runner-terraform"
  assume_role_policy = data.aws_iam_policy_document.kube2iam_assume_role.json
}

resource "aws_iam_role_policy_attachment" "gitlab_runner_terraform" {
  role       = aws_iam_role.gitlab_runner_terraform.name
  policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}

kube2iamが正常に動作しているか,以下のコマンドを実行して確認します.

kubectl run aws -i --rm --image fstab/aws-cli --restart=Never \
  --overrides '{"metadata":{"annotations":{"iam.amazonaws.com/role":"hello-gitlab-runner-terraform"}}}' \
  -- /home/aws/aws/env/bin/aws s3 ls

S3バケットの一覧が表示されたら成功です.権限不足もしくはクレデンシャルが見つからないというエラーが表示された場合はkube2iamのログを確認してください.

GitLab Runnerのデプロイ

GitLab Runner Helm Chartをデプロイします.

releases:
  - name: gitlab-runner-terraform
    namespace: gitlab
    chart: gitlab/gitlab-runner
    values:
      - gitlabUrl: {{ .Environment.Values.gitlabURL }}
        runnerRegistrationToken: {{ .Environment.Values.gitlabRunnerRegistrationToken }}
        rbac:
          create: true
        runners:
          tags: "terraform"
          podAnnotations:
            # Switch to the role for Terraform
            iam.amazonaws.com/role: hello-gitlab-runner-terraform

上記ではterraformというタグが付いているビルドのみ受け入れるように設定しています.

.gitlab-ci.ymlの追加

リポジトリ.gitlab-ci.yml を追加して,ビルドを実行します.

image:
  name: hashicorp/terraform:light
  entrypoint:
    - /usr/bin/env

cache:
  paths:
    - .terraform

before_script:
  - terraform --version
  - terraform init

stages:
  - validate
  - build
  - deploy

validate:
  stage: validate
  script:
    - terraform validate
  tags:
    - terraform

plan:
  stage: build
  script:
    - terraform plan -out=plan.tfplan
  artifacts:
    name: plan
    paths:
      - plan.tfplan
  tags:
    - terraform

apply:
  stage: deploy
  script:
    - terraform apply -input=false plan.tfplan
  dependencies:
    - plan
  when: manual
  only:
    - master
  tags:
    - terraform

client-goでserviceのselectorに合致するpodを検索する

Kubernetesのクライアントであるclient-goを利用して,serviceのselectorに合致するpodを検索する方法を紹介します.コマンドラインではserviceの名前を受け取るが,実際の処理はpodに対して行う必要がある場合に活用できます.

クライアントの生成

準備として,コマンドライン引数からクライアントを生成します.genericclioptions.ConfigFlags を使うと,kubectlの標準的なフラグを簡単に追加できます.

func run() error {
    // コマンドライン引数を解析する
    f := pflag.NewFlagSet("example", pflag.ContinueOnError)
    type cmdOptions struct {
        *genericclioptions.ConfigFlags
    }
    var o cmdOptions
    o.ConfigFlags.AddFlags(f)
    if err := f.Parse(os.Args[1:]); err != nil {
        return xerrors.Errorf("invalid flag: %w", err)
    }

    // kubeconfigからクライアントを生成する
    config, err := o.ConfigFlags.ToRESTConfig()
    if err != nil {
        return xerrors.Errorf("could not load the config: %w", err)
    }
    namespace, _, err := o.ConfigFlags.ToRawKubeConfigLoader().Namespace()
    if err != nil {
        return xerrors.Errorf("could not determine the namespace: %w", err)
    }
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        return xerrors.Errorf("could not create a client: %w", err)
    }
}

実際のツール開発では,pflag を直接利用するのではなく cobra.Command を利用する方が便利かと思います.

serviceの取得とpodの検索

クライアントを利用してserviceを取得してみましょう.ここでは例として echoserver というserviceを取得します.

   serviceName := "echoserver"
    service, err := clientset.CoreV1().Services(namespace).Get(serviceName, metav1.GetOptions{})
    if err != nil {
        return nil, "", xerrors.Errorf("could not find the service: %w", err)
    }
    log.Printf("Service %s found", service.Name)

serviceに対応するpodを検索するには,serviceのselectorを利用します.例えば,echoserver サービスで app=echoserver というselectorが定義されている場合,サービスに対応するpodには app=echoserver というlabelが付いています.kubectlコマンドでserviceとpodの詳細を表示すると,selectorとlabelの関係がよくわかります.

% kubectl describe svc/echoserver
Selector:          app=echoserver

% kubectl describe pod/echoserver-xxx-xxx
Labels:         app=echoserver

クライアントを利用してserviceのselectorに合致するpodを検索します.

   // serviceで定義されているselectorをkey=value形式に変換する
    var selectors []string
    for k, v := range service.Spec.Selector {
        selectors = append(selectors, fmt.Sprintf("%s=%s", k, v))
    }
    selector := strings.Join(selectors, ",")
    // selectorに合致するpodを検索する
    pods, err := clientset.CoreV1().Pods(namespace).List(metav1.ListOptions{LabelSelector: selector})
    if err != nil {
        return nil, "", xerrors.Errorf("could not find the pods by selector %s: %w", selector, err)
    }
    log.Printf("Found pod(s): %+v", pods.Items)

これらのコードを実行すると,以下のような結果が表示されます.

2019/08/19 21:37:48 Service echoserver found
2019/08/19 21:37:48 Found pod(s): []Pod{...}

あとは,podのリストを利用して処理を行えばよいです.

まとめ

client-goを利用して,serviceのselectorに合致するpodを検索する方法を紹介しました.