Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10835] ファイルの概要

このコミットは、Goプロジェクトのビルドダッシュボードにユーザーインターフェースを導入するものです。これまでのバックエンド機能に加えて、ビルドステータスを視覚的に表示するためのWebページが追加されました。具体的には、Go App Engine上で動作するGo言語製のWebアプリケーションとして、コミット情報、ビルド結果、および他のパッケージのビルド状態を表示する機能が実装されています。

コミット

commit 80103cd54fa1a6ae0cd75a8c545a365bf31f58cf
Author: Andrew Gerrand <adg@golang.org>
Date:   Fri Dec 16 10:48:06 2011 +1100

    misc/dashboard: user interface
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5461047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/80103cd54fa1a6ae0cd75a8c545a365bf31f58cf

元コミット内容

misc/dashboard: user interface

R=rsc
CC=golang-dev
https://golang.org/cl/5461047

変更の背景

このコミット以前のGoビルドダッシュボードは、主にビルド結果を記録・提供するバックエンドAPIとして機能していました。しかし、ユーザーが現在のビルドステータスを直感的に把握するためには、視覚的なインターフェースが不可欠でした。この変更の背景には、開発者やコミュニティメンバーがGoプロジェクトの継続的インテグレーション(CI)の状態を容易に監視できるように、使いやすいWebベースのダッシュボードを提供する必要性がありました。これにより、どのコミットがどの環境で成功し、どの環境で失敗したかを一目で確認できるようになり、問題の早期発見と解決に貢献します。

前提知識の解説

  • Go App Engine: Google App Engineは、Googleのインフラストラクチャ上でWebアプリケーションやモバイルバックエンドを構築・ホストするためのPaaS(Platform as a Service)です。Go言語はApp Engineでサポートされており、スケーラブルなアプリケーションを容易にデプロイできます。このダッシュボードもApp Engine上で動作するように設計されています。
  • Google Cloud Datastore: Datastoreは、App Engineアプリケーションが利用するNoSQLドキュメントデータベースです。このコミットでは、CommitPackageResultTagなどのビルド関連データがDatastoreに保存され、管理されています。Datastoreはスキーマレスであり、柔軟なデータモデルをサポートします。
  • html/templateパッケージ: Go言語の標準ライブラリに含まれるhtml/templateパッケージは、HTML出力を安全に生成するためのテンプレートエンジンです。クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープ機能が組み込まれており、Webアプリケーションのセキュリティを向上させます。このコミットでは、ui.htmlファイルがこのテンプレートエンジンによってレンダリングされます。
  • 継続的インテグレーション (CI): ソフトウェア開発手法の一つで、開発者がコードの変更を頻繁にメインブランチにマージし、自動化されたビルドとテストを実行することで、統合の問題を早期に発見します。Goビルドダッシュボードは、GoプロジェクトのCIプロセスの一部として、ビルド結果を可視化する役割を担っています。
  • ビルドボット/ビルダー: 特定の環境(OS、アーキテクチャなど)でコードをビルドし、テストを実行する自動化されたシステムまたはプロセスを指します。ダッシュボードでは、linux-amd64windows-386などの様々なビルダーからの結果が表示されます。

技術的詳細

