<commit_before>package main

import (
	"database/sql"
	"fmt"
	"os"
	"strings"

	"bitbucket.org/liamstask/goose/lib/goose"
	"github.com/docopt/docopt-go"
	"github.com/rtfb/bark"
)

const (
	usage = `migrate-db. A tool to migrate production and staging DBs for rtfblog.

Usage:
  migrate-db [--db=<db>] [--env=<env>] [--srcenv=<env>]
  migrate-db -h | --help
  migrate-db --version

Options:
  --db=<dir>      Path to DB specifier dir for goose [default: db].
  --env=<env>     Environment to migrate [default: staging].
  --srcenv=<env>  Environment to copy data from [default: production].
  -h --help       Show this screen.
  --version       Show version.`
)

var (
	logger *bark.Logger
)

func L10n(x string, y int) string {
	return ""
}

func Capitalize(s string) string {
	return ""
}

func migrateToLatest(db, env string) {
	dbconf, err := goose.NewDBConf(db, env, "")
	logger.LogIf(err)
	target, err := goose.GetMostRecentDBVersion(dbconf.MigrationsDir)
	logger.LogIf(err)
	err = goose.RunMigrations(dbconf, dbconf.MigrationsDir, target)
	logger.LogIf(err)
}

func migrateToTarget(db, env string, target int64) {
	dbconf, err := goose.NewDBConf(db, env, "")
	logger.LogIf(err)
	err = goose.RunMigrations(dbconf, dbconf.MigrationsDir, target)
	logger.LogIf(err)
}

func clearTable(db *sql.DB, table string) error {
	fmt.Printf("Clearing table %v...\n", table)
	_, err := db.Exec("delete from " + table)
	return err
}

func makePlaceholders(num int) string {
	items := make([]string, 0)
	for i := 0; i < num; i++ {
		items = append(items, fmt.Sprintf("$%d", i+1))
	}
	return strings.Join(items, ", ")
}

func copyTable(src, dst *sql.DB, table string) error {
	fmt.Printf("Copying table %v...\n", table)
	rows, err := src.Query("select * from " + table)
	if err != nil {
		return err
	}
	columns, err := rows.Columns()
	if err != nil {
		return err
	}
	for rows.Next() {
		ss := make([]interface{}, 0)
		for i := 0; i < len(columns); i++ {
			var x interface{}
			ss = append(ss, &x)
		}
		rows.Scan(ss...)
		sql := fmt.Sprintf("insert into %s (%s) values (%s)", table,
			strings.Join(columns, ", "), makePlaceholders(len(columns)))
		_, err = dst.Exec(sql, ss...)
		if err != nil {
			return err
		}
	}
	return nil
}

func copyData(source *goose.DBConf, db, env string) {
	targetConf, err := goose.NewDBConf(db, env, "")
	logger.LogIf(err)
	tDB, err := sql.Open(targetConf.Driver.Name, targetConf.Driver.OpenStr)
	logger.LogIf(err)
	logger.LogIf(clearTable(tDB, "comment"))
	logger.LogIf(clearTable(tDB, "commenter"))
	logger.LogIf(clearTable(tDB, "post"))
	logger.LogIf(clearTable(tDB, "tag"))
	logger.LogIf(clearTable(tDB, "tagmap"))
	logger.LogIf(clearTable(tDB, "author"))
	sDB, err := sql.Open(source.Driver.Name, source.Driver.OpenStr)
	logger.LogIf(err)
	logger.LogIf(copyTable(sDB, tDB, "author"))
	logger.LogIf(copyTable(sDB, tDB, "post"))
	logger.LogIf(copyTable(sDB, tDB, "commenter"))
	logger.LogIf(copyTable(sDB, tDB, "comment"))
	logger.LogIf(copyTable(sDB, tDB, "tag"))
	logger.LogIf(copyTable(sDB, tDB, "tagmap"))
	tDB.Close()
}

func main() {
	args, err := docopt.Parse(usage, nil, true, "0.1", false)
	if err != nil {
		panic("Can't docopt.Parse!")
	}
	fmt.Println(args)
	db := args["--db"].(string)
	env := args["--env"].(string)
	srcenv := args["--srcenv"].(string)
	dbConf := "db/dbconf.yml"
	if _, err := os.Stat(dbConf); os.IsNotExist(err) {
		fmt.Printf("Can't find Goose dbconf: %q, exiting.\n", dbConf)
		return
	}
	logger = bark.Create()
	if env == "production" {
		migrateToLatest(db, env)
		return
	} else {
		prodConf, err := goose.NewDBConf(db, srcenv, "")
		logger.LogIf(err)
		target, err := goose.GetDBVersion(prodConf)
		logger.LogIf(err)
		migrateToTarget(db, env, target)
		copyData(prodConf, db, env)
		migrateToLatest(db, env)
	}
}
<commit_msg>migrate-db: migrate to steinbacher/goose<commit_after>package main

import (
	"database/sql"
	"fmt"
	"os"
	"path"
	"strings"

	"github.com/docopt/docopt-go"
	_ "github.com/lib/pq"
	"github.com/rtfb/bark"
	"github.com/steinbacher/goose"
)

const (
	usage = `migrate-db. A tool to migrate production and staging DBs for rtfblog.

Usage:
  migrate-db [--db=<db>] [--env=<env>] [--srcenv=<env>]
  migrate-db -h | --help
  migrate-db --version

Options:
  --db=<dir>      Path to DB specifier dir for goose [default: db].
  --env=<env>     Environment to migrate [default: staging].
  --srcenv=<env>  Environment to copy data from [default: production].
  -h --help       Show this screen.
  --version       Show version.`
)

var (
	logger *bark.Logger
)

func L10n(x string, y int) string {
	return ""
}

func Capitalize(s string) string {
	return ""
}

func migrateToLatest(db, env string) {
	dbconf, err := goose.NewDBConf(db, env)
	logger.LogIf(err)
	target, err := goose.GetMostRecentDBVersion(dbconf.MigrationsDir)
	logger.LogIf(err)
	err = goose.RunMigrations(dbconf, dbconf.MigrationsDir, target)
	logger.LogIf(err)
}

func migrateToTarget(db, env string, target int64) {
	dbconf, err := goose.NewDBConf(db, env)
	logger.LogIf(err)
	err = goose.RunMigrations(dbconf, dbconf.MigrationsDir, target)
	logger.LogIf(err)
}

func clearTable(db *sql.DB, table string) error {
	fmt.Printf("Clearing table %v...\n", table)
	_, err := db.Exec("delete from " + table)
	return err
}

func makePlaceholders(num int) string {
	items := make([]string, 0)
	for i := 0; i < num; i++ {
		items = append(items, fmt.Sprintf("$%d", i+1))
	}
	return strings.Join(items, ", ")
}

func copyTable(src, dst *sql.DB, table string) error {
	fmt.Printf("Copying table %v...\n", table)
	rows, err := src.Query("select * from " + table)
	if err != nil {
		return err
	}
	columns, err := rows.Columns()
	if err != nil {
		return err
	}
	for rows.Next() {
		ss := make([]interface{}, 0)
		for i := 0; i < len(columns); i++ {
			var x interface{}
			ss = append(ss, &x)
		}
		rows.Scan(ss...)
		sql := fmt.Sprintf("insert into %s (%s) values (%s)", table,
			strings.Join(columns, ", "), makePlaceholders(len(columns)))
		_, err = dst.Exec(sql, ss...)
		if err != nil {
			return err
		}
	}
	return nil
}

func copyData(source *goose.DBConf, db, env string) {
	targetConf, err := goose.NewDBConf(db, env)
	logger.LogIf(err)
	tDB, err := sql.Open(targetConf.Driver.Name, targetConf.Driver.DSN)
	logger.LogIf(err)
	logger.LogIf(clearTable(tDB, "comment"))
	logger.LogIf(clearTable(tDB, "commenter"))
	logger.LogIf(clearTable(tDB, "post"))
	logger.LogIf(clearTable(tDB, "tag"))
	logger.LogIf(clearTable(tDB, "tagmap"))
	logger.LogIf(clearTable(tDB, "author"))
	sDB, err := sql.Open(source.Driver.Name, source.Driver.DSN)
	logger.LogIf(err)
	logger.LogIf(copyTable(sDB, tDB, "author"))
	logger.LogIf(copyTable(sDB, tDB, "post"))
	logger.LogIf(copyTable(sDB, tDB, "commenter"))
	logger.LogIf(copyTable(sDB, tDB, "comment"))
	logger.LogIf(copyTable(sDB, tDB, "tag"))
	logger.LogIf(copyTable(sDB, tDB, "tagmap"))
	tDB.Close()
}

