notebook

都内でWEB系エンジニアやってます。

GCPのCloud WorkflowsとCloud SchedulerでTogglの前日読書時間を自動でPixelaに記録する

今回の記事までにいくつかCloud Workflowsの記事を書いた

GCPのCloud Workflowsを試す - notebook

swfz.hatenablog.com

Cloud Workflowsでランタイム引数のデフォルト値を設定する - notebook

swfz.hatenablog.com

これらで行ったことをもとに今回はテーマを決めてワークフローを作ってみる

今回の題材は「Togglの特定プロジェクトの前日分の作業時間をPixelaにPUTする」

f:id:swfz:20210421200837p:plain

細かなサンプルを作る

基本的な動かし方は前回までの記事でわかったので一連の流れの中から切り出せるものを切り出してサンプルとして実行できるようにする

  • 実行時に引数を渡せるようにする
    • デフォルト値も指定可能にする(前回の記事があるので今回は割愛)
  • 昨日の日付文字列を取得する関数を呼び出す
    • あらかじめ適当な関数を作ってデプロイしておく
  • SecretManagerからAPIのTOKENを取得する
    • Toggl,Pixelaともに必要なのでサブワークフローにする
  • TogglのAPIをたたいて集計結果を取得する
  • PixelaのAPIをたたいてグラフにプロットする

昨日の日付文字列を取得する関数を呼び出す

Toggl,Pixelaともにリクエストを送るために日付情報が必要なのでランタイム引数で指定する以外にデフォルトで昨日の日付を設定する

というのをやりたかった

色々試行錯誤してみたがどうにもworkflowsのYAML内のみで行うのが厳しそうだったので簡単なCloudFunctionsのコードを書いた(関数の内容に関しては本記事からは割愛する)

Ruby 2.7(ベータ版)でこんな感じのレスポンスが返ってくる関数を書いた

$ curl -X GET -H "Authorization: Bearer $(gcloud auth print-identity-token)" 'https://us-central1-sample-project-111111.cloudfunctions.net/datetime/yesterday'
{"date":{"from":"2021-03-24","to":"2021-03-24"},"ymd":{"from":"20210324","to":"20210324"},"time":{"from":"2021-03-24T00:00:00","to":"2021-03-24T23:59:59"}}

で、この関数をたたく箇所が下記

  • toggl-to-pixela.workflow.yml(一部抜粋)
     - get_yesterday:
         call: http.get
         args:
           url: ${params.url + "/yesterday"}
           query:
             zone: Asia/Tokyo
           auth:
             type: OIDC
             audience: ${params.url}
         result: yesterday_res

params.url には上記関数のURLが入っている

SecretManagerへアクセスする

ほとんどコネクタのサンプルの内容をもらってサブワークフローにした

  • toggl-to-pixela.workflow.yml(一部抜粋)
get_token_from_secret_manager:
  params: [project, secret]
  steps:
    - get_secret:
        try:
          call: googleapis.secretmanager.v1.projects.secrets.versions.access
          args:
            name: ${"projects/" + project + "/secrets/" + secret + "/versions/latest"}
          result: secretResult
        except:
          as: e
          steps:
            - handle_secret_manager_error:
                switch:
                  - condition: ${e.code == 404}
                    raise: "Secret not found"
                  - condition: ${e.code == 403}
                    raise: "Error authenticating to Secret Manager"
            - unhandled_exception:
                raise: ${e}
    - return_secret:
        return: ${text.decode(base64.decode(secretResult.payload.data))}

TogglのAPIをたたいて集計結果を取得する

TogglのAPIのたたき方に合わせて設定する

レポートのAPIドキュメントは下記

toggl_api_docs/reports.md at master · toggl/toggl_api_docs

今回はSummaryReportを使うので次のドキュメントを見ながら設定する

toggl_api_docs/summary.md at master · toggl/toggl_api_docs

さきほど説明したSecretManagerからTOKENを取得するサブワークフローを呼んでAPI TOKENを引っ張ってきている

こちらも一連の流れをサブワークフローにまとめた

call_toggl_api:
  params: [project, params]
  steps:
    - get_toggl_token:
        call: get_token_from_secret_manager
        args:
          project: ${project}
          secret: TOGGL_API_TOKEN
        result: toggl_api_token
    - set_toggl_auth_value:
        assign:
          - basic_auth_value: ${base64.encode(text.encode(toggl_api_token + ":api_token"))}
    - get_toggl_value:
        call: http.get
        args:
          url: https://api.track.toggl.com/reports/api/v2/summary
          headers:
            Authorization: ${"Basic " + basic_auth_value}
          query:
            page: 1
            workspace_id: ${params.toggl.workspace_id}
            since: ${params.toggl.since}
            until: ${params.toggl.until}
            user_agent: api_test
            project_ids: ${params.toggl.project_id}
        result: toggl_res
    - log_toggl:
        call: sys.log
        args:
          text: ${json.encode_to_string(toggl_res)}
          severity: INFO

    - return_value:
        return: ${toggl_res}

PixelaのAPIをたたいてグラフにプロットする

PixelaのAPIのドキュメントは下記

PUT - /v1/users//graphs// - Pixela API Document

これに合わせてステップを設定する

TogglのAPIをたたくサブワークフローと同様にサブワークフローにまとめた

PixelaのAPIをたたくときに数値でも文字列にキャストしないとエラーになるのでそのへんだけ気を付ける