このコミットは、Go App Engineアプリケーションのフロントエンド部分を大幅に拡張しています。

  1. ルーティングと静的ファイルの提供:

    • app.yamlが更新され、/staticパスがstaticディレクトリにマッピングされ、status_alert.gifstatus_good.gifといった静的画像ファイルが提供されるようになりました。
    • ルートパス/へのリクエストが新しいUIハンドラにルーティングされるようになり、ダッシュボードのWebページがアプリケーションのトップページとして機能します。
  2. データモデルの改善とクエリの最適化 (build/build.go):

    • Commit構造体のResultフィールドがResultDataにリネームされ、[]string型でビルド結果の生データを保持するようになりました。これは、Datastoreに非正規化された形で結果を保存し、クエリの効率を高めるための設計変更です。
    • CommitResult(builder, goHash string)およびResults(goHash string)メソッドが追加され、特定のビルダーやGoハッシュに対応するビルド結果を効率的に取得できるようになりました。これにより、UI側で必要なデータを柔軟にフィルタリング・表示することが可能になります。
    • Package構造体にLastCommit()メソッドが追加され、各パッケージの最新コミットをDatastoreから取得できるようになりました。
    • Tag構造体のValid()メソッドの論理エラーが修正され、GetTag()関数が追加されました。
    • todoHandlerpackagesHandlerなどの既存のハンドラも、新しいデータアクセスロジックに合わせて更新されています。
  3. UIロジックの実装 (build/ui.go):

    • uiHandler関数がHTTPリクエストを処理し、ダッシュボードのメインページをレンダリングします。
    • goCommits関数は、DatastoreからGoリポジトリの最新コミットをページネーション付きで取得します。これにより、大量のコミットがあっても効率的に表示できます。
    • commitBuilders関数は、表示対象のコミットに含まれるユニークなビルダー(ビルド環境)のリストを動的に生成します。これにより、ダッシュボードは利用可能なすべてのビルダーの列を自動的に表示できます。
    • TagState関数は、"tip"などの特定のタグにおける他のGoパッケージのビルド状態(成功/失敗)を取得し、UIに表示するためのPackageState構造体のスライスを生成します。
    • html/templateパッケージを利用して、ui.htmlテンプレートにデータを渡し、最終的なHTMLを生成します。builderTitleshortHashrepoURLといったカスタムテンプレート関数が定義され、表示されるデータの整形やリンク生成に利用されます。
  4. フロントエンドテンプレート (build/ui.html):

    • HTML5と基本的なCSSで構成されたシンプルなレイアウトです。
    • Goのテンプレート構文({{if}}, {{range}}, {{with}})を駆使して、動的にコンテンツを生成します。
    • Goリポジトリのコミット一覧と、他のパッケージのビルド状態の2つの主要なセクションがあります。
    • コミット一覧では、コミットハッシュ、各ビルダーでのビルド結果(成功の場合は"ok"、失敗の場合は"fail"とログへのリンク)、コミット者、日時、説明が表示されます。
    • ページネーションリンクが実装されており、過去のコミットを閲覧できます。
    • 他のパッケージのセクションでは、パッケージ名と、"tip"タグでのビルド状態(成功/失敗を示す画像と詳細リンク)が表示されます。
  5. テストの改善 (build/test.go):

    • tCommitヘルパー関数が追加され、テスト用のCommitオブジェクトをより簡単に、かつ一貫性のあるタイムスタンプで生成できるようになりました。これにより、テストコードの可読性と保守性が向上します。
    • 新しいテストケースが追加され、UIで表示される可能性のある様々なビルド結果シナリオ(特に失敗したビルドとログへのリンク)がカバーされています。

全体として、このコミットはGoビルドダッシュボードを、単なるデータストアから、開発者がGoプロジェクトの健全性をリアルタイムで監視できるインタラクティブなWebアプリケーションへと進化させました。