func main() {
	args, err := docopt.Parse(usage, nil, true, "0.1", false)
	if err != nil {
		panic("Can't docopt.Parse!")
	}
	fmt.Println(args)
	db := args["--db"].(string)
	env := args["--env"].(string)
	srcenv := args["--srcenv"].(string)
	dbConf := path.Join(db, "dbconf.yml")
	if _, err := os.Stat(dbConf); os.IsNotExist(err) {
		fmt.Printf("Can't find Goose dbconf: %q, exiting.\n", dbConf)
		return
	}
	logger = bark.Create()
	if env == "production" {
		migrateToLatest(db, env)
		return
	} else {
		prodConf, err := goose.NewDBConf(db, srcenv)
		logger.LogIf(err)
		target, err := goose.GetDBVersion(prodConf)
		logger.LogIf(err)
		migrateToTarget(db, env, target)
		copyData(prodConf, db, env)
		migrateToLatest(db, env)
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/golang/glog"
	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

func githubClient(token string) *github.Client {
	// create github client
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(ctx, ts)
	return github.NewClient(tc)
}

func ReportOnIssue(e error, logs, token, org, repo string, issue int) error {
	ctx := context.Background()
	client := githubClient(token)

	// filter out token, if it happens to be in the log (it shouldn't!)
	logs = strings.Replace(logs, token, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", -1)

	// who am I?
	myself, resp, err := client.Users.Get(ctx, "")
	if err != nil {
		glog.Fatalf("Failed to get own user: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		glog.Fatalf("Failed to get own user: HTTP code %d", resp.StatusCode)
	}

	// create new newComment
	body := fmt.Sprintf("The last publishing run failed: %v\n\n```%s```\n", e, tail(logs, 65000))
	newComment, resp, err := client.Issues.CreateComment(ctx, org, repo, issue, &github.IssueComment{
		Body: &body,
	})
	if err != nil {
		glog.Fatalf("Failed to comment on issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		glog.Fatalf("Failed to comment on issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	// re-open issue in case it was closed
	_, resp, err = client.Issues.Edit(ctx, org, repo, issue, &github.IssueRequest{
		State: github.String("open"),
	})
	if err != nil {
		glog.Fatalf("Failed to re-open issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		glog.Fatalf("Failed to re-open issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	// delete all other comments from this user
	comments, resp, err := client.Issues.ListComments(ctx, org, repo, issue, &github.IssueListCommentsOptions{})
	if err != nil {
		glog.Fatalf("Failed to get github comments of issue #%d: %v", issue, err)
	}
	if resp.StatusCode != http.StatusOK {
		glog.Fatalf("Failed to get github comments of issue #%d: HTTP code %d", issue, resp.StatusCode)
	}
	for _, c := range comments {
		if *c.User.ID == *myself.ID && *c.ID != *newComment.ID {
			resp, err = client.Issues.DeleteComment(ctx, org, repo, *c.ID)
		}
	}

	return nil
}

func CloseIssue(token, org, repo string, issue int) error {
	ctx := context.Background()
	client := githubClient(token)

	_, resp, err := client.Issues.Edit(ctx, org, repo, issue, &github.IssueRequest{
		State: github.String("closed"),
	})
	if err != nil {
		glog.Fatalf("Failed to close issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		glog.Fatalf("Failed to close issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	return nil
}

// tail returns lines with a maximum of the given bytes in length. Only complete lines are
// returned, with the exception if the is only one line, then "..." are put in front.
// maxBytes must be at least 3.
func tail(msg string, maxBytes int) string {
	msg = strings.Trim(msg, "\n")
	if len(msg) <= maxBytes {
		return msg
	}
	lines := strings.Split(msg, "\n")
	if len(lines) == 1 {
		if len(lines[0]) <= maxBytes {
			return lines[0]
		}
		return "..." + lines[0][max(0, len(lines[0])-maxBytes+3):]
	}

	prefix := "..."
	ret := []string{}
	n := len(prefix)
	for i := len(lines) - 1; i >= 0; i-- {
		if n+len("\n")+len(lines[i]) > maxBytes {
			if len(ret) == 0 {
				return "..." + lines[0][max(0, len(lines[0])-maxBytes+3):]
			}
			break
		}
		ret = append(ret, lines[i])
		n += len(lines[i]) + len("\n")
	}

	return strings.Join(reverse(append(ret, prefix)), "\n")
}

func reverse(s []string) []string {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
	return s
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
<commit_msg>Return github errors, don't call glog.Fatalf<commit_after>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

func githubClient(token string) *github.Client {
	// create github client
	ctx := context.Background()
	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(ctx, ts)
	return github.NewClient(tc)
}

func ReportOnIssue(e error, logs, token, org, repo string, issue int) error {
	ctx := context.Background()
	client := githubClient(token)

	// filter out token, if it happens to be in the log (it shouldn't!)
	logs = strings.Replace(logs, token, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", -1)

	// who am I?
	myself, resp, err := client.Users.Get(ctx, "")
	if err != nil {
		return fmt.Errorf("failed to get own user: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to get own user: HTTP code %d", resp.StatusCode)
	}

	// create new newComment
	body := fmt.Sprintf("The last publishing run failed: %v\n\n```%s```\n", e, tail(logs, 65000))
	newComment, resp, err := client.Issues.CreateComment(ctx, org, repo, issue, &github.IssueComment{
		Body: &body,
	})
	if err != nil {
		return fmt.Errorf("failed to comment on issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		return fmt.Errorf("failed to comment on issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	// re-open issue in case it was closed
	_, resp, err = client.Issues.Edit(ctx, org, repo, issue, &github.IssueRequest{
		State: github.String("open"),
	})
	if err != nil {
		return fmt.Errorf("failed to re-open issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		return fmt.Errorf("failed to re-open issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	// delete all other comments from this user
	comments, resp, err := client.Issues.ListComments(ctx, org, repo, issue, &github.IssueListCommentsOptions{})
	if err != nil {
		return fmt.Errorf("failed to get github comments of issue #%d: %v", issue, err)
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to get github comments of issue #%d: HTTP code %d", issue, resp.StatusCode)
	}
	for _, c := range comments {
		if *c.User.ID == *myself.ID && *c.ID != *newComment.ID {
			resp, err = client.Issues.DeleteComment(ctx, org, repo, *c.ID)
		}
	}

	return nil
}

func CloseIssue(token, org, repo string, issue int) error {
	ctx := context.Background()
	client := githubClient(token)

	_, resp, err := client.Issues.Edit(ctx, org, repo, issue, &github.IssueRequest{
		State: github.String("closed"),
	})
	if err != nil {
		return fmt.Errorf("failed to close issue #%d: %v", issue, err)
	}
	if resp.StatusCode >= 300 {
		return fmt.Errorf("failed to close issue #%d: HTTP code %d", issue, resp.StatusCode)
	}

	return nil
}

// tail returns lines with a maximum of the given bytes in length. Only complete lines are
// returned, with the exception if the is only one line, then "..." are put in front.
// maxBytes must be at least 3.
func tail(msg string, maxBytes int) string {
	msg = strings.Trim(msg, "\n")
	if len(msg) <= maxBytes {
		return msg
	}
	lines := strings.Split(msg, "\n")
	if len(lines) == 1 {
		if len(lines[0]) <= maxBytes {
			return lines[0]
		}
		return "..." + lines[0][max(0, len(lines[0])-maxBytes+3):]
	}

	prefix := "..."
	ret := []string{}
	n := len(prefix)
	for i := len(lines) - 1; i >= 0; i-- {
		if n+len("\n")+len(lines[i]) > maxBytes {
			if len(ret) == 0 {
				return "..." + lines[0][max(0, len(lines[0])-maxBytes+3):]
			}
			break
		}
		ret = append(ret, lines[i])
		n += len(lines[i]) + len("\n")
	}

	return strings.Join(reverse(append(ret, prefix)), "\n")
}

func reverse(s []string) []string {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
	return s
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
<|endoftext|>


<commit_before>package templates

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"

	"github.com/PuerkitoBio/ghost"
)

var (
	compilers  = make(map[string]TemplateCompiler)
	templaters = make(map[string]Templater)
)

// TODO : How to manage Go nested templates?
// TODO : Support Go's port of the mustache template?

// Register a template compiler for the specified extension. Extensions are case-sensitive.
// The extension must start with a dot (it is compared to the result of path.Ext() on a
// given file name).
//
// Registering is not thread-safe. Compilers should be registered before the http server
// is started.
func Register(ext string, c TemplateCompiler) {
	if c == nil {
		panic("ghost: Register TemplateCompiler is nil")
	}
	if _, dup := compilers[ext]; dup {
		panic("ghost: Register called twice for extension " + ext)
	}
	compilers[ext] = c
}

// Compile all templates that have a matching compiler (based on their extension) in the
// specified directory.
func CompileDir(dir string) error {
	return filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
		if !fi.IsDir() {
			err = compileTemplate(path)
			if err != nil {
				ghost.LogFn("ghost.templates : error compiling template %s : %s", path, err)
				return err
			}
		}
		return nil
	})
}

// Compile the specified template file if there is a matching compiler.
func compileTemplate(p string) error {
	ext := path.Ext(p)
	c, ok := compilers[ext]
	// Ignore file if no template compiler exist for this extension
	if ok {
		t, err := c.Compile(p)
		if err != nil {
			return err
		}
		ghost.LogFn("ghost : storing template for file %s", p)
		templaters[p] = t
	}
	return nil
}

// Execute the template.
func Execute(tplName string, w io.Writer, data interface{}) error {
	t, ok := templaters[tplName]
	if !ok {
		return fmt.Errorf("no template found for file %s", tplName)
	}
	return t.Execute(w, data)
}

// Render is the same as Execute, except that it takes a http.ResponseWriter
// instead of a generic io.Writer, and sets the Content-Type to text/html.
func Render(tplName string, w http.ResponseWriter, data interface{}) error {
	w.Header().Set("Content-Type", "text/html")
	return Execute(tplName, w, data)
}

// Defines the interface that the template compiler must return. The Go native
// templates implement this interface.
type Templater interface {
	Execute(wr io.Writer, data interface{}) error
}

// The interface that a template engine must implement to be used by Ghost.
type TemplateCompiler interface {
	Compile(fileName string) (Templater, error)
}
<commit_msg>do not panic if templates directory does not exist<commit_after>package templates

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"

	"github.com/PuerkitoBio/ghost"
)

var (
	compilers  = make(map[string]TemplateCompiler)
	templaters = make(map[string]Templater)
)

// TODO : How to manage Go nested templates?
// TODO : Support Go's port of the mustache template?

// Register a template compiler for the specified extension. Extensions are case-sensitive.
// The extension must start with a dot (it is compared to the result of path.Ext() on a
// given file name).
//
// Registering is not thread-safe. Compilers should be registered before the http server
// is started.
func Register(ext string, c TemplateCompiler) {
	if c == nil {
		panic("ghost: Register TemplateCompiler is nil")
	}
	if _, dup := compilers[ext]; dup {
		panic("ghost: Register called twice for extension " + ext)
	}
	compilers[ext] = c
}

// Compile all templates that have a matching compiler (based on their extension) in the
// specified directory.
func CompileDir(dir string) error {
	return filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
		if fi == nil {
			return fmt.Errorf("directory %s does not exist", path)
		}
		if !fi.IsDir() {
			err = compileTemplate(path)
			if err != nil {
				ghost.LogFn("ghost.templates : error compiling template %s : %s", path, err)
				return err
			}
		}
		return nil
	})
}

// Compile the specified template file if there is a matching compiler.
func compileTemplate(p string) error {
	ext := path.Ext(p)
	c, ok := compilers[ext]
	// Ignore file if no template compiler exist for this extension
	if ok {
		t, err := c.Compile(p)
		if err != nil {
			return err
		}
		ghost.LogFn("ghost : storing template for file %s", p)
		templaters[p] = t
	}
	return nil
}

// Execute the template.
func Execute(tplName string, w io.Writer, data interface{}) error {
	t, ok := templaters[tplName]
	if !ok {
		return fmt.Errorf("no template found for file %s", tplName)
	}
	return t.Execute(w, data)
}

// Render is the same as Execute, except that it takes a http.ResponseWriter
// instead of a generic io.Writer, and sets the Content-Type to text/html.
func Render(tplName string, w http.ResponseWriter, data interface{}) error {
	w.Header().Set("Content-Type", "text/html")
	return Execute(tplName, w, data)
}

// Defines the interface that the template compiler must return. The Go native
// templates implement this interface.
type Templater interface {
	Execute(wr io.Writer, data interface{}) error
}

// The interface that a template engine must implement to be used by Ghost.
type TemplateCompiler interface {
	Compile(fileName string) (Templater, error)
}
<|endoftext|>


<commit_before>package quota_test

import (
	. "github.com/cloudfoundry/cli/cf/commands/quota"
	"github.com/cloudfoundry/cli/cf/models"
	. "github.com/cloudfoundry/cli/testhelpers/matchers"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	"github.com/cloudfoundry/cli/cf/errors"
	testapi "github.com/cloudfoundry/cli/testhelpers/api"
	testcmd "github.com/cloudfoundry/cli/testhelpers/commands"
	testconfig "github.com/cloudfoundry/cli/testhelpers/configuration"
	testreq "github.com/cloudfoundry/cli/testhelpers/requirements"
	testterm "github.com/cloudfoundry/cli/testhelpers/terminal"
)

var _ = Describe("quotas command", func() {
	var (
		ui                  *testterm.FakeUI
		quotaRepo           *testapi.FakeQuotaRepository
		requirementsFactory *testreq.FakeReqFactory
	)

	BeforeEach(func() {
		ui = &testterm.FakeUI{}
		quotaRepo = &testapi.FakeQuotaRepository{}
		requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true}
	})

	runCommand := func() bool {
		cmd := NewListQuotas(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo)
		return testcmd.RunCommand(cmd, []string{}, requirementsFactory)
	}

	Describe("requirements", func() {
		It("requires the user to be logged in", func() {
			requirementsFactory.LoginSuccess = false
			Expect(runCommand()).ToNot(HavePassedRequirements())
		})
	})

	Context("when quotas exist", func() {
		BeforeEach(func() {
			quotaRepo.FindAllReturns.Quotas = []models.QuotaFields{
				models.QuotaFields{
					Name:                    "quota-name",
					MemoryLimit:             1024,
					RoutesLimit:             111,
					ServicesLimit:           222,
					NonBasicServicesAllowed: true,
				},
				models.QuotaFields{
					Name:                    "quota-non-basic-not-allowed",
					MemoryLimit:             434,
					RoutesLimit:             1,
					ServicesLimit:           2,
					NonBasicServicesAllowed: false,
				},
			}
		})

		It("lists quotas", func() {
			Expect(Expect(runCommand()).To(HavePassedRequirements())).To(HavePassedRequirements())
			Expect(ui.Outputs).To(ContainSubstrings(
				[]string{"Getting quotas as", "my-user"},
				[]string{"OK"},
				[]string{"name", "memory limit", "routes", "service instances", "paid service plans"},
				[]string{"quota-name", "1G", "111", "222", "allowed"},
				[]string{"quota-non-basic-not-allowed", "434M", "1", "2", "disallowed"},
			))
		})
	})

	Context("when an error occurs fetching quotas", func() {
		BeforeEach(func() {
			quotaRepo.FindAllReturns.Error = errors.New("I haz a borken!")
		})

		It("prints an error", func() {
			Expect(runCommand()).To(HavePassedRequirements())
			Expect(ui.Outputs).To(ContainSubstrings(
				[]string{"Getting quotas as", "my-user"},
				[]string{"FAILED"},
			))
		})
	})

})
<commit_msg>Cleanup imports in quotas test<commit_after>package quota_test

import (
	"github.com/cloudfoundry/cli/cf/errors"
	"github.com/cloudfoundry/cli/cf/models"
	testapi "github.com/cloudfoundry/cli/testhelpers/api"
	testcmd "github.com/cloudfoundry/cli/testhelpers/commands"
	testconfig "github.com/cloudfoundry/cli/testhelpers/configuration"
	testreq "github.com/cloudfoundry/cli/testhelpers/requirements"
	testterm "github.com/cloudfoundry/cli/testhelpers/terminal"

	. "github.com/cloudfoundry/cli/cf/commands/quota"
	. "github.com/cloudfoundry/cli/testhelpers/matchers"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("quotas command", func() {
	var (
		ui                  *testterm.FakeUI
		quotaRepo           *testapi.FakeQuotaRepository
		requirementsFactory *testreq.FakeReqFactory
	)

	BeforeEach(func() {
		ui = &testterm.FakeUI{}
		quotaRepo = &testapi.FakeQuotaRepository{}
		requirementsFactory = &testreq.FakeReqFactory{LoginSuccess: true}
	})

	runCommand := func() bool {
		cmd := NewListQuotas(ui, testconfig.NewRepositoryWithDefaults(), quotaRepo)
		return testcmd.RunCommand(cmd, []string{}, requirementsFactory)
	}

	Describe("requirements", func() {
		It("requires the user to be logged in", func() {
			requirementsFactory.LoginSuccess = false
			Expect(runCommand()).ToNot(HavePassedRequirements())
		})
	})

	Context("when quotas exist", func() {
		BeforeEach(func() {
			quotaRepo.FindAllReturns.Quotas = []models.QuotaFields{
				models.QuotaFields{
					Name:                    "quota-name",
					MemoryLimit:             1024,
					RoutesLimit:             111,
					ServicesLimit:           222,
					NonBasicServicesAllowed: true,
				},
				models.QuotaFields{
					Name:                    "quota-non-basic-not-allowed",
					MemoryLimit:             434,
					RoutesLimit:             1,
					ServicesLimit:           2,
					NonBasicServicesAllowed: false,
				},
			}
		})

		It("lists quotas", func() {
			Expect(Expect(runCommand()).To(HavePassedRequirements())).To(HavePassedRequirements())
			Expect(ui.Outputs).To(ContainSubstrings(
				[]string{"Getting quotas as", "my-user"},
				[]string{"OK"},
				[]string{"name", "memory limit", "routes", "service instances", "paid service plans"},
				[]string{"quota-name", "1G", "111", "222", "allowed"},
				[]string{"quota-non-basic-not-allowed", "434M", "1", "2", "disallowed"},
			))
		})
	})

	Context("when an error occurs fetching quotas", func() {
		BeforeEach(func() {
			quotaRepo.FindAllReturns.Error = errors.New("I haz a borken!")
		})

		It("prints an error", func() {
			Expect(runCommand()).To(HavePassedRequirements())
			Expect(ui.Outputs).To(ContainSubstrings(
				[]string{"Getting quotas as", "my-user"},
				[]string{"FAILED"},
			))
		})
	})

})
<|endoftext|>


<commit_before>// Copyright 2015 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !nodiskstats
// +build !nodiskstats

package collector

import (
	"fmt"
	"github.com/go-kit/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/procfs/blockdevice"
)

const (
	secondsPerTick = 1.0 / 1000.0

	// Read sectors and write sectors are the "standard UNIX 512-byte sectors, not any device- or filesystem-specific block size."
	// See also https://www.kernel.org/doc/Documentation/block/stat.txt
	unixSectorSize = 512.0

	diskstatsDefaultIgnoredDevices = "^(ram|loop|fd|(h|s|v|xv)d[a-z]|nvme\\d+n\\d+p)\\d+$"
)

type typedFactorDesc struct {
	desc      *prometheus.Desc
	valueType prometheus.ValueType
}

func (d *typedFactorDesc) mustNewConstMetric(value float64, labels ...string) prometheus.Metric {
	return prometheus.MustNewConstMetric(d.desc, d.valueType, value, labels...)
}

type diskstatsCollector struct {
	deviceFilter deviceFilter
	fs           blockdevice.FS
	infoDesc     typedFactorDesc
	descs        []typedFactorDesc
	logger       log.Logger
}

func init() {
	registerCollector("diskstats", defaultEnabled, NewDiskstatsCollector)
}

// NewDiskstatsCollector returns a new Collector exposing disk device stats.
// Docs from https://www.kernel.org/doc/Documentation/iostats.txt
func NewDiskstatsCollector(logger log.Logger) (Collector, error) {
	var diskLabelNames = []string{"device"}
	fs, err := blockdevice.NewFS(*procPath, *sysPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open sysfs: %w", err)
	}

	deviceFilter, err := newDiskstatsDeviceFilter(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to parse device filter flags: %w", err)
	}

	return &diskstatsCollector{
		deviceFilter: deviceFilter,
		fs:           fs,
		infoDesc: typedFactorDesc{
			desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "info"),
				"Info of /sys/block/<block_device>.",
				[]string{"device", "major", "minor"},
				nil,
			), valueType: prometheus.GaugeValue,
		},
		descs: []typedFactorDesc{
			{
				desc: readsCompletedDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "reads_merged_total"),
					"The total number of reads merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: readBytesDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: readTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: writesCompletedDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "writes_merged_total"),
					"The number of writes merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: writtenBytesDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: writeTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "io_now"),
					"The number of I/Os currently in progress.",
					diskLabelNames,
					nil,
				), valueType: prometheus.GaugeValue,
			},
			{
				desc: ioTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "io_time_weighted_seconds_total"),
					"The weighted # of seconds spent doing I/Os.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discards_completed_total"),
					"The total number of discards completed successfully.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discards_merged_total"),
					"The total number of discards merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discarded_sectors_total"),
					"The total number of sectors discarded successfully.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discard_time_seconds_total"),
					"This is the total number of seconds spent by all discards.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "flush_requests_total"),
					"The total number of flush requests completed successfully",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "flush_requests_time_seconds_total"),
					"This is the total number of seconds spent by all flush requests.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
		},
		logger: logger,
	}, nil
}

func (c *diskstatsCollector) Update(ch chan<- prometheus.Metric) error {
	diskStats, err := c.fs.ProcDiskstats()
	if err != nil {
		return fmt.Errorf("couldn't get diskstats: %w", err)
	}

	for _, stats := range diskStats {
		dev := stats.DeviceName
		if c.deviceFilter.ignored(dev) {
			continue
		}
		ch <- c.infoDesc.mustNewConstMetric(1.0, dev, fmt.Sprint(stats.MajorNumber), fmt.Sprint(stats.MinorNumber))

		statCount := stats.IoStatsCount - 3 // Total diskstats record count, less MajorNumber, MinorNumber and DeviceName

		for i, val := range []float64{
			float64(stats.ReadIOs),
			float64(stats.ReadMerges),
			float64(stats.ReadSectors) * unixSectorSize,
			float64(stats.ReadTicks) * secondsPerTick,
			float64(stats.WriteIOs),
			float64(stats.WriteMerges),
			float64(stats.WriteSectors) * unixSectorSize,
			float64(stats.WriteTicks) * secondsPerTick,
			float64(stats.IOsInProgress),
			float64(stats.IOsTotalTicks) * secondsPerTick,
			float64(stats.WeightedIOTicks) * secondsPerTick,
			float64(stats.DiscardIOs),
			float64(stats.DiscardMerges),
			float64(stats.DiscardSectors),
			float64(stats.DiscardTicks) * secondsPerTick,
			float64(stats.FlushRequestsCompleted),
			float64(stats.TimeSpentFlushing) * secondsPerTick,
		} {
			if i >= statCount {
				break
			}
			ch <- c.descs[i].mustNewConstMetric(val, dev)
		}
	}
	return nil
}
<commit_msg>collector/diskstats: Add labels and metrics from udev<commit_after>// Copyright 2015 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !nodiskstats
// +build !nodiskstats

package collector

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/go-kit/log"
	"github.com/go-kit/log/level"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/procfs/blockdevice"
)

const (
	secondsPerTick = 1.0 / 1000.0

	// Read sectors and write sectors are the "standard UNIX 512-byte sectors, not any device- or filesystem-specific block size."
	// See also https://www.kernel.org/doc/Documentation/block/stat.txt
	unixSectorSize = 512.0

	diskstatsDefaultIgnoredDevices = "^(ram|loop|fd|(h|s|v|xv)d[a-z]|nvme\\d+n\\d+p)\\d+$"

	// udev attributes
	udevDMLVLayer               = "E:DM_LV_LAYER"
	udevDMLVName                = "E:DM_LV_NAME"
	udevDMName                  = "E:DM_NAME"
	udevDMUUID                  = "E:DM_UUID"
	udevDMVGName                = "E:DM_VG_NAME"
	udevIDATA                   = "E:ID_ATA"
	udevIDATARotationRateRPM    = "E:ID_ATA_ROTATION_RATE_RPM"
	udevIDATASATA               = "E:ID_ATA_SATA"
	udevIDATASATASignalRateGen1 = "E:ID_ATA_SATA_SIGNAL_RATE_GEN1"
	udevIDATASATASignalRateGen2 = "E:ID_ATA_SATA_SIGNAL_RATE_GEN2"
	udevIDATAWriteCache         = "E:ID_ATA_WRITE_CACHE"
	udevIDATAWriteCacheEnabled  = "E:ID_ATA_WRITE_CACHE_ENABLED"
	udevIDFSType                = "E:ID_FS_TYPE"
	udevIDFSUsage               = "E:ID_FS_USAGE"
	udevIDFSUUID                = "E:ID_FS_UUID"
	udevIDFSVersion             = "E:ID_FS_VERSION"
	udevIDModel                 = "E:ID_MODEL"
	udevIDPath                  = "E:ID_PATH"
	udevIDRevision              = "E:ID_REVISION"
	udevIDSerialShort           = "E:ID_SERIAL_SHORT"
	udevIDWWN                   = "E:ID_WWN"
)

type typedFactorDesc struct {
	desc      *prometheus.Desc
	valueType prometheus.ValueType
}

type udevInfo map[string]string

func (d *typedFactorDesc) mustNewConstMetric(value float64, labels ...string) prometheus.Metric {
	return prometheus.MustNewConstMetric(d.desc, d.valueType, value, labels...)
}

type diskstatsCollector struct {
	deviceFilter         deviceFilter
	fs                   blockdevice.FS
	infoDesc             typedFactorDesc
	descs                []typedFactorDesc
	filesystemInfoDesc   typedFactorDesc
	deviceMapperInfoDesc typedFactorDesc
	ataDescs             map[string]typedFactorDesc
	logger               log.Logger
}

func init() {
	registerCollector("diskstats", defaultEnabled, NewDiskstatsCollector)
}

// NewDiskstatsCollector returns a new Collector exposing disk device stats.
// Docs from https://www.kernel.org/doc/Documentation/iostats.txt
func NewDiskstatsCollector(logger log.Logger) (Collector, error) {
	var diskLabelNames = []string{"device"}
	fs, err := blockdevice.NewFS(*procPath, *sysPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open sysfs: %w", err)
	}

	deviceFilter, err := newDiskstatsDeviceFilter(logger)
	if err != nil {
		return nil, fmt.Errorf("failed to parse device filter flags: %w", err)
	}

	return &diskstatsCollector{
		deviceFilter: deviceFilter,
		fs:           fs,
		infoDesc: typedFactorDesc{
			desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "info"),
				"Info of /sys/block/<block_device>.",
				[]string{"device", "major", "minor", "path", "wwn", "model", "serial", "revision"},
				nil,
			), valueType: prometheus.GaugeValue,
		},
		descs: []typedFactorDesc{
			{
				desc: readsCompletedDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "reads_merged_total"),
					"The total number of reads merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: readBytesDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: readTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: writesCompletedDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "writes_merged_total"),
					"The number of writes merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: writtenBytesDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: writeTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "io_now"),
					"The number of I/Os currently in progress.",
					diskLabelNames,
					nil,
				), valueType: prometheus.GaugeValue,
			},
			{
				desc: ioTimeSecondsDesc, valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "io_time_weighted_seconds_total"),
					"The weighted # of seconds spent doing I/Os.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discards_completed_total"),
					"The total number of discards completed successfully.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discards_merged_total"),
					"The total number of discards merged.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discarded_sectors_total"),
					"The total number of sectors discarded successfully.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "discard_time_seconds_total"),
					"This is the total number of seconds spent by all discards.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "flush_requests_total"),
					"The total number of flush requests completed successfully",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
			{
				desc: prometheus.NewDesc(
					prometheus.BuildFQName(namespace, diskSubsystem, "flush_requests_time_seconds_total"),
					"This is the total number of seconds spent by all flush requests.",
					diskLabelNames,
					nil,
				), valueType: prometheus.CounterValue,
			},
		},
		filesystemInfoDesc: typedFactorDesc{
			desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "filesystem_info"),
				"Info about disk filesystem.",
				[]string{"device", "type", "usage", "uuid", "version"},
				nil,
			), valueType: prometheus.GaugeValue,
		},
		deviceMapperInfoDesc: typedFactorDesc{
			desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "device_mapper_info"),
				"Info about disk device mapper.",
				[]string{"device", "name", "uuid", "vg_name", "lv_name", "lv_layer"},
				nil,
			), valueType: prometheus.GaugeValue,
		},
		ataDescs: map[string]typedFactorDesc{
			udevIDATAWriteCache: {
				desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "ata_write_cache"),
					"ATA disk has a write cache.",
					[]string{"device"},
					nil,
				), valueType: prometheus.GaugeValue,
			},
			udevIDATAWriteCacheEnabled: {
				desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "ata_write_cache_enabled"),
					"ATA disk has its write cache enabled.",
					[]string{"device"},
					nil,
				), valueType: prometheus.GaugeValue,
			},
			udevIDATARotationRateRPM: {
				desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, diskSubsystem, "ata_rotation_rate_rpm"),
					"ATA disk rotation rate in RPMs (0 for SSDs).",
					[]string{"device"},
					nil,
				), valueType: prometheus.GaugeValue,
			},
		},
		logger: logger,
	}, nil
}