call_pixela_api:
  params: [project, params, toggl_value]
  steps:
    - get_pixela_token:
        call: get_token_from_secret_manager
        args:
          project: ${project}
          secret: PIXELA_API_TOKEN
        result: pixela_api_token
    - to_pixela:
        call: http.put
        args:
          url: ${"https://pixe.la/v1/users/" + params.pixela.user + "/graphs/" + params.pixela.graph_id + "/" + params.pixela.target_date}
          headers:
            X-USER-TOKEN: ${pixela_api_token}
          body:
            quantity: ${string(toggl_value)}
        result: pixela_res
    - log_pixela:
        call: sys.log
        args:
          text: ${json.encode_to_string(pixela_res)}
          severity: INFO

    - return_value:
        return: ${pixela_res}

各サブワークフローをつなげてワークフローを完成させる

値のハンドリング

Togglのレスポンス内容で前日に該当作業をしていなかった場合に分岐を発生させる

前日作業なしの場合はPixelaのAPIをたたかないようにする

具体的には次のようなレスポンスが返ってくるのでそれに合わせて条件分岐を設定する

  • 該当期間中にレコードがない場合
{
  "total_grand": null,
  "total_billable": null,
  "total_currencies": [],
  "data": []
}
  • 該当期間中にレコードがある場合
{
  "total_grand": 36600000,
  "total_billable": null,
  "total_currencies": [
    {
      "currency": null,
      "amount": null
    }
  ],
  "data": [
    {
      "id": 156548296,
      "title": {
        "project": "読書",
        "client": null,
        "color": "0",
        "hex_color": "#0b83d9"
      },
      "time": 36600000,
      "total_currencies": [
        {
          "currency": null,
          "amount": null
        }
      ],
      "items": [
        {
          "title": {
            "time_entry": "なぜ人と組織は変われないのか"
          },
          "time": 16727000,
          "cur": null,
          "sum": null,
          "rate": null
        },
        {
          "title": {
            "time_entry": "理科系の作文技術"
          },
          "time": 19873000,
          "cur": null,
          "sum": null,
          "rate": null
        }
      ]
    }
  ]
}

レコードがある場合とない場合でbody.dataの件数に違いがあるのでレスポンスによって書き分けるようにした(conditional_switchのステップ)

それらをまとめたmainのワークフローは下記のようになる

  • toggl-to-pixela.workflow.yml(一部抜粋)
main:
  params: [args]
  steps:
    - init_variables:
        assign:
          - project: ${sys.get_env("GOOGLE_CLOUD_PROJECT_NUMBER")}
    - build_parameters:
        call: build_params
        args:
          params: ${args}
        result: merged_params

    - get_toggl_value:
        call: call_toggl_api
        args:
          project: ${project}
          params: ${merged_params}
        result: toggl_res

    - conditional_switch:
        switch:
          - condition: ${len(toggl_res.body.data) > 0}
            steps:
              - to_min_value:
                  assign:
                    - toggl_value: ${toggl_res.body.total_grand / 1000 / 60}
              - log_value:
                  call: sys.log
                  args:
                    text: ${toggl_value}
                    severity: INFO
              - to_pixela:
                  call: call_pixela_api
                  args:
                    project: ${project}
                    params: ${merged_params}
                    toggl_value: ${toggl_value}
                  result: pixela_res

              next: return_value
          next: no_project_record

      - no_project_record:
          steps:
            - return:
                return: "not found target project time."

      - return_value:
          return: ${pixela_res}

スッキリまとまった(きがする)

mainのフロー図

f:id:swfz:20210421200844p:plain

IaC

動くものが作れるようになったのであとはIaCで管理できるようにする

TerraformでSchedulerとWorkflows、サービスアカウントを管理する

SecretManagerに関しては手動で登録した

functionsもTerraformとは別で管理するようにしてdataで関数を指定して必要な項目を指定できるようにした

少しつまずいたのがSchedulerからWorkflowsをたたくときの作法

Cloud Scheduler の使用によるワークフローのスケジュール設定  |  Google Cloud

cloud.google.com

をみて「なるほどね」って感じで設定したが実行時にパラメータを渡すときどうするのかは書いていなかった

そのため単純に渡したいJSONをhttp_target.bodyに突っ込んだだけではINVALID_ARGUMENTSで怒られた

REST Resource: projects.locations.workflows.executions  |  ワークフロー

cloud.google.com

上記のドキュメントからAPIでWorkflowsをたたくためのJSONの中身を調べた

argumentというキーにencodeしたパラメータのJSON文字列をいれてたたくと意図通りパラメータが渡されるようになった

あとはtfファイルを書くだけ

リポジトリ

今回使ったソースコードがあるリポジトリはこちらです

swfz/toggl-to-pixela-cloud-workflows

github.com

テンプレートリポジトリにしてあるのでREADME読めばある程度使えるかと思います

ぜひ使ってみてください

余談

今回色々触ってたらいくつか不具合っぽいものにあたってしまったのでissue投稿したところどちらも短期間で対応してくれた

  • Workflows

Assigned variables cannot be reference [184491211] - Visible to Public - Issue Tracker

  • terraform-provider-google

'Error: Provider produced inconsistent final plan' when using workflows and scheduler · Issue #8832 · hashicorp/terraform-provider-google

次回は自分でPR作れるようにGo言語書く機会持ちたいなーと思った