コアとなるコードの変更箇所

  • misc/dashboard/app/app.yaml:
    • /staticハンドラの追加: 静的ファイル(画像など)を提供するための設定。
    • /ハンドラの変更: ルートパスをGoアプリケーションにルーティングし、UIを表示。
  • misc/dashboard/app/build/build.go:
    • Commit構造体のResultフィールドをResultData []stringにリネーム。
    • CommitLastCommit(), Result(), Results(), partsToHash()メソッドを追加。
    • TagGetTag()関数を追加し、Valid()メソッドの論理エラーを修正。
    • todoHandlerpackagesHandlerなどの既存ハンドラで、新しいデータアクセスロジックを使用するように変更。
  • misc/dashboard/app/build/test.go:
    • tCommitヘルパー関数を追加し、テストデータ生成を改善。
    • 既存のテストリクエストをtCommitを使用するように更新。
  • misc/dashboard/app/build/ui.go (新規ファイル):
    • uiHandler関数: ダッシュボードのメインUIロジック。
    • goCommits関数: Goコミットのページネーション付き取得。
    • commitBuilders関数: ビルダーリストの動的生成。
    • TagState関数: 他のパッケージのビルド状態取得。
    • uiTemplate変数: HTMLテンプレートの定義とカスタム関数の登録。
  • misc/dashboard/app/build/ui.html (新規ファイル):
    • GoビルドダッシュボードのHTML構造とテンプレートロジック。
    • コミット一覧、ビルダーごとの結果表示、ページネーション、他のパッケージの状態表示。
  • misc/dashboard/app/static/status_alert.gif (新規ファイル): ビルド失敗を示す画像。
  • misc/dashboard/app/static/status_good.gif (新規ファイル): ビルド成功を示す画像。

コアとなるコードの解説

misc/dashboard/app/build/build.go の変更点

// Commit struct: Result field renamed and new methods for result access
type Commit struct {
	// ... other fields ...
	ResultData []string `datastore:",noindex"` // Renamed from Result
}

// LastCommit returns the most recent Commit for this Package.
func (p *Package) LastCommit(c appengine.Context) (*Commit, os.Error) {
	var commits []*Commit
	_, err := datastore.NewQuery("Commit").
		Ancestor(p.Key(c)).
		Order("-Time").
		Limit(1).
		GetAll(c, &commits)
	if err != nil {
		return nil, err
	}
	if len(commits) != 1 {
		return nil, datastore.ErrNoSuchEntity
	}
	return commits[0], nil
}

// Result returns the build Result for this Commit for the given builder/goHash.
func (c *Commit) Result(builder, goHash string) *Result {
	for _, r := range c.ResultData {
		p := strings.SplitN(r, "|", 4)
		if len(p) != 4 || p[0] != builder || p[3] != goHash {
			continue
		}
		return partsToHash(c, p)
	}
	return nil
}

// Results returns the build Results for this Commit for the given goHash.
func (c *Commit) Results(goHash string) (results []*Result) {
	for _, r := range c.ResultData {
		p := strings.SplitN(r, "|", 4)
		if len(p) != 4 || p[3] != goHash {
			continue
		}
		results = append(results, partsToHash(c, p))
	}
	return
}

// partsToHash converts a Commit and ResultData substrings to a Result.
func partsToHash(c *Commit, p []string) *Result {
	return &Result{
		Builder:     p[0],
		Hash:        c.Hash,
		PackagePath: c.PackagePath,
		GoHash:      p[3],
		OK:          p[1] == "true",
		LogHash:     p[2],
	}
}

Commit構造体のResultフィールドがResultDataに変わり、ビルド結果の生データ("builder|ok_status|log_hash|go_hash"のような文字列)を保持するようになりました。これは、Datastoreへの保存を簡素化し、必要なときにResultオブジェクトに変換するためのものです。LastCommitは、特定のパッケージの最新コミットを効率的に取得するためのヘルパーです。ResultResultsメソッドは、ResultDataスライスを走査し、指定された条件(ビルダーやGoハッシュ)に合致するビルド結果をResult構造体として返します。partsToHashは、この文字列データをResult構造体に変換する内部ヘルパー関数です。これらの変更により、UI側でビルド結果を柔軟に取得・表示するための基盤が強化されました。

misc/dashboard/app/build/ui.go (新規ファイル)

package build

import (
	"appengine"
	"appengine/datastore"
	"exp/template/html" // Note: exp/template/html is an experimental package at the time
	"http"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"template" // Note: template is the old text/template, html/template is preferred for HTML
)

func init() {
	http.HandleFunc("/", uiHandler)
	html.Escape(uiTemplate) // Ensures the template is safely escaped
}