func (c *diskstatsCollector) Update(ch chan<- prometheus.Metric) error {
	diskStats, err := c.fs.ProcDiskstats()
	if err != nil {
		return fmt.Errorf("couldn't get diskstats: %w", err)
	}

	for _, stats := range diskStats {
		dev := stats.DeviceName
		if c.deviceFilter.ignored(dev) {
			continue
		}

		info, err := udevDeviceInformation(stats.MajorNumber, stats.MinorNumber)
		if err != nil {
			level.Error(c.logger).Log("msg", "Failed to parse udev info", "err", err)
		}

		ch <- c.infoDesc.mustNewConstMetric(1.0, dev,
			fmt.Sprint(stats.MajorNumber),
			fmt.Sprint(stats.MinorNumber),
			info[udevIDPath],
			info[udevIDWWN],
			info[udevIDModel],
			info[udevIDSerialShort],
			info[udevIDRevision],
		)

		statCount := stats.IoStatsCount - 3 // Total diskstats record count, less MajorNumber, MinorNumber and DeviceName

		for i, val := range []float64{
			float64(stats.ReadIOs),
			float64(stats.ReadMerges),
			float64(stats.ReadSectors) * unixSectorSize,
			float64(stats.ReadTicks) * secondsPerTick,
			float64(stats.WriteIOs),
			float64(stats.WriteMerges),
			float64(stats.WriteSectors) * unixSectorSize,
			float64(stats.WriteTicks) * secondsPerTick,
			float64(stats.IOsInProgress),
			float64(stats.IOsTotalTicks) * secondsPerTick,
			float64(stats.WeightedIOTicks) * secondsPerTick,
			float64(stats.DiscardIOs),
			float64(stats.DiscardMerges),
			float64(stats.DiscardSectors),
			float64(stats.DiscardTicks) * secondsPerTick,
			float64(stats.FlushRequestsCompleted),
			float64(stats.TimeSpentFlushing) * secondsPerTick,
		} {
			if i >= statCount {
				break
			}
			ch <- c.descs[i].mustNewConstMetric(val, dev)
		}

		if fsType := info[udevIDFSType]; fsType != "" {
			ch <- c.filesystemInfoDesc.mustNewConstMetric(1.0, dev,
				fsType,
				info[udevIDFSUsage],
				info[udevIDFSUUID],
				info[udevIDFSVersion],
			)
		}

		if name := info[udevDMName]; name != "" {
			ch <- c.deviceMapperInfoDesc.mustNewConstMetric(1.0, dev,
				name,
				info[udevDMUUID],
				info[udevDMVGName],
				info[udevDMLVName],
				info[udevDMLVLayer],
			)
		}

		if ata := info[udevIDATA]; ata != "" {
			for attr, desc := range c.ataDescs {
				str, ok := info[attr]
				if !ok {
					level.Debug(c.logger).Log("msg", "Udev attribute does not exist", "attribute", attr)
					continue
				}

				if value, err := strconv.ParseFloat(str, 64); err == nil {
					ch <- desc.mustNewConstMetric(value, dev)
				} else {
					level.Error(c.logger).Log("msg", "Failed to parse ATA value", "err", err)
				}
			}
		}
	}
	return nil
}

