an odd fellow

仕事のメモ

kustomizeのTimeoutについて

注意: kustomize1系の話。2系を使う場合以下は参考にならない。

背景

k8syamlを書くのにkustomizeを使っている。kustomizeの secretGenerator.commands で時間のかかる処理(GCSに置いてある秘匿情報を gsutil cat で取得するなど)をしたところ以下のエラーメッセージを吐いて終了してしまった。

Error: NewResMapFromSecretArgs: NewResMapFromSecretArgs: commands map[password:gsutil cat gs://myproject/mysql/password username:gsutil cat gs://myproject/mysql/username]: signal: killed

secretGenerator.commands に書かれた処理はデフォルトでは5秒で終了する

わからないことはググる。「kustomize "timeout"」でググった。timeoutをダブルクオーテーションでくくらないと有用な情報に当たらなかった。timeoutに完全一致させて検索すると以下のPRが一番上に来た。

Hard-wired timeout of 5s for secretGenerator commands · Issue #252 · kubernetes-sigs/kustomize · GitHub

secretGenerator.commands に書かれた処理は5秒でタイムアウトするらしい。このPRでは kustomize build--command-timeout というオプションを付けることを提案している。しかし、「 kustomize build にはオプション作りたくないんだ」と言われて却下されている。

TimeoutSeconds オプションができる

同じ課題を抱えていることはわかったので誰かが何とかしてくれているはずである。PRの検索欄にtimeoutと入れて探すと以下のPRが見つかる。

Allow setting shell and timeout in generatorOptions by Liujingfang1 · Pull Request #497 · kubernetes-sigs/kustomize · GitHub

TimeoutSeconds オプションができている。

結論

TimeoutSeconds オプションを付けるだけ。

secretGenerator:
  - name: db-secrets
    commands:
      username: "gsutil cat gs://myproject/mysql/username"
      password: "gsutil cat gs://myproject/mysql/password"
    TimeoutSeconds: 30

NginxでCORSの設定を入れるときはOPTIONSの処理を忘れない

server {
    listen 80;
...
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD";
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Expose-Headers x-total-count;
    if ($request_method = "OPTIONS") {
        return 204;
    }
...
}

OPTIONSにNGINXが返答しない場合、裏側のアプリケーションサーバーまで通信が行くが、そこで処理できない場合適当なエラーが帰る。

おまけ: Railsの場合

rack-corsがOPTIONSリクエストに対して200番を返してくれている。

Rack CORSコードリーディング

Reduxを使ってるときのAuth0の使い方

背景

ログイン処理にAuth0を使っている。Auth0はサンプルが豊富でReactを使ったサンプルも在るが、そのサンプルではReduxは使わずPure React構成のため、Reduxを使っている場合の使い方がイマイチ分からなかったので、考えて実装した結果をまとめておく。

どこで悩むか

基本的には Auth0 React SDK Quickstarts: Login を見てまず実装した上でRedux化していくのが良い。その上で悩みポイントは2つ。

  • Authオブジェクトで直接historyAPIを叩いているが、react-router-reduxを使っている場合それはできない
  • Authオブジェクトのインスタンス化のタイミング

AuthオブジェクトからhistoryAPIを叩く方法

react-router-reduxを使う場合historyAPIもActionを発行しReducerに処理させなければならない。そうなると store.dispatch をAuthオブジェクトは使わざるを得ない。

つまり公式のサンプルでは hitsoty.replace('/home') とやっているところを、 dispatch(push('/home')) とすれば良い。コードは以下のようになる。

Auth.js

import auth0 from 'auth0-js'
import { push } from 'react-router-redux'

export default class Auth {
  auth0 = new auth0.WebAuth({
    domain: AUTH0_DOMAIN
    clientID: AUTH0_CLIENT_ID,
    redirectUri: AUTH0_REDIRECT_URI
    responseType: 'token id_token',
    scope: 'openid'
  })

  constructor (dispatch) {
    this.login = this.login.bind(this)
    this.handleAuthentication = this.handleAuthentication.bind(this)
    this.isAuthenticated = this.isAuthenticated.bind(this)
    this.logout = this.logout.bind(this)
    this.dispatch = dispatch  // constructorの引数でdispatchをもらってthisに束縛する
  }

  login () {
    this.auth0.authorize()
  }

  handleAuthentication () {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        this.setSession(authResult)
        this.dispatch(push('/home'))  // ここが公式のサンプルでは history.replace('/home') になってる
      } else if (err) {
        console.log(err)
        this.dispatch(push('/hoe'))
      }
    })
  }

  setSession (authResult) {
    let expiresAt = JSON.stringify(
      authResult.expiresIn * 1000 + new Date().getTime()
    )
    localStorage.setItem('access_token', authResult.accessToken)
    localStorage.setItem('id_token', authResult.idToken)
    localStorage.setItem('expires_at', expiresAt)
    this.dispatch(push('/home'))
  }

  isAuthenticated () {
    let expiresAt = JSON.parse(localStorage.getItem('expires_at'))
    return new Date().getTime() < expiresAt
  }

  logout () {
    // Clear Access Token and ID Token from local storage
    localStorage.removeItem('access_token')
    localStorage.removeItem('id_token')
    localStorage.removeItem('expires_at')
    this.dispatch(push('/home'))
  }
}