// uiHandler draws the build status page.
func uiHandler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)

	page, _ := strconv.Atoi(r.FormValue("page"))
	if page < 0 {
		page = 0
	}

	commits, err := goCommits(c, page) // Fetch Go commits
	if err != nil {
		logErr(w, r, err)
		return
	}
	builders := commitBuilders(commits) // Get unique builders from commits

	tipState, err := TagState(c, "tip") // Get state of other packages at "tip"
	if err != nil {
		logErr(w, r, err)
		return
	}

	p := &Pagination{} // Pagination logic
	if len(commits) == commitsPerPage {
		p.Next = page + 1
	}
	if page > 0 {
		p.Prev = page - 1
		p.HasPrev = true
	}
	data := &uiTemplateData{commits, builders, tipState, p}
	if err := uiTemplate.Execute(w, data); err != nil { // Render template
		logErr(w, r, err)
	}
}

// goCommits gets a slice of the latest Commits to the Go repository.
func goCommits(c appengine.Context, page int) ([]*Commit, os.Error) {
	q := datastore.NewQuery("Commit").
		Ancestor((&Package{}).Key(c)).
		Order("-Time").
		Limit(commitsPerPage).
		Offset(page * commitsPerPage)
	var commits []*Commit
	_, err := q.GetAll(c, &commits)
	return commits, err
}

// commitBuilders returns the names of the builders that provided
// Results for the provided commits.
func commitBuilders(commits []*Commit) []string {
	builders := make(map[string]bool)
	for _, commit := range commits {
		for _, r := range commit.Results("") { // Get all results for a commit
			builders[r.Builder] = true
		}
	}
	return keys(builders) // Return sorted unique builder names
}

// TagState fetches the results for all non-Go packages at the specified tag.
func TagState(c appengine.Context, name string) ([]*PackageState, os.Error) {
	tag, err := GetTag(c, name) // Get the tag (e.g., "tip")
	if err != nil {
		return nil, err
	}
	pkgs, err := Packages(c) // Get all non-Go packages
	if err != nil {
		return nil, err
	}
	var states []*PackageState
	for _, pkg := range pkgs {
		commit, err := pkg.LastCommit(c) // Get last commit for each package
		if err != nil {
			c.Errorf("no Commit found: %v", pkg)
			continue
		}
		results := commit.Results(tag.Hash) // Get results for this package at the tag's Go hash
		ok := len(results) > 0
		for _, r := range results {
			ok = ok && r.OK
		}
		states = append(states, &PackageState{
			pkg, commit, results, ok,
		})
	}
	return states, nil
}

// uiTemplate defines the HTML template and its custom functions.
var uiTemplate = template.Must(
	template.New("ui").
		Funcs(template.FuncMap{
			"builderTitle": builderTitle,
			"shortHash":    shortHash,
			"repoURL":      repoURL,
		}).
		ParseFile("build/ui.html"),
)

ui.goは、ダッシュボードのWebページを生成する中心的なロジックを含んでいます。init関数でルートパスにuiHandlerを登録し、アプリケーション起動時にUIが利用可能になるようにします。uiHandlerは、HTTPリクエストを受け取り、Datastoreからコミットデータやパッケージの状態を取得し、それらをuiTemplateData構造体にまとめてuiTemplateに渡してレンダリングします。goCommitsはGoリポジトリのコミットをページネーション付きで取得し、commitBuildersは表示すべきビルダーのリストを動的に決定します。TagStateは、"tip"などの特定のタグにおける他のGoパッケージのビルド状態を収集します。uiTemplateは、ui.htmlファイルを読み込み、builderTitleshortHashrepoURLといったカスタム関数を登録することで、HTML内でGoのデータを整形して表示できるようにします。

misc/dashboard/app/build/ui.html (新規ファイル)