func udevDeviceInformation(major, minor uint32) (udevInfo, error) {
	filename := fmt.Sprintf("/run/udev/data/b%d:%d", major, minor)

	data, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer data.Close()

	info := make(udevInfo)

	scanner := bufio.NewScanner(data)
	for scanner.Scan() {
		if name, value, found := strings.Cut(scanner.Text(), "="); found {
			info[name] = value
		}
	}

	return info, nil
}
<|endoftext|>


<commit_before>package agent

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"strconv"
	"strings"

	"github.com/docker/docker/pkg/ioutils"
	cstructs "github.com/hashicorp/nomad/client/structs"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/ugorji/go/codec"
)

var (
	allocIDNotPresentErr  = fmt.Errorf("must provide a valid alloc id")
	fileNameNotPresentErr = fmt.Errorf("must provide a file name")
	taskNotPresentErr     = fmt.Errorf("must provide task name")
	logTypeNotPresentErr  = fmt.Errorf("must provide log type (stdout/stderr)")
	clientNotRunning      = fmt.Errorf("node is not running a Nomad Client")
	invalidOrigin         = fmt.Errorf("origin must be start or end")
)

func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/")
	switch {
	case strings.HasPrefix(path, "ls/"):
		return s.DirectoryListRequest(resp, req)
	case strings.HasPrefix(path, "stat/"):
		return s.FileStatRequest(resp, req)
	case strings.HasPrefix(path, "readat/"):
		return s.FileReadAtRequest(resp, req)
	case strings.HasPrefix(path, "cat/"):
		return s.FileCatRequest(resp, req)
	case strings.HasPrefix(path, "stream/"):
		return s.Stream(resp, req)
	case strings.HasPrefix(path, "logs/"):
		return s.Logs(resp, req)
	default:
		return nil, CodedError(404, ErrInvalidMethod)
	}
}

func (s *HTTPServer) DirectoryListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/ls/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = req.URL.Query().Get("path"); path == "" {
		path = "/"
	}

	// Create the request
	args := &cstructs.FsListRequest{
		AllocID: allocID,
		Path:    path,
	}
	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

	// Make the RPC
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)

	var reply cstructs.FsListResponse
	var rpcErr error
	if localClient {
		rpcErr = s.agent.Client().ClientRPC("FileSystem.List", &args, &reply)
	} else if remoteClient {
		rpcErr = s.agent.Client().RPC("FileSystem.List", &args, &reply)
	} else if localServer {
		rpcErr = s.agent.Server().RPC("FileSystem.List", &args, &reply)
	}

	if rpcErr != nil {
		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
			rpcErr = CodedError(404, rpcErr.Error())
		}

		return nil, rpcErr
	}

	return reply.Files, nil
}

