REST風にコマンドを呼び出せる(例えばhttp://localhost/bin/date　な)command-as-a-serviceのコンテナを作ってみた

# 概要

このエントリでは、REST風にOS上のコマンドを呼び出せるcommand-as-a-serviceのコンテナを作ってみたときに作業した内容について紹介します。

## 対象読者

以下の方を対象とします
- ネタが嫌いではない方
- 厳格なREST主義以外でもネタとして受け付けられる方
- お仕事でそのまま使ってやろう、と思う前にいろいろ自分で確認できる心の余裕がある方

## 動機

「http://localhost/bin/grep」とかでコマンド呼び出せたら、意味もなく面白いのでは、と思ったので。

## 関連

限られた1つのコマンドだけを実行したいときには、「[Goで外部コマンドを呼び出すのをHTTPリクエスト越しに実行してみる(Ginを使って)](https://qiita.com/hrkt/items/dbaba8874a2dcf3bd6eb)」も参照ください。Ginが対応しているものがいろいろ使えるので、カタく使うならおすすめです。

# TL;DR; 一言で言ってこういうものです

## 様子だけ知りたい方

コンテナを実行しておいて、

```console
$ docker run -p 8080:8080 hrkt/command-as-a-service:latest
```

curlでアクセスすると、こんな感じになります。

```console
$ curl "http://localhost:8080/bin/date"
Sat Jul 27 07:08:47 UTC 2019
```

標準入力にはPOSTのbodyを渡して、コマンドのオプションはクエリパラメーターで与えます。

```
$ curl "http://localhost:8080/usr/bin/sort?-r&-n" --data-binary @testdata/test.txt 
hello
goodbye
```

(補足)上記の例でのデータの内容

```
$ cat testdata/test.txt
goodbye
hello
```


## ご自身でも動かしてみたい方

Dockerhubで公開しておりますので、上記のコマンドの通り起動してみて、(デフォルトではWhitelistで制限された)コマンドを試してみてください。

# 何をやっているか

## URLのハンドル

Go-langのnet/httpを使い、自分でハンドラを書いています。

以下を行いました。

- リクエストのディレクトリをOSのコマンドへのパスに読み替えるために利用したい。一般的な用途に対して使いやすい、パスを固定的に関数にバインドしてくれるルータを使うと帰って大変になるので、一つのハンドラで足りるように書く
- OSのコマンドへのオプションとして利用するため、クエリパラメータを解析。goのモジュールのお便利関数を使うと解析結果がmapになってしまうので、そこを実装
- コマンドの実行は、別エントリで書いたものを踏襲。ただし、コマンド実行時のエラーハンドルを少しできるようにし、httpのリターンコードとして表現できるようにする

コードは↓な感じです。

```golang

func MyServer() http.Handler {
	return &myHandler{}
}

type myHandler struct {
}

func (f *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Println("called:" + r.URL.Path)
	buffer, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Printf(err.Error())
	}

	unescaped, _ := url.QueryUnescape(r.URL.RawQuery)
	params := strings.Split(unescaped, "&")
	log.Println(params)
	code, res := executeIt(r.URL.Path, string(buffer), params)
	if code == 400 {
		http.Error(w, res, http.StatusBadRequest)
		return
	}
	w.Write([]byte(res))
}
```

## OSコマンドの実行

OSのコマンド実行を全部できるようにしてしまうと、流石にイマイチではないかと考え、実行可能なコマンドは、設定ファイルでホワイトリストとして与えるようにしました。

(が、細かいことはいいんだよ、という目的も考え、設定ファイルで「DangerousMode」を有効化すると、何でもかんでも実行できるようにしてあります)

```golang
func executeIt(path string, requestBody string, params []string) (int, string) {

	if !appConfig.DangerousMode {
		_, ok := whitelistMap[path]
		if !ok {
			return 400, string("ERROR: command " + path + " not in the whitelist")
		}
	}

	var cmd *exec.Cmd
	if len(params) > 0 && params[0] == "" {
		cmd = exec.Command(path)
	} else {
		cmd = exec.Command(path, params[:]...)
	}
	cmd.Stdin = strings.NewReader(requestBody)
	var stdout bytes.Buffer
	cmd.Stdout = &stdout
	err := cmd.Run()
	if err != nil {
		log.Printf(err.Error())
		return 400, string("ERROR: command execution failed. reason: " + err.Error())
	}
	log.Println(stdout.String())
	return 0, string(stdout.String())
}
```

# まとめ

このエントリでは、REST風にコマンドを呼び出せるcommand-as-a-serviceのコンテナを作ってみるにあたり、あれこれ試してみた結果について紹介しました。

## 参照

- GitHub: https://github.com/hrkt/command-as-a-service
- DockerHub: https://cloud.docker.com/repository/registry-1.docker.io/hrkt/command-as-a-service


# (参考)開発スタイル

以下でやってます。

- GitHub使う
- GitHubの「Project」を使って、タスクをIssueとして書く
- コード書くのはmacOSでVScode、GitHub Desktopを併用
- CIは、CircleCI使う。PRのMerge時にテストこけないようにする
- 出来上がったコンテナは、DockerHubの自分のアカウントで公開する

この作業をやっていた2019/7/27は、Fuji Rockの中継をYoutubeでやっていたので、BGMとして楽しみました。