<!DOCTYPE HTML>
<html>
  <head>
    <title>Go Build Dashboard</title>
    <style>
      /* ... CSS styles ... */
    </style>
  </head>
  <body>

    <h1>Go Build Status</h1>

    <h2>Go</h2>

  {{if $.Commits}}
    <table class="build">
      <tr>
        <th>&nbsp;</th>
    {{range $.Builders}}
        <th class="result">{{builderTitle .}}</th>
    {{end}}
      </tr>
    {{range $c := $.Commits}}
      <tr>
      <td class="hash"><a href="{{repoURL .Hash ""}}">{{shortHash .Hash}}</a></td>
      {{range $.Builders}}
      <td class="result">
      {{with $c.Result . ""}}
        {{if .OK}}
        <span class="ok">ok</span>
        {{else}}
        <a href="/log/{{.LogHash}}" class="fail">fail</a>
        {{end}}
      {{else}}
        &nbsp;
      {{end}}
      </td>
      {{end}}
      <td class="user">{{.User}}</td>
      <td class="time">{{.Time.Time}}</td>
      <td class="desc">{{.Desc}}</td>
      </tr>
    {{end}}
    </table>

    {{with $.Pagination}}
    <div class="paginate">
      <a {{if .HasPrev}}href="?page={{.Prev}}"{{else}}class="inactive"{{end}}>prev</a>
      <a {{if .Next}}href="?page={{.Next}}"{{else}}class="inactive"{{end}}>next</a>
      <a {{if .HasPrev}}href="?page=0}"{{else}}class="inactive"{{end}}>top</a>
    </div>
    {{end}}

  {{else}}
    <p>No commits to display. Hm.</p>
  {{end}}

    <h2>Other packages</h2>

    <table class="packages">
    <tr>
      <th>State</th>
      <th>Package</th>
      <th>&nbsp;</th>
    </tr>
  {{range $state := $.TipState}}
    <tr>
      <td>
    {{if .Results}}
        <img src="/static/status_{{if .OK}}good{{else}}alert{{end}}.gif" />
    {{else}}
        &nbsp;
    {{end}}
      </td>
      <td><a title="{{.Package.Path}}">{{.Package.Name}}</a></td>
      <td>
    {{range .Results}}
        <div>
          {{$h := $state.Commit.Hash}}
          <a href="{{repoURL $h $state.Commit.PackagePath}}">{{shortHash $h}}</a>
          <a href="/log/{{.LogHash}}">failed</a>
          on {{.Builder}}/<a href="{{repoURL .GoHash ""}}">{{shortHash .GoHash}}</a>
        </a></div>
    {{end}}
      </td>
    </tr>
  {{end}}
    </table>

  </body>
</html>

ui.htmlは、Goビルドダッシュボードのユーザーインターフェースを定義するHTMLテンプレートです。{{if}}{{range}}{{with}}といったGoのテンプレートアクションを使用して、ui.goから渡されたデータに基づいて動的にコンテンツを生成します。

  • Goビルドステータス: $.Commitsをループして各コミットの情報を表示し、$.Buildersをループして各ビルダーのビルド結果を表示します。ビルドが成功した場合は"ok"、失敗した場合は"fail"と表示され、失敗時にはログへのリンクが提供されます。
  • ページネーション: $.Paginationデータに基づいて、"prev"、"next"、"top"へのリンクを生成し、過去のコミットを閲覧できるようにします。
  • Other packages: $.TipStateをループして、他のGoパッケージのビルド状態を表示します。成功/失敗はstatus_good.gifまたはstatus_alert.gif画像で視覚的に示され、パッケージ名と詳細なビルド結果(失敗時のログリンクを含む)が表示されます。 このテンプレートは、Goのビルド状態を簡潔かつ効果的にユーザーに伝えるための視覚的な表現を提供します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(html/templatenet/httpappengineパッケージなど)
  • Google App Engineのドキュメント
  • GoプロジェクトのGitHubリポジトリ
  • コミットメッセージに記載されているCode Reviewリンク: https://golang.org/cl/5461047