func (s *HTTPServer) FileStatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string
	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = req.URL.Query().Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	// Create the request
	args := &cstructs.FsStatRequest{
		AllocID: allocID,
		Path:    path,
	}
	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

	// Make the RPC
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)

	var reply cstructs.FsStatResponse
	var rpcErr error
	if localClient {
		rpcErr = s.agent.Client().ClientRPC("FileSystem.Stat", &args, &reply)
	} else if remoteClient {
		rpcErr = s.agent.Client().RPC("FileSystem.Stat", &args, &reply)
	} else if localServer {
		rpcErr = s.agent.Server().RPC("FileSystem.Stat", &args, &reply)
	}

	if rpcErr != nil {
		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
			rpcErr = CodedError(404, rpcErr.Error())
		}

		return nil, rpcErr
	}

	return reply.Info, nil
}

func (s *HTTPServer) FileReadAtRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string
	var offset, limit int64
	var err error

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/readat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	if offset, err = strconv.ParseInt(q.Get("offset"), 10, 64); err != nil {
		return nil, fmt.Errorf("error parsing offset: %v", err)
	}

	// Parse the limit
	if limitStr := q.Get("limit"); limitStr != "" {
		if limit, err = strconv.ParseInt(limitStr, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing limit: %v", err)
		}
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID:   allocID,
		Path:      path,
		Offset:    offset,
		Origin:    "start",
		Limit:     limit,
		PlainText: true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/cat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID:   allocID,
		Path:      path,
		Origin:    "start",
		PlainText: true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

// Stream streams the content of a file blocking on EOF.
// The parameters are:
// * path: path to file to stream.
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
//           applied. Defaults to "start".
func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}

	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	var offset int64
	offsetString := q.Get("offset")
	if offsetString != "" {
		var err error
		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing offset: %v", err)
		}
	}

	origin := q.Get("origin")
	switch origin {
	case "start", "end":
	case "":
		origin = "start"
	default:
		return nil, invalidOrigin
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID: allocID,
		Path:    path,
		Origin:  origin,
		Offset:  offset,
		Follow:  true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

// Logs streams the content of a log blocking on EOF. The parameters are:
// * task: task name to stream logs for.
// * type: stdout/stderr to stream.
// * follow: A boolean of whether to follow the logs.
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
//           applied. Defaults to "start".
func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, task, logType string
	var plain, follow bool
	var err error

	q := req.URL.Query()
	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}

	if task = q.Get("task"); task == "" {
		return nil, taskNotPresentErr
	}

	if followStr := q.Get("follow"); followStr != "" {
		if follow, err = strconv.ParseBool(followStr); err != nil {
			return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err)
		}
	}

	if plainStr := q.Get("plain"); plainStr != "" {
		if plain, err = strconv.ParseBool(plainStr); err != nil {
			return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err)
		}
	}

	logType = q.Get("type")
	switch logType {
	case "stdout", "stderr":
	default:
		return nil, logTypeNotPresentErr
	}

	var offset int64
	offsetString := q.Get("offset")
	if offsetString != "" {
		var err error
		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing offset: %v", err)
		}
	}

	origin := q.Get("origin")
	switch origin {
	case "start", "end":
	case "":
		origin = "start"
	default:
		return nil, invalidOrigin
	}

	// Create the request arguments
	fsReq := &cstructs.FsLogsRequest{
		AllocID:   allocID,
		Task:      task,
		LogType:   logType,
		Offset:    offset,
		Origin:    origin,
		PlainText: plain,
		Follow:    follow,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID)
}

// fsStreamImpl is used to make a streaming filesystem call that serializes the
// args and then expects a stream of StreamErrWrapper results where the payload
// is copied to the response body.
func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter,
	req *http.Request, method string, args interface{}, allocID string) (interface{}, error) {

	// Get the correct handler
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
	var handler structs.StreamingRpcHandler
	var handlerErr error
	if localClient {
		handler, handlerErr = s.agent.Client().StreamingRpcHandler(method)
	} else if remoteClient {
		handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method)
	} else if localServer {
		handler, handlerErr = s.agent.Server().StreamingRpcHandler(method)
	}

	if handlerErr != nil {
		return nil, CodedError(500, handlerErr.Error())
	}

	// Create a pipe connecting the (possibly remote) handler to the http response
	httpPipe, handlerPipe := net.Pipe()
	decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle)
	encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle)

	// Create a goroutine that closes the pipe if the connection closes.
	ctx, cancel := context.WithCancel(req.Context())
	go func() {
		<-ctx.Done()
		httpPipe.Close()
	}()

	// Create an output that gets flushed on every write
	output := ioutils.NewWriteFlusher(resp)

	// Create a channel that decodes the results
	errCh := make(chan HTTPCodedError)
	go func() {
		// Send the request
		if err := encoder.Encode(args); err != nil {
			errCh <- CodedError(500, err.Error())
			cancel()
			return
		}
		encoder.Reset(httpPipe)

		for {
			select {
			case <-ctx.Done():
				errCh <- nil
				cancel()
				return
			default:
			}

			var res cstructs.StreamErrWrapper
			if err := decoder.Decode(&res); err != nil {
				errCh <- CodedError(500, err.Error())
				cancel()
				return
			}
			decoder.Reset(httpPipe)

			if err := res.Error; err != nil {
				if err.Code != nil {
					errCh <- CodedError(int(*err.Code), err.Error())
					cancel()
					return
				}
			}

			if _, err := io.Copy(output, bytes.NewBuffer(res.Payload)); err != nil {
				errCh <- CodedError(500, err.Error())
				cancel()
				return
			}
		}
	}()

	handler(handlerPipe)
	cancel()
	codedErr := <-errCh
	if codedErr != nil &&
		(codedErr == io.EOF ||
			strings.Contains(codedErr.Error(), "closed") ||
			strings.Contains(codedErr.Error(), "EOF")) {
		codedErr = nil
	}
	return nil, codedErr
}
<commit_msg>client: ensure cancel is always called when func exits<commit_after>package agent

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"strconv"
	"strings"

	"github.com/docker/docker/pkg/ioutils"
	cstructs "github.com/hashicorp/nomad/client/structs"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/ugorji/go/codec"
)

var (
	allocIDNotPresentErr  = fmt.Errorf("must provide a valid alloc id")
	fileNameNotPresentErr = fmt.Errorf("must provide a file name")
	taskNotPresentErr     = fmt.Errorf("must provide task name")
	logTypeNotPresentErr  = fmt.Errorf("must provide log type (stdout/stderr)")
	clientNotRunning      = fmt.Errorf("node is not running a Nomad Client")
	invalidOrigin         = fmt.Errorf("origin must be start or end")
)

func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/")
	switch {
	case strings.HasPrefix(path, "ls/"):
		return s.DirectoryListRequest(resp, req)
	case strings.HasPrefix(path, "stat/"):
		return s.FileStatRequest(resp, req)
	case strings.HasPrefix(path, "readat/"):
		return s.FileReadAtRequest(resp, req)
	case strings.HasPrefix(path, "cat/"):
		return s.FileCatRequest(resp, req)
	case strings.HasPrefix(path, "stream/"):
		return s.Stream(resp, req)
	case strings.HasPrefix(path, "logs/"):
		return s.Logs(resp, req)
	default:
		return nil, CodedError(404, ErrInvalidMethod)
	}
}

func (s *HTTPServer) DirectoryListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/ls/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = req.URL.Query().Get("path"); path == "" {
		path = "/"
	}

	// Create the request
	args := &cstructs.FsListRequest{
		AllocID: allocID,
		Path:    path,
	}
	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

	// Make the RPC
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)

	var reply cstructs.FsListResponse
	var rpcErr error
	if localClient {
		rpcErr = s.agent.Client().ClientRPC("FileSystem.List", &args, &reply)
	} else if remoteClient {
		rpcErr = s.agent.Client().RPC("FileSystem.List", &args, &reply)
	} else if localServer {
		rpcErr = s.agent.Server().RPC("FileSystem.List", &args, &reply)
	}

	if rpcErr != nil {
		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
			rpcErr = CodedError(404, rpcErr.Error())
		}

		return nil, rpcErr
	}

	return reply.Files, nil
}

func (s *HTTPServer) FileStatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string
	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = req.URL.Query().Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	// Create the request
	args := &cstructs.FsStatRequest{
		AllocID: allocID,
		Path:    path,
	}
	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

	// Make the RPC
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)

	var reply cstructs.FsStatResponse
	var rpcErr error
	if localClient {
		rpcErr = s.agent.Client().ClientRPC("FileSystem.Stat", &args, &reply)
	} else if remoteClient {
		rpcErr = s.agent.Client().RPC("FileSystem.Stat", &args, &reply)
	} else if localServer {
		rpcErr = s.agent.Server().RPC("FileSystem.Stat", &args, &reply)
	}

	if rpcErr != nil {
		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
			rpcErr = CodedError(404, rpcErr.Error())
		}

		return nil, rpcErr
	}

	return reply.Info, nil
}

func (s *HTTPServer) FileReadAtRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string
	var offset, limit int64
	var err error

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/readat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	if offset, err = strconv.ParseInt(q.Get("offset"), 10, 64); err != nil {
		return nil, fmt.Errorf("error parsing offset: %v", err)
	}

	// Parse the limit
	if limitStr := q.Get("limit"); limitStr != "" {
		if limit, err = strconv.ParseInt(limitStr, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing limit: %v", err)
		}
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID:   allocID,
		Path:      path,
		Offset:    offset,
		Origin:    "start",
		Limit:     limit,
		PlainText: true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/cat/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}
	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID:   allocID,
		Path:      path,
		Origin:    "start",
		PlainText: true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

// Stream streams the content of a file blocking on EOF.
// The parameters are:
// * path: path to file to stream.
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
//           applied. Defaults to "start".
func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, path string

	q := req.URL.Query()

	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}

	if path = q.Get("path"); path == "" {
		return nil, fileNameNotPresentErr
	}

	var offset int64
	offsetString := q.Get("offset")
	if offsetString != "" {
		var err error
		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing offset: %v", err)
		}
	}

	origin := q.Get("origin")
	switch origin {
	case "start", "end":
	case "":
		origin = "start"
	default:
		return nil, invalidOrigin
	}

	// Create the request arguments
	fsReq := &cstructs.FsStreamRequest{
		AllocID: allocID,
		Path:    path,
		Origin:  origin,
		Offset:  offset,
		Follow:  true,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
}

