Goのinterface
Goで単体試験などをしたくなった場合、テスト対象となる機能が呼び出す関数を外から渡したくなる。 そんなときはinterfaceを使うことで実現できる。
こんな感じ
package main import "fmt" // インタフェースとして、どんな関数を具備している必要があるかを定義。 type Speaker interface { Greeting() } // そのインタフェースで定義された関数を使う関数を定義 // 単体テストのことを想定しているならば、これがテスト対象 func Run(i Speaker) { i.Greeting() } // インタフェースを持たせる構造体を定義 type Japanese struct { } // インタフェースに合うように、インタフェースで定義されている // 関数を構造体に対して実装 func (p *Japanese)Greeting() { fmt.Println("こんにちは") } // もうひとつ、同じインタフェースを持たせる別の構造体を定義しておいて、 type American struct { } // 先ほどと同じく、インタフェースに合うように、インタフェースで定義されている // 関数を構造体に対して実装 func (a *American)Greeting() { fmt.Println("Hi!") } // 使ってみる func main() { // Japanese構造体を渡してもちゃんと実行できるし、 japanesePeople := &Japanese{} Run(japanesePeople) // American構造体を渡してもちゃんと実行できる americanPeople := &American{} Run(americanPeople) }
Redmineを試したくなった
Dockerを使うのが簡単だ。
docker -p 3000:3000 redmine
管理者の初期アカウントはadmin、パスワードもadmin。
管理者権限のあるアカウントでログインして、以下の作業を実施していく。 ページ最上部の左寄りの場所に「管理」というリンクがあるのでそこをクリックして管理画面に遷移。各項目の設定画面へのリンクがあるのでそちらで作業する。
- ユーザを作る
- ユーザはAPIでも作ることができるので、何かのシステムと同じユーザを作るなどといったことも簡単にできる。
- ロールを作る
- ステータスを作る
- トラッカーを作るのに必要
- トラッカーを作る
- プロジェクトを作る
- APIで作ることも可能、だけど、そんなにたくさん作るものでもないのであまり意味ないか?
- プロジェクトにユーザを追加する、同時にそのプロジェクトにおけるそのユーザのロールも設定する。
- チェックボックスになっているので、同じロールの人であれば複数のユーザを一気に登録可能。
- 優先度を作る
- チケットを作るときに必要
- 設定の中の値のリストの中にあるので注意
この辺までいじって、自分がやりたいことができないことがわかったので、終了。 (複数のグループをissueに割り当てたかった)
ginを使って作成したWebアプリをsystemdを使ってデーモンとして動かす
ginを使って作成したWebアプリをデーモンとして動かす方法について、少し前の記事でsupervisordを用いる方法を延々書いてしまったが、ここではsystemdを使う方法を書いてみる。
1. ログをsyslogに吐くようにする。
デーモン化したらログは画面ではなくファイルに出したいところ。 Linuxで動かす(よね?)なら、syslogを使ってしまうのがお手軽だ。
logger, err := syslog.New( syslog.LOG_NOTICE|syslog.LOG_USER, "SampleWebApp") if err != nil { panic(err) } gin.SetMode(gin.ReleaseMode) gin.DisableConsoleColor() gin.DefaultWriter = logger
2. systemd用のユニットファイルを作成する。
たぶん、これくらいが一番シンプルなんじゃないかな。。
[Unit] Description=Sample Web Application [Service] ExecStart=/usr/local/bin/SampleWebApp KillMode=process Restart=always [Install] WantedBy=multi-user.target
3. sytemctlコマンドで起動する。
systemctl restart サービス名
4. おまけ firewalldでポートを開放する
とりあえず今の状態を確認して、
firewall-cmd --list-all
たとえばこんなコマンドを打って、
firewall-cmd --add-port=3000/tcp --zone=public --permanent
そのあとリロードしてから、
firewall-cmd --reload
再度状態を確認すると、
firewall-cmd --list-all
先ほど追加したポートが開放されていることがわかるはず。
ginのログ出力先をsyslogにする
こんなかんじ
package main import "github.com/gin-gonic/gin" import "log/syslog" func main() { logger, err := syslog.New( syslog.LOG_NOTICE|syslog.LOG_USER, "gin-test") if err != nil { panic(err) } gin.SetMode(gin.ReleaseMode) gin.DisableConsoleColor() gin.DefaultWriter = logger app := gin.Default() grp := app.Group("/api") grp.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "ok"}) }) app.Run("localhost:3000") }
まぁでもsupervisorとか使うだろうからログも標準出力にだして、あとはsupervisorに任せればよいでしょう。(※と、当初書いたけれど、systemdでやればいいんじゃないのって言われたが、とりあえず記事はそのまま残しておく。)
RHEL8系の場合、
sudo dnf install epel-release
ってやって(すでにやってあれば当然不要)
sudo yum -y install supervisor
とかやるとインストールされ、設定ファイルは /etc/supervisord.conf になっているので、そのなかに設定を追記するなり、設定ファイルをよく見るとわかるように /etc/supervisord.d/*.iniが読み込まれるようになっているのでそれにマッチする個別の設定ファイルを置くなりして自分のプログラムがsupervisordに起動されるようにすればよい。
例えば自分のプログラムがapiという名前で識別されるようにしたかったら、/etc/supervisord.d/api.ini などといった名前のファイルを作って、その中に
[program:api] command=/usr/local/bin/api autostart=true autorestart=true stderr_logfile=/var/log/api.err.log stderr_logfile=/var/log/api.out.log
などと書いてやればよいだろう。
ってsupervisorの話のほうが長くなった。
firewalldの設定を確認する方法
firewall-cmd --list-all
それだけ。
ginでAPIサーバを作る
ディレクトリをほって初期化する。 モジュール名のところはリポジトリ名にするんだよ、とする情報が多くて、正しいのだけれど、公開しないものであれば別にそのフォルダ名とかでも特に支障はない(たぶん)。
mkdir XXXX cd XXXX go mod init モジュール名
DBを立ち上げるdocker-compose.ymlをかく
services: db: image: postgres:latest container_name: postgresql ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - ./pgdata:/var/lib/postgresql/data - ./pginit:/docker-entrypoint-initdb.d
初期化して初期データをつっこむSQLを書く
DROP TABLE IF EXISTS Users; CREATE TABLE Users ( user_id SERIAL, username VARCHAR(128), account VARCHAR(64), password VARCHAR(64) ); INSERT INTO Users(username, account, password) VALUES('あ', 'user01', 'user01'); INSERT INTO Users(username, account, password) VALUES('い', 'user02', 'user02'); INSERT INTO Users(username, account, password) VALUES('う', 'user03', 'user03'); INSERT INTO Users(username, account, password) VALUES('え', 'user04', 'user04'); INSERT INTO Users(username, account, password) VALUES('お', 'user05', 'user05');
勢いで書く。
package main import ( "database/sql" "fmt" "log" "strconv" "strings" "time" _ "github.com/lib/pq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" ) const SECRETKEY = "secretkey" func main() { // ログにファイル名と行番号を出す設定 log.SetFlags(log.Lshortfile) app := gin.Default() // グループの作成 apiRouter := app.Group("/api/v0") // CORS設定、どこからでもアクセスOKにしてみた apiRouter.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"POST", "GET", "OPTIONS"}, AllowHeaders: []string{"Authorization", "Content-Type"}})) // このグループ共通のミドル、ログインしていたらユーザ情報を取得する apiRouter.Use(middleware) // ログイン apiRouter.POST("/login", Login) // ログインしているユーザの情報の取得 apiRouter.GET("/me", Me) app.Run() } // // Authorizationヘッダに正しいトークンが乗っていたらコンテキストにそのユーザの情報を乗せる // func middleware(c *gin.Context) { // Authorizationヘッダからトークンを取り出す authorizationHeader := c.Request.Header.Get("Authorization") if authorizationHeader != "" { ary := strings.Split(authorizationHeader, " ") if len(ary) == 2 { if ary[0] == "Bearer" { // ここまででトークンと思われるものを取り出せたので、解析する // ここのロジックは公式サイト参照 // https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac token, err := jwt.Parse(ary[1], func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte(SECRETKEY), nil }) if err == nil { // 解析に成功したら、ユーザIDを取り出す if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userIDStr := claims["sub"].(string) userID, err := strconv.Atoi(userIDStr) if err == nil { // ユーザIDを使って新しいトークンを取り出す(有効期限切れ対策) newToken, err := generateToken(userID) if err == nil { // ユーザの情報を取り出す conn, err := connectDB() if err == nil { defer conn.Close() var username string err := conn.QueryRow(` SELECT username FROM Users WHERE user_id = $1`, userID).Scan(&username) if err == nil { // 新しいトークンとユーザの情報をコンテキストに保存 c.Set("token", newToken) c.Set("username", username) } } } } } } } } } c.Next() } // // 投入されたログイン情報を格納するための構造体 // type LoginInfo struct { Account string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` } // // ログイン処理 // 正しいアカウントとパスワードが渡されたらトークンを生成して返す // func Login(c *gin.Context) { // 投げつけられた情報を取り出す var loginInfo LoginInfo err := c.ShouldBindJSON(&loginInfo) if err != nil { log.Println(err) c.JSON(400, gin.H{"message": "bad request"}) return } // DBに接続 conn, err := connectDB() if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } defer conn.Close() // 渡された情報に該当するユーザを探す。 // 本当だったらDBに生パスワードは保存しないだろうからこの前にハッシュ化などが必要 var userID int rows, err := conn.Query(` SELECT user_id FROM Users WHERE account = $1 AND password = $2`, loginInfo.Account, loginInfo.Password) for rows.Next() { err := rows.Scan(&userID) if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } } // トークン生成 token, err := generateToken(userID) if err != nil { c.JSON(500, gin.H{"message": "server error"}) return } c.JSON(200, gin.H{"token": token}) } // // ログインしているユーザの自分自身の情報取得 // func Me(c *gin.Context) { // 自作ミドルウェアがコンテキストに保存した情報を取り出す username, exist := c.Get("username") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } token, exist := c.Get("token") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } c.JSON(200, gin.H{"username": username, "token": token}) } // // DBへの接続 // func connectDB() (*sql.DB, error) { db, err := sql.Open("postgres", ` host=localhost port=5432 dbname=postgres user=postgres password=postgres sslmode=disable`) if err != nil { log.Println(err) return nil, err } return db, nil } // // トークン生成 // func generateToken(userID int) (string, error) { now := time.Now() token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "sub": fmt.Sprintf("%d", userID), "iat": now.Unix(), "exp": now.Add(60 * time.Minute).Unix(), // 一時間で無効になる }) return token.SignedString([]byte(SECRETKEY)) }