LoginSignup
3
1

More than 3 years have passed since last update.

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

Posted at

概要

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

対象読者

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

動機

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

関連

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

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

様子だけ知りたい方

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

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

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

$ 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のリターンコードとして表現できるようにする

コードは↓な感じです。


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」を有効化すると、何でもかんでも実行できるようにしてあります)

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

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1