// Logs streams the content of a log blocking on EOF. The parameters are:
// * task: task name to stream logs for.
// * type: stdout/stderr to stream.
// * follow: A boolean of whether to follow the logs.
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
//           applied. Defaults to "start".
func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
	var allocID, task, logType string
	var plain, follow bool
	var err error

	q := req.URL.Query()
	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" {
		return nil, allocIDNotPresentErr
	}

	if task = q.Get("task"); task == "" {
		return nil, taskNotPresentErr
	}

	if followStr := q.Get("follow"); followStr != "" {
		if follow, err = strconv.ParseBool(followStr); err != nil {
			return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err)
		}
	}

	if plainStr := q.Get("plain"); plainStr != "" {
		if plain, err = strconv.ParseBool(plainStr); err != nil {
			return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err)
		}
	}

	logType = q.Get("type")
	switch logType {
	case "stdout", "stderr":
	default:
		return nil, logTypeNotPresentErr
	}

	var offset int64
	offsetString := q.Get("offset")
	if offsetString != "" {
		var err error
		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
			return nil, fmt.Errorf("error parsing offset: %v", err)
		}
	}

	origin := q.Get("origin")
	switch origin {
	case "start", "end":
	case "":
		origin = "start"
	default:
		return nil, invalidOrigin
	}

	// Create the request arguments
	fsReq := &cstructs.FsLogsRequest{
		AllocID:   allocID,
		Task:      task,
		LogType:   logType,
		Offset:    offset,
		Origin:    origin,
		PlainText: plain,
		Follow:    follow,
	}
	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)

	// Make the request
	return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID)
}

// fsStreamImpl is used to make a streaming filesystem call that serializes the
// args and then expects a stream of StreamErrWrapper results where the payload
// is copied to the response body.
func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter,
	req *http.Request, method string, args interface{}, allocID string) (interface{}, error) {

	// Get the correct handler
	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
	var handler structs.StreamingRpcHandler
	var handlerErr error
	if localClient {
		handler, handlerErr = s.agent.Client().StreamingRpcHandler(method)
	} else if remoteClient {
		handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method)
	} else if localServer {
		handler, handlerErr = s.agent.Server().StreamingRpcHandler(method)
	}

	if handlerErr != nil {
		return nil, CodedError(500, handlerErr.Error())
	}

	// Create a pipe connecting the (possibly remote) handler to the http response
	httpPipe, handlerPipe := net.Pipe()
	decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle)
	encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle)

	// Create a goroutine that closes the pipe if the connection closes.
	ctx, cancel := context.WithCancel(req.Context())
	go func() {
		<-ctx.Done()
		httpPipe.Close()
	}()

	// Create an output that gets flushed on every write
	output := ioutils.NewWriteFlusher(resp)

	// Create a channel that decodes the results
	errCh := make(chan HTTPCodedError)
	go func() {
		defer cancel()

		// Send the request
		if err := encoder.Encode(args); err != nil {
			errCh <- CodedError(500, err.Error())
			return
		}
		encoder.Reset(httpPipe)

		for {
			select {
			case <-ctx.Done():
				errCh <- nil
				return
			default:
			}

			var res cstructs.StreamErrWrapper
			if err := decoder.Decode(&res); err != nil {
				errCh <- CodedError(500, err.Error())
				return
			}
			decoder.Reset(httpPipe)

			if err := res.Error; err != nil {
				if err.Code != nil {
					errCh <- CodedError(int(*err.Code), err.Error())
					return
				}
			}

			if _, err := io.Copy(output, bytes.NewBuffer(res.Payload)); err != nil {
				errCh <- CodedError(500, err.Error())
				return
			}
		}
	}()

	handler(handlerPipe)
	cancel()
	codedErr := <-errCh
	if codedErr != nil &&
		(codedErr == io.EOF ||
			strings.Contains(codedErr.Error(), "closed") ||
			strings.Contains(codedErr.Error(), "EOF")) {
		codedErr = nil
	}
	return nil, codedErr
}
<|endoftext|>


<commit_before>package format

import (
	"bufio"
	"bytes"
	"fmt"
	"strings"

	"github.com/hashicorp/hcl2/hcl"
	"github.com/hashicorp/hcl2/hcled"
	"github.com/hashicorp/hcl2/hclparse"
	"github.com/hashicorp/terraform/tfdiags"
	"github.com/mitchellh/colorstring"
	wordwrap "github.com/mitchellh/go-wordwrap"
)

// Diagnostic formats a single diagnostic message.
//
// The width argument specifies at what column the diagnostic messages will
// be wrapped. If set to zero, messages will not be wrapped by this function
// at all. Although the long-form text parts of the message are wrapped,
// not all aspects of the message are guaranteed to fit within the specified
// terminal width.
func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string {
	if diag == nil {
		// No good reason to pass a nil diagnostic in here...
		return ""
	}

	var buf bytes.Buffer

	switch diag.Severity() {
	case tfdiags.Error:
		buf.WriteString(color.Color("\n[bold][red]Error: [reset]"))
	case tfdiags.Warning:
		buf.WriteString(color.Color("\n[bold][yellow]Warning: [reset]"))
	default:
		// Clear out any coloring that might be applied by Terraform's UI helper,
		// so our result is not context-sensitive.
		buf.WriteString(color.Color("\n[reset]"))
	}

	desc := diag.Description()
	sourceRefs := diag.Source()

	// We don't wrap the summary, since we expect it to be terse, and since
	// this is where we put the text of a native Go error it may not always
	// be pure text that lends itself well to word-wrapping.
	fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)

	if sourceRefs.Subject != nil {
		// We'll borrow HCL's range implementation here, because it has some
		// handy features to help us produce a nice source code snippet.
		highlightRange := sourceRefs.Subject.ToHCL()
		snippetRange := highlightRange
		if sourceRefs.Context != nil {
			snippetRange = sourceRefs.Context.ToHCL()
		}
		// Make sure the snippet includes the highlight. This should be true
		// for any reasonable diagnostic, but we'll make sure.
		snippetRange = hcl.RangeOver(snippetRange, highlightRange)

		// We can't illustrate an empty range, so we'll turn such ranges into
		// single-character ranges, which might not be totally valid (may point
		// off the end of a line, or off the end of the file) but are good
		// enough for the bounds checks we do below.
		if snippetRange.Empty() {
			snippetRange.End.Byte++
			snippetRange.End.Column++
		}
		if highlightRange.Empty() {
			highlightRange.End.Byte++
			highlightRange.End.Column++
		}

		var src []byte
		if sources != nil {
			src = sources[snippetRange.Filename]
		}
		if src == nil {
			// This should generally not happen, as long as sources are always
			// loaded through the main loader. We may load things in other
			// ways in weird cases, so we'll tolerate it at the expense of
			// a not-so-helpful error message.
			fmt.Fprintf(&buf, "  on %s line %d:\n  (source code not available)\n\n", highlightRange.Filename, highlightRange.Start.Line)
		} else {
			contextStr := sourceCodeContextStr(src, highlightRange)
			if contextStr != "" {
				contextStr = ", in " + contextStr
			}
			fmt.Fprintf(&buf, "  on %s line %d%s:\n", highlightRange.Filename, highlightRange.Start.Line, contextStr)

			sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
			for sc.Scan() {
				lineRange := sc.Range()
				if !lineRange.Overlaps(snippetRange) {
					continue
				}
				beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
				if highlightedRange.Empty() {
					fmt.Fprintf(&buf, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
				} else {
					before := beforeRange.SliceBytes(src)
					highlighted := highlightedRange.SliceBytes(src)
					after := afterRange.SliceBytes(src)
					fmt.Fprintf(
						&buf, color.Color("%4d: %s[underline]%s[reset]%s\n"),
						lineRange.Start.Line,
						before, highlighted, after,
					)
				}
			}

			buf.WriteByte('\n')
		}
	}

	if desc.Detail != "" {
		detail := desc.Detail
		if width != 0 {
			detail = wordwrap.WrapString(detail, uint(width))
		}
		fmt.Fprintf(&buf, "%s\n", detail)
	}

	return buf.String()
}

// sourceCodeContextStr attempts to find a user-friendly description of
// the location of the given range in the given source code.
//
// An empty string is returned if no suitable description is available, e.g.
// because the source is invalid, or because the offset is not inside any sort
// of identifiable container.
func sourceCodeContextStr(src []byte, rng hcl.Range) string {
	filename := rng.Filename
	offset := rng.Start.Byte

	// We need to re-parse here to get a *hcl.File we can interrogate. This
	// is not awesome since we presumably already parsed the file earlier too,
	// but this re-parsing is architecturally simpler than retaining all of
	// the hcl.File objects and we only do this in the case of an error anyway
	// so the overhead here is not a big problem.
	parser := hclparse.NewParser()
	var file *hcl.File
	var diags hcl.Diagnostics
	if strings.HasSuffix(filename, ".json") {
		file, diags = parser.ParseJSON(src, filename)
	} else {
		file, diags = parser.ParseHCL(src, filename)
	}
	if diags.HasErrors() {
		return ""
	}

	return hcled.ContextString(file, offset)
}
<commit_msg>command/format: Include variable values in diagnostic messages<commit_after>package format

import (
	"bufio"
	"bytes"
	"fmt"
	"sort"
	"strings"

	"github.com/hashicorp/hcl2/hcl"
	"github.com/hashicorp/hcl2/hcled"
	"github.com/hashicorp/hcl2/hclparse"
	"github.com/hashicorp/terraform/tfdiags"
	"github.com/mitchellh/colorstring"
	wordwrap "github.com/mitchellh/go-wordwrap"
	"github.com/zclconf/go-cty/cty"
)

