Fine Software Writings ( www.aoky.net ) にアクセスできなくなっている

11月くらいから Fine Software Writings にアクセスできなくなっている。

閉鎖したという話は聞いてないし、まったく騒がれていないので、自分の環境の問題かもと考えたが自宅でもモバイルでもアクセスできない。

Waybach Machineを見るかぎり9/14にはアクセスできていた模様。

web.archive.org

著者のかたも心配だし、あれだけの良質な文章がインターネットの海に消えるかもしれないことも心配。

Pull Requestで最新のコメント以外は最小化するやつ

github.com

デモを見るとやりたいことはわかると思う。

https://github.com/user-attachments/assets/0da3da2f-f649-4563-84bf-b7c7cb7d9f82

terraformのGitHub Actionsのドキュメントでは古いコメントを削除しているが、Atlantisでは --hide-prev-plan-comments オプションで古いコメントを最小化するようになっており、その開発体験がよかったのでほかのツールでも同じようなコメントができるようなCLIを作成した。

使い方

-o にオーナー、-rリポジトリ名、-n にIssue/Pull Request番号を指定し、引数のファイルを読み込んでコメントする。 - を指定すると標準入力を読み込む。

# https://github.com/winebarrel/hello-world/pull/12
echo "hello my comment" | lastcmt -o winebarrel -r hello-world -n 12 -

実装について

コメントの先頭に <!-- lastcmt: {{ id }} --> というMarkdownコメントをつけて、lastcmtからの投稿を識別して最小化している。

GitHub APIについて

なぜかv3にはminimizeCommentのAPIがないのでshurcooL/githubv4でv4 APIをたたくようにした。

github.com

v4 APIだとIssueとPull Requestを単純に同じものとして扱えないので、やや冗長なクエリになった。

https://github.com/winebarrel/lastcmt/blob/main/client.go#L113-L140

   var q struct {
        Repository struct {
            IssueOrPullRequest struct {
                Issue struct {
                    Id       githubv4.ID
                    URL      string
                    Comments struct {
                        Nodes    []comment
                        PageInfo struct {
                            EndCursor   githubv4.String
                            HasNextPage bool
                        }
                    } `graphql:"comments(first: 100, after: $issueCursor)"`
                } `graphql:"... on Issue"`
                PullRequest struct {
                    Id       githubv4.ID
                    URL      string
                    Comments struct {
                        Nodes    []comment
                        PageInfo struct {
                            EndCursor   githubv4.String
                            HasNextPage bool
                        }
                    } `graphql:"comments(first: 100, after: $pullRequestCursor)"`
                } `graphql:"... on PullRequest"`
            } `graphql:"issueOrPullRequest(number: $number)"`
        } `graphql:"repository(owner: $owner, name: $repo)"`
    }

宣言的スキーマ管理のためのデータマイグレーション

宣言的スキーマ管理をする場合に「データマイグレーションをどうするか」という課題があって、Ridgepoleについては「データマイグレーションはほかのツールでやってほしい」というポリシーで開発してきた。

C社にいた頃はLiquibaseを使って「GitHubSQLファイルがマージされたら冪等に実行する」という社内ツールを作ったが、現職でも宣言的スキーマ管理をするケースが増えて「データマイグレーションをどげんかせんといかん」という機運になってきたので、テータマイグレーション用のツールを作った。

github.com

qrevはSQLファイルを実行し、履歴を管理するツール。

設計方針は以下の通り。

  • up・downという概念はない。ロールバックもない
  • 成否にかかわらず一回実行されたら再実行はしない
  • ただし失敗したSQLファイルを編集したら再実行できる

極めてシンプルなツールだが、データマイグレーションには必要十分ではないかと考えている。

開発フローに投入して様子見中。

テストの時だけメソッドをはやす

main.go

package main

import "fmt"

type Foo struct {
    I int
    J int
}

func (foo *Foo) Sum() int {
    return foo.I + foo.J
}

func main() {
    foo := &Foo{1, 2}
    fmt.Println(foo.Sum())
}

helper_test.go

package main

import (
    "fmt"
)