ディレクトリ構成

Authにstore.dispatchを渡さなければならないため、Authオブジェクトのインスタンス化とStoreのインスタンス化のタイミングは同時のタイミングのほうが都合が良い。よってディレクトリの構成は以下のようにした。

公式のサンプルだと routes.js というのがあって、ルーティングの設定を切り出しているが、 App.js で全てやってしまった。コードは以下のようになる。

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux'
import createBrowserHistory from 'history/createBrowserHistory'
import App from './App'
import Auth from './auth'
import { createStore } from './store'

const history = createBrowserHistory()
const store = createStore(history)
const auth = new Auth(store.dispatch) // authのdispatchを渡す

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App auth={auth} />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
)

App.js

handleAuthenticationの引数にauthを追加しているのに注意。公式のサンプルだとauthがスコープにいたが、この構成だと明示的に渡してやる必要がある。

import React from 'react'
import { Route, Switch, Redirect } from 'react-router-dom'

const handleAuthentication = (nextState, replace, auth) => {
  if (/access_token|id_token|error/.test(nextState.location.hash)) {
    auth.handleAuthentication()
  }
}

export default ({ auth }) => (
  <Switch>
    <Route exact path='/login' render={() => <LoginPage auth={auth} />} />
    <Route
      exact
      path='/callback'
      render={props => {
        handleAuthentication(props, null, auth)
        return <p>ログイン中</p>
      }}
    />
  </Switch>
)

ログイン処理はこうなる

  1. handleAuthentication() の処理の完了を待たず "ログイン中" がレンダリングされる
  2. handleAuthentication() でログイン処理が終了すると push('/') がstoreにdispatchされ、react-router-reduxのRouterReducerがページ遷移を実行する

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

JavascriptのPromiseとasync/awaitについて調べる

背景

ReactでSPAを作っている。APIからデータを取得するときredux thunkとaxiosを使っているが、このときの処理がexampleを見て見よう見まねでやっていてよくわかっていない。

例えば投稿を取得するソースコードはこんな感じになっているが、return async とか axios.get のあとの then とかがわかっていない。

export const fetchPosts = () => {
  return async (dispatch, getState) => {
    const posts = getState().posts
    dispatch(actions.startRequest(posts))
    axios
      .get("http://example.com/posts")
      .then(response => {
        dispatch(
          actions.receiveData(
            posts,
            null,
            normalize(response.data, [schemas.post])
          )
        )
      })
      .catch(error => {
        dispatch(actions.receiveData(posts, error))
      })
      .finally(() => {
        dispatch(actions.finishRequest(posts))
      })
  }
}

これを理解するためにいくつかドキュメントにあたったり実験した結果をまとめる。

そもそも同期/非同期とは

この記事がわかりやすかった。

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる

console.log(1);
setTimeout(function(){console.log(2)}, 0);
console.log(3);

待ち時間がゼロなので、console.log(1)、console.log(2)、console.log(3)の順に実行されると勘違いしやすいです。 しかしあくまでlog(1)、setTimeout(〜略〜)、log(3)が順に、つまり同期的にキューに登録された後、setTimeout(〜略〜)が実行されたタイミングで、つまり非同期でlog(2)がキューに登録される

だから、出力は以下のようになる。

1
3
2

Promiseやasync/awaitを使うと

1
2
3

のように書いた順番通りに実行できるような仕組みのようだ。これを同期的と呼んでいる。

async/awaitの前にPromiseを理解するべきっぽい

適当にググっていると以下の記事を見つけた。そこにはこうある。

Promiseの使い方、それに代わるasync/awaitの使い方

async/awaitを理解するには、Promiseも知る必要がある。

なのでまずPromiseから調べる。

Promiseの基本的な使い方

コマンドライン引数がaかどうかでresolve、rejectを切り替える。

promise-basic.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic.js a
GOOD
$ node promise-basic.js b
BAD

Promise.resolveでPromiseのインスタンスを作れる。

promise-basic2.js

promise = Promise.resolve("GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-basic2.js
GOOD

resolveとかrejectの結果を変数に束縛できるか?

こんなような感じで書いて変数hogeに"GOOD"とか"BAD"とか入って欲しい。

promise-bind.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = promise.then(value=>value).catch(value=>value)
console.log(hoge)

実行結果

$ node promise-bind.js a
Promise { <pending> }

thencatch の中でごちゃごちゃと書くしか無いのかなあ…。

thenの中でreturnすれば外に出せるのでは説

thenreturn したら外に出せるのでは?と思ったけどダメだった。 promise-bind2.js

const promise = new Promise((resolve, reject) => {
  if (process.argv[2] === 'a') {
    resolve("GOOD")
  } else {
    reject("BAD")
  }
})

const hoge = (
  () => {promise.then(value=>return value).catch(value=> return value)}
  )()
console.log(hoge)

実行結果

$ node promise-bind2.js a   
/Users/roronya/src/github.com/roronya/async-await/promise-bind2.js:10
  () => {promise.then(value=>return value).catch(value=> return value)}
                             ^^^^^^

SyntaxError: Unexpected token return
    at new Script (vm.js:51:7)
    at createScript (vm.js:136:10)
    at Object.runInThisContext (vm.js:197:10)
    at Module._compile (internal/modules/cjs/loader.js:618:28)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
    at Module.load (internal/modules/cjs/loader.js:566:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
    at Function.Module._load (internal/modules/cjs/loader.js:498:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:695:10)
    at startup (internal/bootstrap/node.js:201:19)

resolveとかrejectとか書かなかったらどうなるのか?

何も起こらなかった。

promise-not-resolve.js

const promise = new Promise((resolve, reject) => "GOOD")

promise.then(value=>console.log(value)).catch(value=>console.log(value))

実行結果

$ node promise-not-resolve.js
# 何も出ない

Promiseを使った感想

  • Promiseの内部で使った値を呼び出し側に出せないのが使いづらい
    • もし値を使いたいなら関数を呼んでその引数にするしか無さそう
  • Promise.resolveは使い所無い気がする
  • Promiseが返ってくる関数は、resolveやrejectに何が渡されるのか実装によって調べる必要があるっぽい

async/awaitの使い方

asyncだけで使う

関数にasyncと付けると Promise.resolve(関数の返り値) と同じ結果になるっぽい。 async-basic.js

const f = async () => "GOOD"

console.log(f)
f().then(value => console.log(value))

実行結果

$ node async-basic.js
[AsyncFunction: f]
GOOD

asyncの中でだけawaitが使える

await-basic.js

const f = async () => "GOOD"
const g = async () => {
  const y = await f()
  console.log('inner', y)
  return y
}

g().then(value => console.log('outer', value))

実行結果

$ node await-basic.js
inner GOOD
outer GOOD

classメソッドでもasync使える?

使える

async-class.js

class C {
  async f() {
    return "GOOD"
  }
}

const c = new C()
console.log(c.f)
c.f().then(v => console.log(v))

実行結果

[AsyncFunction: f]
GOOD

async/await使った感想

  • 受け取る側でPromiseを処理すればasyncは気軽に付けてよい
  • await付けるとPromiseの中の値を外に持ってこれる
    • が、結局asyncによってPromiseに包まれるから、もともとの呼び出し側はPromise.then.catchは書かないとダメっぽい

Prettierとstandardを併用する

背景

今のプロダクトにlinterを入れたかった。今までは雑にググってヒットしたPrettierのみ使っていた。

Linterも入れたほうがいいらしい

PrettierはFormatterであってLinterではない。だからPrettierはコードの「この部分が良くないよ!」とは教えてくれず問答無用で整形だけしてくれる。「この部分が良くないよ!」を教えてもらうにはLinterが必要。JavascriptならESLintがデファクトスタンダード

eslint --fix で良くない?

おれもそう思ったが、2つの理由でPrettierも一緒に使ってもよいかなと思う。

まず、WebStormがPrettierに公式で対応していて使うのが簡単だから。Prettierがインストールされていれば何の設定も無しに Alt-Shift-Cmd-P を押せば整形してくれる。これは以前記事にも書いた。

prettierがいい感じ - an odd fellow

ふたつめが、横長過ぎるコードをESLintの --fix では整形できないが、Prettierはこれができる。なぜかというと、Prettierは書かれているコードをパースしてASTにして整形するから、らしい(What is Prettier? · Prettier)。だから横長過ぎるコードも妥当な位置で改行したりインデントを入れることができるようだ。

というわけでESLintとPrettierを併用することにした。

standard

ESLintには、Javascriptのコーディング規約に則っているかチェックしてもらいたい。Javascriptのコーディング規約は世の中にいくつかあるが、Javascript Standard Styleに従うことにした。

JavaScript Standard Style

Standard Style用にESLintの設定を書くのは骨が折れれるが、Standard StyleはGitHub - standard/standard: 🌟 JavaScript Style Guide, with linter & automatic code fixerというツールを提供している。これはESLintの設定をまるっとやって standard コマンドにラップしている。だからインストールして $ standard --fix と叩けばStandard Styleに則ったコードに整形される。素敵。

prettier-standardでPrettierとstandardを併用する

standardを使うと、今度困るのがPrettierで、PrettierのコードフォーマットもJavascript Standard Styleに従ってもらう必要がある。しかし、Prettierの設定でStandard Styleになるよう書くのもやはり骨が折れる。そこで調べているとprettier-standardというのがあった。

GitHub - sheerun/prettier-standard: (✿◠‿◠) Prettier and standard brought together!

これは $ prettier-standard ''/path/to/file' でstandardとPrettierを両方適用してくれる。素敵!!!

WebStormからprettier-standard呼べるの?

残念ながらWebStormから呼べるのはPrettier単体のみ。ただし、あとで説明するようにgit commit時にprettier-standardが走るように設定しておけば、書いている最中はどんな風に整形しても問題はない。だからPrettier単体で呼び出して使っています。ただし、セミコロンの設定だけは気になったから .prettierrc を作って ; は補完されないようにしている。

{
  "semi": false
}

git commit時にprettier-standardを走らせるようにする

commit hookはcloneしてから各自で設定する必要があり微妙に使いづらいが、huskyというライブラリを使うと、インストール時にcommit hookの設定を書き込んでくれる。git commit時に具体的にどのようなコマンドを実行するかは package.json に書けるようになるという素晴らしいライブラリ。これとlint-stagedというライブラリを併用すると、git commit時にコードフォーマットを当ててからgit commitするということが可能になる。この記事が詳しい。

コミット前に Lint を強制するなら lint-staged が便利

結局どうなったの

package.jsonに以下のように書き足すことでgit commit時にprettier-standardが走って全JSファイルをフォーマットしてくれる。

{
  "scripts": {
    "format": "prettier-standard 'src/**/*.js'"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": [
      "yarn format",
      "git add"
    ]
  },
}

関数の引数にスプレッド構文を使った場合

背景

React書いていると以下のような...propsのような記述がたまに見られるが、どういう振る舞いになるのか少し混乱したから整理する。

const DeleteButton = ({className, ...props}) => (
  <span className={[style.root, className].join(' ')} {...props}>
    <TrashCanIcon />
    <Baloon>削除する</Baloon>
  </span>
);

スプレッド構文

ドットを3つつなげたこれはスプレッド構文と呼ばれている。Iterableなオブジェクトの中身を展開してくれるようだ。

スプレッド構文 - JavaScript | MDN

これらは以下の3つの場面で使うことができる。 1. 関数呼び出し 2. Arrayリテラル 3. Objectリテラル

2,3は想像が付くんだが関数呼び出しのときの振る舞いがうまく想像できない。

実験

とりあえず使ってみる。

const hage = ({...props}) => {console.log(props)};
hage({a:1});
// => {a: 1}

const hage=(props) => {console.log(props)};と変わらないっぽい? propsの中身を一度展開し、再度Objectにしたものをpropsという名前で束縛しているのか? そもそも({a, b}) => {}って引数に渡されたObjectからabのプロパティだけを抽出する構文だよね?なんかおかしくね?

const hige = ({...props}) => {console.log(a)};
hige({a: 1});
// => ReferenceError: a is not defined

これでa1が束縛されるんじゃないかと思ったんだけどな…。

propsの中身を一度展開し、再度Objectにしたものをpropsという名前で束縛している?

これが正解っぽい

const huge = ({a, ...props}) => {console.log(a, props)};
huge({a: 1, b: 2, c: 3});
/// => (1, {b: 2, c: 3})

なるほど?...を付けたら問答無用で残りは引き渡ってくれるのかー。

結論

というわけで最初の例はなんでもごちゃごちゃに渡されてくるpropsからclassNameだけをチュ出し、残りの要素は再度propsに束縛するという感じか。納得はしてないが振る舞いはわかった。

再掲

const DeleteButton = ({className, ...props}) => (
  <span className={[style.root, className].join(' ')} {...props}>
    <TrashCanIcon />
    <Baloon>削除する</Baloon>
  </span>
);

storybookに階層構造を追加する

背景

デフォルトでは1階層しかパスが切れない。

例えばAtomsのButton、MoleculesのFormがあったとして、理想的には以下のように整理したい。

  • Atoms
    • Button
      • with text
      • with some emoji
  • Molecules
    • Form
      • ContactForm
      • MailAddressForm

しかしデフォルトでは以下のようにするしかない。

  • Button
    • with text
    • with some emoji
  • Form
    • ContactForm
    • MailAddressForm

@storybook/addon-chaptersを使うと実現できるらしい。おれは以下の本を読んでこのアドオンの存在を知った。

Atomic Design ~堅牢で使いやすいUIを効率良く設計する | 五藤 佑典 | コンピュータ・IT | Kindleストア | Amazon

公式のaddon galleryでも紹介されている。

https://storybook.js.org/addons/addon-gallery/#chapters

階層構造と読んでいたがChapterと言うようだ。

@storybook/addon-chapters

install

npmを見る。

@storybook/addon-chapters - npm

storiesOf('Atoms', module)
  .addChapter('Button', chapter=>chapter
    .add('with text', () => <Button>with text</Button>)
    .add('with som emoji', () => <Button>with some emoji</Button>)
  );
storiesOf('Molecules', module)
  .addChapter('Form', chapter=>chapter
    .add(
    ...

というような感じで階層構造を追加出来た。

storybook-directory-chapters

components以下のdirectory構造をそのままstorybookにできるnpmパッケージもあったので試してみたが動かないようだ。 storybook-directory-chapters - npm

設定で module.exports を使っているがimport文も使っておりEJSとCJSの仕様がごちゃまぜになってる。module.exportsexports defaultに書き換えたが今度はcontext(...) is not a functionというエラーが出てしまい解消できなかった。