// Diagnostic formats a single diagnostic message.
//
// The width argument specifies at what column the diagnostic messages will
// be wrapped. If set to zero, messages will not be wrapped by this function
// at all. Although the long-form text parts of the message are wrapped,
// not all aspects of the message are guaranteed to fit within the specified
// terminal width.
func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string {
	if diag == nil {
		// No good reason to pass a nil diagnostic in here...
		return ""
	}

	var buf bytes.Buffer

	switch diag.Severity() {
	case tfdiags.Error:
		buf.WriteString(color.Color("\n[bold][red]Error: [reset]"))
	case tfdiags.Warning:
		buf.WriteString(color.Color("\n[bold][yellow]Warning: [reset]"))
	default:
		// Clear out any coloring that might be applied by Terraform's UI helper,
		// so our result is not context-sensitive.
		buf.WriteString(color.Color("\n[reset]"))
	}

	desc := diag.Description()
	sourceRefs := diag.Source()

	// We don't wrap the summary, since we expect it to be terse, and since
	// this is where we put the text of a native Go error it may not always
	// be pure text that lends itself well to word-wrapping.
	fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)

	if sourceRefs.Subject != nil {
		// We'll borrow HCL's range implementation here, because it has some
		// handy features to help us produce a nice source code snippet.
		highlightRange := sourceRefs.Subject.ToHCL()
		snippetRange := highlightRange
		if sourceRefs.Context != nil {
			snippetRange = sourceRefs.Context.ToHCL()
		}
		// Make sure the snippet includes the highlight. This should be true
		// for any reasonable diagnostic, but we'll make sure.
		snippetRange = hcl.RangeOver(snippetRange, highlightRange)

		// We can't illustrate an empty range, so we'll turn such ranges into
		// single-character ranges, which might not be totally valid (may point
		// off the end of a line, or off the end of the file) but are good
		// enough for the bounds checks we do below.
		if snippetRange.Empty() {
			snippetRange.End.Byte++
			snippetRange.End.Column++
		}
		if highlightRange.Empty() {
			highlightRange.End.Byte++
			highlightRange.End.Column++
		}

		var src []byte
		if sources != nil {
			src = sources[snippetRange.Filename]
		}
		if src == nil {
			// This should generally not happen, as long as sources are always
			// loaded through the main loader. We may load things in other
			// ways in weird cases, so we'll tolerate it at the expense of
			// a not-so-helpful error message.
			fmt.Fprintf(&buf, "  on %s line %d:\n  (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line)
		} else {
			contextStr := sourceCodeContextStr(src, highlightRange)
			if contextStr != "" {
				contextStr = ", in " + contextStr
			}
			fmt.Fprintf(&buf, "  on %s line %d%s:\n", highlightRange.Filename, highlightRange.Start.Line, contextStr)

			sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
			for sc.Scan() {
				lineRange := sc.Range()
				if !lineRange.Overlaps(snippetRange) {
					continue
				}
				beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
				if highlightedRange.Empty() {
					fmt.Fprintf(&buf, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
				} else {
					before := beforeRange.SliceBytes(src)
					highlighted := highlightedRange.SliceBytes(src)
					after := afterRange.SliceBytes(src)
					fmt.Fprintf(
						&buf, color.Color("%4d: %s[underline]%s[reset]%s\n"),
						lineRange.Start.Line,
						before, highlighted, after,
					)
				}
			}

		}

		if fromExpr := diag.FromExpr(); fromExpr != nil {
			// We may also be able to generate information about the dynamic
			// values of relevant variables at the point of evaluation, then.
			// This is particularly useful for expressions that get evaluated
			// multiple times with different values, such as blocks using
			// "count" and "for_each", or within "for" expressions.
			expr := fromExpr.Expression
			ctx := fromExpr.EvalContext
			vars := expr.Variables()
			stmts := make([]string, 0, len(vars))
			seen := make(map[string]struct{}, len(vars))
		Traversals:
			for _, traversal := range vars {
				for len(traversal) > 1 {
					val, diags := traversal.TraverseAbs(ctx)
					if diags.HasErrors() {
						// Skip anything that generates errors, since we probably
						// already have the same error in our diagnostics set
						// already.
						traversal = traversal[:len(traversal)-1]
						continue
					}

					traversalStr := traversalStr(traversal)
					if _, exists := seen[traversalStr]; exists {
						continue Traversals // don't show duplicates when the same variable is referenced multiple times
					}
					switch {
					case !val.IsKnown():
						// Can't say anything about this yet, then.
						continue Traversals
					case val.IsNull():
						stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr))
					default:
						stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is %s"), traversalStr, compactValueStr(val)))
					}
					seen[traversalStr] = struct{}{}
				}
			}

			sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?

			if len(stmts) > 0 {
				fmt.Fprint(&buf, color.Color("    [dark_gray]|----------------[reset]\n"))
			}
			for _, stmt := range stmts {
				fmt.Fprintf(&buf, color.Color("    [dark_gray]|[reset] %s\n"), stmt)
			}
		}

		buf.WriteByte('\n')
	}

	if desc.Detail != "" {
		detail := desc.Detail
		if width != 0 {
			detail = wordwrap.WrapString(detail, uint(width))
		}
		fmt.Fprintf(&buf, "%s\n", detail)
	}

	return buf.String()
}

// sourceCodeContextStr attempts to find a user-friendly description of
// the location of the given range in the given source code.
//
// An empty string is returned if no suitable description is available, e.g.
// because the source is invalid, or because the offset is not inside any sort
// of identifiable container.
func sourceCodeContextStr(src []byte, rng hcl.Range) string {
	filename := rng.Filename
	offset := rng.Start.Byte

	// We need to re-parse here to get a *hcl.File we can interrogate. This
	// is not awesome since we presumably already parsed the file earlier too,
	// but this re-parsing is architecturally simpler than retaining all of
	// the hcl.File objects and we only do this in the case of an error anyway
	// so the overhead here is not a big problem.
	parser := hclparse.NewParser()
	var file *hcl.File
	var diags hcl.Diagnostics
	if strings.HasSuffix(filename, ".json") {
		file, diags = parser.ParseJSON(src, filename)
	} else {
		file, diags = parser.ParseHCL(src, filename)
	}
	if diags.HasErrors() {
		return ""
	}

	return hcled.ContextString(file, offset)
}

// traversalStr produces a representation of an HCL traversal that is compact,
// resembles HCL native syntax, and is suitable for display in the UI.
func traversalStr(traversal hcl.Traversal) string {
	// This is a specialized subset of traversal rendering tailored to
	// producing helpful contextual messages in diagnostics. It is not
	// comprehensive nor intended to be used for other purposes.

	var buf bytes.Buffer
	for _, step := range traversal {
		switch tStep := step.(type) {
		case hcl.TraverseRoot:
			buf.WriteString(tStep.Name)
		case hcl.TraverseAttr:
			buf.WriteByte('.')
			buf.WriteString(tStep.Name)
		case hcl.TraverseIndex:
			buf.WriteByte('[')
			if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
				buf.WriteString(compactValueStr(tStep.Key))
			} else {
				// We'll just use a placeholder for more complex values,
				// since otherwise our result could grow ridiculously long.
				buf.WriteString("...")
			}
			buf.WriteByte(']')
		}
	}
	return buf.String()
}

// compactValueStr produces a compact, single-line summary of a given value
// that is suitable for display in the UI.
//
// For primitives it returns a full representation, while for more complex
// types it instead summarizes the type, size, etc to produce something
// that is hopefully still somewhat useful but not as verbose as a rendering
// of the entire data structure.
func compactValueStr(val cty.Value) string {
	// This is a specialized subset of value rendering tailored to producing
	// helpful but concise messages in diagnostics. It is not comprehensive
	// nor intended to be used for other purposes.

	ty := val.Type()
	switch {
	case val.IsNull():
		return "null"
	case !val.IsKnown():
		// Should never happen here because we should filter before we get
		// in here, but we'll do something reasonable rather than panic.
		return "(not yet known)"
	case ty == cty.Bool:
		if val.True() {
			return "true"
		}
		return "false"
	case ty == cty.Number:
		bf := val.AsBigFloat()
		return bf.Text('g', 10)
	case ty == cty.String:
		// Go string syntax is not exactly the same as HCL native string syntax,
		// but we'll accept the minor edge-cases where this is different here
		// for now, just to get something reasonable here.
		return fmt.Sprintf("%q", val.AsString())
	case ty.IsCollectionType() || ty.IsTupleType():
		l := val.LengthInt()
		switch l {
		case 0:
			return "empty " + ty.FriendlyName()
		case 1:
			return ty.FriendlyName() + " with 1 element"
		default:
			return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
		}
	case ty.IsObjectType():
		atys := ty.AttributeTypes()
		l := len(atys)
		switch l {
		case 0:
			return "object with no attributes"
		case 1:
			var name string
			for k := range atys {
				name = k
			}
			return fmt.Sprintf("object with 1 attribute %q", name)
		default:
			return fmt.Sprintf("object with %d attributes", l)
		}
	default:
		return ty.FriendlyName()
	}
}
<|endoftext|>


<commit_before>package command

import (
	"sort"
	"testing"

	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/helper"
	"github.com/stretchr/testify/require"
)

func TestDeviceQualifiedID(t *testing.T) {

	require := require.New(t)

	require.Equal("vendor/type/name[id]", deviceQualifiedID("vendor", "type", "name", "id"))
	require.Equal("vendor/type[id]", deviceQualifiedID("vendor", "type", "", "id"))
	require.Equal("vendor[id]", deviceQualifiedID("vendor", "", "", "id"))
}

func TestBuildDeviceStatsSummaryMap(t *testing.T) {
	hostStats := &api.HostStats{
		DeviceStats: []*api.DeviceGroupStats{
			{
				Vendor: "vendor1",
				Type:   "type1",
				Name:   "name1",
				InstanceStats: map[string]*api.DeviceStats{
					"id1": {
						Summary: &api.StatValue{
							StringVal: helper.StringToPtr("stat1"),
						},
					},
					"id2": {
						Summary: &api.StatValue{
							IntNumeratorVal: helper.Int64ToPtr(2),
						},
					},
				},
			},
			{
				Vendor: "vendor2",
				Type:   "type2",
				InstanceStats: map[string]*api.DeviceStats{
					"id1": {
						Summary: &api.StatValue{
							StringVal: helper.StringToPtr("stat3"),
						},
					},
					"id2": {
						Summary: &api.StatValue{
							IntNumeratorVal: helper.Int64ToPtr(4),
						},
					},
				},
			},
		},
	}

	expected := map[string]*api.StatValue{
		"vendor1/type1/name1[id1]": {
			StringVal: helper.StringToPtr("stat1"),
		},
		"vendor1/type1/name1[id2]": {
			IntNumeratorVal: helper.Int64ToPtr(2),
		},
		"vendor2/type2[id1]": {
			StringVal: helper.StringToPtr("stat3"),
		},
		"vendor2/type2[id2]": {
			IntNumeratorVal: helper.Int64ToPtr(4),
		},
	}

	require.EqualValues(t, expected, buildDeviceStatsSummaryMap(hostStats))
}

func TestFormatDeviceStats(t *testing.T) {
	statValue := func(v string) *api.StatValue {
		return &api.StatValue{
			StringVal: helper.StringToPtr(v),
		}
	}

	stat := &api.StatObject{
		Attributes: map[string]*api.StatValue{
			"k0": statValue("v0"),
		},
		Nested: map[string]*api.StatObject{
			"nested1": &api.StatObject{
				Attributes: map[string]*api.StatValue{
					"k1_0": statValue("v1_0"),
					"k1_1": statValue("v1_1"),
				},
				Nested: map[string]*api.StatObject{
					"nested1_1": &api.StatObject{
						Attributes: map[string]*api.StatValue{
							"k11_0": statValue("v11_0"),
							"k11_1": statValue("v11_1"),
						},
					},
				},
			},
			"nested2": &api.StatObject{
				Attributes: map[string]*api.StatValue{
					"k2": statValue("v2"),
				},
			},
		},
	}

	result := []string{"preseedkey|pressededvalue"}
	formatDeviceStats(stat, "", &result)

	// check array is appended only
	require.Equal(t, "preseedkey|pressededvalue", result[0])

	// check rest of values
	sort.Strings(result)
	expected := []string{
		"k0|v0",
		"nested1.k1_0|v1_0",
		"nested1.k1_1|v1_1",
		"nested1.nested1_1.k11_0|v11_0",
		"nested1.nested1_1.k11_1|v11_1",
		"nested2.k2|v2",
		"preseedkey|pressededvalue",
	}

	require.Equal(t, expected, result)
}
<commit_msg>gofmt -s -w command/helper_stats_test.go<commit_after>package command

import (
	"sort"
	"testing"

	"github.com/hashicorp/nomad/api"
	"github.com/hashicorp/nomad/helper"
	"github.com/stretchr/testify/require"
)

func TestDeviceQualifiedID(t *testing.T) {

	require := require.New(t)

	require.Equal("vendor/type/name[id]", deviceQualifiedID("vendor", "type", "name", "id"))
	require.Equal("vendor/type[id]", deviceQualifiedID("vendor", "type", "", "id"))
	require.Equal("vendor[id]", deviceQualifiedID("vendor", "", "", "id"))
}