func (foo *Foo) Dump() string {
    return fmt.Sprintf("%+v", foo)
}

main_test.go

package main_test

// ↓ではメソッドをはやせない
// type Foo = main.Foo
// type Foo main.Foo

import (
    main "hello"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHello(t *testing.T) {
    assert := assert.New(t)
    foo := &main.Foo{1, 2}
    assert.Equal(3, foo.Sum())
    t.Log(foo.Dump())
}

terraform-provider-googlesheetsを作っていた

terraformからGoogleスプレッドシートの値を取得したいことがたま〜にあるので、少し前にスプレッドシート取得用のproviderを作った。

github.com

サービスアカウントにスプレッドシートを共有して

シート名とレンジを指定するとその範囲のデータをJSON文字列としてとってこれる

provider "googlesheets" {
  credentials_json = file("credentials.json")
  # credentials_env = "CREDS_ENV_NAME"
}

data "googlesheets_sheet" "my_sheet" {
  sheet_id = "..."
  range    = "シート1!A1:B2"
}

output "values" {
  value = jsondecode(data.googlesheets_sheet.my_sheet.json)
}

googlesheets_sensitive_sheetリソースを使うとsensitiveな値として扱える。

data "googlesheets_sensitive_sheet" "my_sheet" {
  sheet_id = "..."
  range    = "シート1!A1:B2"
}

output "sensitive_values" {
  value     = jsondecode(data.googlesheets_sensitive_sheet.my_sheet.json)
  sensitive = true
}

エフェメラルリソースもある。

ephemeral "googlesheets_sheet" "my_sheet" {
  sheet_id = "..."
  range    = "シート1!A1:B1"
}

provider "any_other" {
  api_key = jsondecode(ephemeral.googlesheets_sheet.my_sheet.json)[0][0]
}

pgx/v5でstring配列をやりとりする

github.com/jackc/pgx/v5 v5.7.6 での検証

pgx.Conn

pgx.Connを使っている場合は、[]stringをそのまま使えるのであまり考える必要はない。

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jackc/pgx/v5"
)

func main() {
    ctx := context.Background()
    conn, err := pgx.Connect(ctx, "postgres://postgres@localhost:5432/postgres")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(ctx)

    // CREATE TABLE users (
    //  id    SERIAL PRIMARY KEY,
    //  name  VARCHAR(100),
    //  email TEXT,
    //  tags  VARCHAR(50)[]
    // );

    var name string
    var tags []string

    emails := []string{"sugawara@example.com", "hoge@example.com"}
    err = conn.QueryRow(ctx, "SELECT name, tags FROM users WHERE email = ANY($1)", emails).Scan(&name, &tags)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(name, tags) //=> sugawara [foo bar zoo]
}

stdlib

プレースホルダーにstring配列を渡す

プレースホルダーには[]stringをそのまま渡せる。

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
    db, err := sql.Open("pgx", "postgres://postgres@localhost:5432/postgres")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // CREATE TABLE users (
    //  id    SERIAL PRIMARY KEY,
    //  name  VARCHAR(100),
    //  email TEXT,
    //  tags  VARCHAR(50)[]
    // );

    var name string

    emails := []string{"sugawara@example.com", "hoge@example.com"}
    err = db.QueryRow("SELECT name FROM users WHERE email = ANY($1)", emails).Scan(&name)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(name) //=> sugawara
}

結果のstring配列を受け取る

string配列を受け取る場合はpgtype.NewMapを使う必要がある。

package main

import (
    "database/sql"
    "fmt"
    "log"

    "github.com/jackc/pgx/v5/pgtype"
    _ "github.com/jackc/pgx/v5/stdlib"
  // "github.com/lib/pq"
)

func main() {
    db, err := sql.Open("pgx", "postgres://postgres@localhost:5432/postgres")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // CREATE TABLE users (
    //  id    SERIAL PRIMARY KEY,
    //  name  VARCHAR(100),
    //  email TEXT,
    //  tags  VARCHAR(50)[]
    // );

    var tags []string

    err = db.QueryRow("SELECT tags FROM users WHERE name = $1", "sugawara").Scan(pgtype.NewMap().SQLScanner(&tags))
    // あるいは pq.Arrayを使う https://pkg.go.dev/github.com/lib/pq#Array
    //err = db.QueryRow("SELECT tags FROM users WHERE name = $1", "sugawara").Scan(pq.Array(&tags))

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(tags) //=> [foo bar zoo]
}