func TestBuildDeviceStatsSummaryMap(t *testing.T) {
	hostStats := &api.HostStats{
		DeviceStats: []*api.DeviceGroupStats{
			{
				Vendor: "vendor1",
				Type:   "type1",
				Name:   "name1",
				InstanceStats: map[string]*api.DeviceStats{
					"id1": {
						Summary: &api.StatValue{
							StringVal: helper.StringToPtr("stat1"),
						},
					},
					"id2": {
						Summary: &api.StatValue{
							IntNumeratorVal: helper.Int64ToPtr(2),
						},
					},
				},
			},
			{
				Vendor: "vendor2",
				Type:   "type2",
				InstanceStats: map[string]*api.DeviceStats{
					"id1": {
						Summary: &api.StatValue{
							StringVal: helper.StringToPtr("stat3"),
						},
					},
					"id2": {
						Summary: &api.StatValue{
							IntNumeratorVal: helper.Int64ToPtr(4),
						},
					},
				},
			},
		},
	}

	expected := map[string]*api.StatValue{
		"vendor1/type1/name1[id1]": {
			StringVal: helper.StringToPtr("stat1"),
		},
		"vendor1/type1/name1[id2]": {
			IntNumeratorVal: helper.Int64ToPtr(2),
		},
		"vendor2/type2[id1]": {
			StringVal: helper.StringToPtr("stat3"),
		},
		"vendor2/type2[id2]": {
			IntNumeratorVal: helper.Int64ToPtr(4),
		},
	}

	require.EqualValues(t, expected, buildDeviceStatsSummaryMap(hostStats))
}

func TestFormatDeviceStats(t *testing.T) {
	statValue := func(v string) *api.StatValue {
		return &api.StatValue{
			StringVal: helper.StringToPtr(v),
		}
	}

	stat := &api.StatObject{
		Attributes: map[string]*api.StatValue{
			"k0": statValue("v0"),
		},
		Nested: map[string]*api.StatObject{
			"nested1": {
				Attributes: map[string]*api.StatValue{
					"k1_0": statValue("v1_0"),
					"k1_1": statValue("v1_1"),
				},
				Nested: map[string]*api.StatObject{
					"nested1_1": {
						Attributes: map[string]*api.StatValue{
							"k11_0": statValue("v11_0"),
							"k11_1": statValue("v11_1"),
						},
					},
				},
			},
			"nested2": {
				Attributes: map[string]*api.StatValue{
					"k2": statValue("v2"),
				},
			},
		},
	}

	result := []string{"preseedkey|pressededvalue"}
	formatDeviceStats(stat, "", &result)

	// check array is appended only
	require.Equal(t, "preseedkey|pressededvalue", result[0])

	// check rest of values
	sort.Strings(result)
	expected := []string{
		"k0|v0",
		"nested1.k1_0|v1_0",
		"nested1.k1_1|v1_1",
		"nested1.nested1_1.k11_0|v11_0",
		"nested1.nested1_1.k11_1|v11_1",
		"nested2.k2|v2",
		"preseedkey|pressededvalue",
	}

	require.Equal(t, expected, result)
}
<|endoftext|>


<commit_before>package main

import (
	"runtime"
	"time"

	twodee "../libs/twodee"
	"github.com/go-gl/gl"
)

func init() {
	// See https://code.google.com/p/go/issues/detail?id=3527
	runtime.LockOSThread()
}

type Application struct {
	layers                *twodee.Layers
	counter               *twodee.Counter
	Context               *twodee.Context
	AudioSystem           *AudioSystem
	WinBounds             twodee.Rectangle
	GameEventHandler      *twodee.GameEventHandler
	gameClosingObserverId int
	InitiateCloseGame     bool
	overlayLayer          *OverlayLayer
}

func NewApplication() (app *Application, err error) {
	var (
		layers            *twodee.Layers
		context           *twodee.Context
		winbounds         = twodee.Rect(0, 0, 640, 640)
		counter           = twodee.NewCounter()
		gameLayer         *GameLayer
		menuLayer         *MenuLayer
		hudLayer          *HudLayer
		gameEventHandler  = twodee.NewGameEventHandler(NumGameEventTypes)
		initiateCloseGame = false
	)
	if context, err = twodee.NewContext(); err != nil {
		return
	}
	context.SetFullscreen(false)
	context.SetCursor(false)
	if err = context.CreateWindow(int(winbounds.Max.X), int(winbounds.Max.Y), "LD29"); err != nil {
		return
	}
	layers = twodee.NewLayers()
	app = &Application{
		layers:            layers,
		counter:           counter,
		Context:           context,
		WinBounds:         winbounds,
		GameEventHandler:  gameEventHandler,
		InitiateCloseGame: initiateCloseGame,
	}
	if app.AudioSystem, err = NewAudioSystem(app); err != nil {
		return
	}
	if gameLayer, err = NewGameLayer(app); err != nil {
		return
	}
	if menuLayer, err = NewMenuLayer(winbounds, app); err != nil {
		return
	}
	if hudLayer, err = NewHudLayer(app, gameLayer); err != nil {
		return
	}
	if app.overlayLayer, err = NewOverlayLayer(app, gameLayer); err != nil {
		return
	}
	if err = gameLayer.LoadLevel("assets/level00/"); err != nil {
		return
	}
	app.overlayLayer.Show(OverlayTitleFrame)
	layers.Push(gameLayer)
	layers.Push(hudLayer)
	layers.Push(app.overlayLayer)
	layers.Push(menuLayer)
	app.gameClosingObserverId = app.GameEventHandler.AddObserver(GameIsClosing, app.CloseGame)
	return
}

func (a *Application) Draw() {
	a.counter.Incr()
	gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
	a.layers.Render()
}

func (a *Application) Update(elapsed time.Duration) {
	a.layers.Update(elapsed)
}

func (a *Application) Delete() {
	a.GameEventHandler.RemoveObserver(GameIsClosing, a.gameClosingObserverId)
	a.layers.Delete()
	a.AudioSystem.Delete()
	a.Context.Delete()
}

func (a *Application) ProcessEvents() {
	var (
		evt  twodee.Event
		loop = true
	)
	for loop {
		select {
		case evt = <-a.Context.Events.Events:
			a.layers.HandleEvent(evt)
		default:
			// No more events
			loop = false
		}
	}
}

func (a *Application) CloseGame(e twodee.GETyper) {
	a.InitiateCloseGame = true
}

func main() {
	var (
		app *Application
		err error
	)

	if app, err = NewApplication(); err != nil {
		panic(err)
	}
	defer app.Delete()

	var (
		current_time = time.Now()
		updated_to   = current_time
		step         = twodee.Step60Hz
	)
	for !app.Context.Window.ShouldClose() && !app.InitiateCloseGame {
		for !updated_to.After(current_time) {
			app.Update(step)
			updated_to = updated_to.Add(step)
		}
		app.Draw()
		app.Context.Window.SwapBuffers()
		app.Context.Events.Poll()
		app.GameEventHandler.Poll()
		app.ProcessEvents()
		current_time = time.Now()
	}
}
<commit_msg>Dumb fix to not take 100% cpu when backgrounded.<commit_after>package main

import (
	"runtime"
	"time"

	twodee "../libs/twodee"
	"github.com/go-gl/gl"
)

func init() {
	// See https://code.google.com/p/go/issues/detail?id=3527
	runtime.LockOSThread()
}

type Application struct {
	layers                *twodee.Layers
	counter               *twodee.Counter
	Context               *twodee.Context
	AudioSystem           *AudioSystem
	WinBounds             twodee.Rectangle
	GameEventHandler      *twodee.GameEventHandler
	gameClosingObserverId int
	InitiateCloseGame     bool
	overlayLayer          *OverlayLayer
}

func NewApplication() (app *Application, err error) {
	var (
		layers            *twodee.Layers
		context           *twodee.Context
		winbounds         = twodee.Rect(0, 0, 640, 640)
		counter           = twodee.NewCounter()
		gameLayer         *GameLayer
		menuLayer         *MenuLayer
		hudLayer          *HudLayer
		gameEventHandler  = twodee.NewGameEventHandler(NumGameEventTypes)
		initiateCloseGame = false
	)
	if context, err = twodee.NewContext(); err != nil {
		return
	}
	context.SetFullscreen(false)
	context.SetCursor(false)
	if err = context.CreateWindow(int(winbounds.Max.X), int(winbounds.Max.Y), "LD29"); err != nil {
		return
	}
	layers = twodee.NewLayers()
	app = &Application{
		layers:            layers,
		counter:           counter,
		Context:           context,
		WinBounds:         winbounds,
		GameEventHandler:  gameEventHandler,
		InitiateCloseGame: initiateCloseGame,
	}
	if app.AudioSystem, err = NewAudioSystem(app); err != nil {
		return
	}
	if gameLayer, err = NewGameLayer(app); err != nil {
		return
	}
	if menuLayer, err = NewMenuLayer(winbounds, app); err != nil {
		return
	}
	if hudLayer, err = NewHudLayer(app, gameLayer); err != nil {
		return
	}
	if app.overlayLayer, err = NewOverlayLayer(app, gameLayer); err != nil {
		return
	}
	if err = gameLayer.LoadLevel("assets/level00/"); err != nil {
		return
	}
	app.overlayLayer.Show(OverlayTitleFrame)
	layers.Push(gameLayer)
	layers.Push(hudLayer)
	layers.Push(app.overlayLayer)
	layers.Push(menuLayer)
	app.gameClosingObserverId = app.GameEventHandler.AddObserver(GameIsClosing, app.CloseGame)
	return
}

func (a *Application) Draw() {
	a.counter.Incr()
	gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
	a.layers.Render()
}

func (a *Application) Update(elapsed time.Duration) {
	a.layers.Update(elapsed)
}

func (a *Application) Delete() {
	a.GameEventHandler.RemoveObserver(GameIsClosing, a.gameClosingObserverId)
	a.layers.Delete()
	a.AudioSystem.Delete()
	a.Context.Delete()
}

func (a *Application) ProcessEvents() {
	var (
		evt  twodee.Event
		loop = true
	)
	for loop {
		select {
		case evt = <-a.Context.Events.Events:
			a.layers.HandleEvent(evt)
		default:
			// No more events
			loop = false
		}
	}
}

func (a *Application) CloseGame(e twodee.GETyper) {
	a.InitiateCloseGame = true
}

func main() {
	var (
		app *Application
		err error
	)

	if app, err = NewApplication(); err != nil {
		panic(err)
	}
	defer app.Delete()

	var (
		last_render = time.Now()
		current_time = time.Now()
		updated_to   = current_time
		step         = twodee.Step60Hz
		render_max  = step
	)
	for !app.Context.Window.ShouldClose() && !app.InitiateCloseGame {
		for !updated_to.After(current_time) {
			app.Update(step)
			updated_to = updated_to.Add(step)
		}
		if diff := current_time.Sub(last_render); diff < render_max {
			time.Sleep(diff)
		}
		app.Draw()
		app.Context.Window.SwapBuffers()
		last_render = current_time
		app.Context.Events.Poll()
		app.GameEventHandler.Poll()
		app.ProcessEvents()
		current_time = time.Now()
	}
}
<|endoftext|>