以下のIssueではpgtype.FlatArrayを使っているが不要なようだった。

github.com

https://github.com/jackc/pgx/blob/61d3c965ad442cc14d6b0e39e0ab3821f3684c03/stdlib/sql.go#L58-L65

// # PostgreSQL Specific Data Types
//
// The pgtype package provides support for PostgreSQL specific types. *pgtype.Map.SQLScanner is an adapter that makes
// these types usable as a sql.Scanner.
//
// m := pgtype.NewMap()
// var a []int64
// err := db.QueryRow("select '{1,2,3}'::bigint[]").Scan(m.SQLScanner(&a))

Google Apps Scriptからのアクセスを雑に認証する

Google Apps ScriptからプライベートなサービスのAPIを呼び出す場合、何らかの認証が必要になるので、GASから取得したOAuth2アクセストークンを検証するプロキシサーバを作ってみた。

github.com

仕組み

ScriptApp.getOAuthToken()を使うと有効なユーザーの OAuth 2.0 アクセス トークンを取得できる。※appscript.jsonの修正が必要

// appscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "openid",
    "email",
    "https://www.googleapis.com/auth/script.external_request"
  ]
}

トークンを使ってさらにユーザー情報を取得できる。

$ curl -H "Authorization: Bearer ${TOKEN}" https://openidconnect.googleapis.com/v1/userinfo
{
  "sub": "...",
  "picture": "...,
  "email": "sugawara@winebarrel.jp",
  "email_verified": true,
  "hd": "winebarrel.jp"
}

作成したプロキシサーバではユーザー情報のemailが許可されているかを検証する。

使い方

$ docker run --rm ghcr.io/winebarrel/gap --help
Usage: gap --backend=BACKEND --port=UINT --header-name=STRING --allow-list=ALLOW-LIST,... [flags]

Flags:
  -h, --help                  Show help.
  -b, --backend=BACKEND       Backend URL ($GAP_BACKEND).
  -p, --port=UINT             Listening port ($GAP_PORT).
  -n, --header-name=STRING    Header name to pass the access token
                              ($GAP_HEADER).
  -e, --allow-list=ALLOW-LIST,...
                              Allowed email list that may contain wildcards
                              ($GAP_ALLOW_LIST).
      --version

バックエンドのURL、リッスンポート、アクセストークンを渡すヘッダ名、許可するメールアドレスを指定してプロキシサーバを起動する。

$ docker run --rm -p 8080:8080 ghcr.io/winebarrel/gap -b https://example.com -e '*@winebarrel.jp' -p 8080 -n x-my-gap-token

正しいアクセストークンを渡すとプロキシサーバからバックエンドにアクセスできる。

$ curl -s -H "x-my-gap-token: ${TOKEN}" localhost:8080
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

間違ったアクセストークンを渡すとはじかれる。

$ curl -s -H "x-my-gap-token: ${TOKEN}x" localhost:8080
forbidden

GASからはUrlFetchApp.fetch()でアクセスできる。

function myFunction() {
  const token = ScriptApp.getOAuthToken();

  const response = UrlFetchApp.fetch('https://api.my-private.example.com/foo/bar/zoo', {
    headers: {
      "x-my-gap-token": token
    }
  });

  console.log(response.getContentText());
}

GASをスプレッドシートのカスタム関数として使っている場合、関数から直接アクセストークンを取得することはできないが、メニューのアイテムとしてScriptApp.getOAuthToken()を呼び出してキャッシュに保存することで、カスタム関数内でアクセストークンを使うことができる。

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu("認証").addItem("認証", "auth").addToUi();
}

function auth() {
  const cache = CacheService.getUserCache();
  const token = ScriptApp.getOAuthToken();
  cache.put("token", token);
}

function myFunction() {
  const cache = CacheService.getUserCache();
  const token = cache.get("token");
  // ...