<commit_before>package web

import (
	"bytes"
	"fmt"
	"net/http"
	"net/mail"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
	"bosun.org/_third_party/github.com/bradfitz/slice"
	"bosun.org/cmd/bosun/conf"
	"bosun.org/cmd/bosun/expr"
	"bosun.org/cmd/bosun/sched"
	"bosun.org/opentsdb"
)

func Expr(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	e, err := expr.New(r.FormValue("q"), schedule.Conf.Funcs())
	if err != nil {
		return nil, err
	}
	now, err := getTime(r)
	if err != nil {
		return nil, err
	}
	res, queries, err := e.Execute(schedule.Conf.TSDBCacheContext(), schedule.Conf.GraphiteContext(), schedule.Conf.LogstashElasticHost, t, now, 0, false, schedule.Search, nil)
	if err != nil {
		return nil, err
	}
	for _, r := range res.Results {
		if r.Computations == nil {
			r.Computations = make(expr.Computations, 0)
		}
	}
	ret := struct {
		Type    string
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		e.Tree.Root.Return().String(),
		res.Results,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}

func getTime(r *http.Request) (now time.Time, err error) {
	now = time.Now().UTC()
	if fd := r.FormValue("date"); len(fd) > 0 {
		if ft := r.FormValue("time"); len(ft) > 0 {
			fd += " " + ft
		} else {
			fd += " " + now.Format("15:04")
		}
		now, err = time.Parse("2006-01-02 15:04", fd)
	}
	return
}

type Res struct {
	*sched.Event
	Key expr.AlertKey
}

func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time, summary bool, email string, template_group string) (*ruleResult, error) {
	s := &sched.Schedule{}
	s.Init(c)
	s.Metadata = schedule.Metadata
	s.Search = schedule.Search
	rh := s.NewRunHistory(now)
	if _, err := s.CheckExpr(t, rh, a, a.Warn, sched.StWarning, nil); err != nil {
		return nil, err
	}
	if _, err := s.CheckExpr(t, rh, a, a.Crit, sched.StCritical, nil); err != nil {
		return nil, err
	}
	keys := make(expr.AlertKeys, len(rh.Events))
	errors, criticals, warnings, normals := make([]expr.AlertKey, 0), make([]expr.AlertKey, 0), make([]expr.AlertKey, 0), make([]expr.AlertKey, 0)
	i := 0
	for k, v := range rh.Events {
		v.Time = now
		keys[i] = k
		i++
		switch v.Status {
		case sched.StNormal:
			normals = append(normals, k)
		case sched.StWarning:
			warnings = append(warnings, k)
		case sched.StCritical:
			criticals = append(criticals, k)
		case sched.StError:
			errors = append(errors, k)
		default:
			return nil, fmt.Errorf("unknown state type %v", v.Status)
		}
	}
	sort.Sort(keys)
	body := new(bytes.Buffer)
	subject := new(bytes.Buffer)
	var data interface{}
	warning := make([]string, 0)
	if !summary && len(keys) > 0 {
		var instance *sched.State
		if template_group != "" {
			ts, err := opentsdb.ParseTags(template_group)
			if err != nil {
				return nil, err
			}
			for _, ak := range keys {
				if ak.Group().Subset(ts) {
					instance = s.Status(ak)
					instance.History = []sched.Event{*rh.Events[ak]}
					break
				}
			}
		}
		if instance == nil {
			instance = s.Status(keys[0])
			instance.History = []sched.Event{*rh.Events[keys[0]]}
			if template_group != "" {
				warning = append(warning, fmt.Sprintf("template group %s was not a subset of any result", template_group))
			}
		}
		var b_err, s_err error
		func() {
			if _, b_err = s.ExecuteBody(body, rh, a, instance, false); b_err != nil {
				warning = append(warning, b_err.Error())
			}
			defer func() {
				if err := recover(); err != nil {
					s := fmt.Sprint(err)
					warning = append(warning, s)
					b_err = fmt.Errorf(s)
				}
			}()
		}()
		func() {
			if s_err = s.ExecuteSubject(subject, rh, a, instance); s_err != nil {
				warning = append(warning, s_err.Error())
			}
			defer func() {
				if err := recover(); err != nil {
					s := fmt.Sprint(err)
					warning = append(warning, s)
					s_err = fmt.Errorf(s)
				}
			}()
		}()
		if s_err != nil || b_err != nil {
			var err error
			subject, _, err = s.ExecuteBadTemplate(s_err, b_err, rh, a, instance)
			if err != nil {
				subject = bytes.NewBufferString(fmt.Sprintf("unable to create tempalate error notification: %v", err))
			}
		} else if email != "" {
			m, err := mail.ParseAddress(email)
			if err != nil {
				return nil, err
			}
			n := conf.Notification{
				Email: []*mail.Address{m},
			}
			email := new(bytes.Buffer)
			attachments, err := s.ExecuteBody(email, rh, a, instance, true)
			if err != nil {
				warning = append(warning, err.Error())
			} else {
				n.DoEmail(subject.Bytes(), email.Bytes(), schedule.Conf, string(instance.AlertKey()), attachments...)
			}
		}
		data = s.Data(rh, instance, a, false)
	}
	return &ruleResult{
		errors,
		criticals,
		warnings,
		normals,
		now,
		body.String(),
		subject.String(),
		data,
		rh.Events,
		warning,
	}, nil
}

type ruleResult struct {
	Errors    []expr.AlertKey
	Criticals []expr.AlertKey
	Warnings  []expr.AlertKey
	Normals   []expr.AlertKey
	Time      time.Time

	Body    string
	Subject string
	Data    interface{}
	Result  map[expr.AlertKey]*sched.Event
	Warning []string
}

func Rule(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	var from, to time.Time
	var err error
	if f := r.FormValue("from"); len(f) > 0 {
		from, err = time.Parse(tsdbFormatSecs, f)
		if err != nil {
			return nil, err
		}
	}
	if f := r.FormValue("to"); len(f) > 0 {
		to, err = time.Parse(tsdbFormatSecs, f)
		if err != nil {
			return nil, err
		}
	}
	intervals := 1
	if i := r.FormValue("intervals"); len(i) > 0 {
		intervals, err = strconv.Atoi(r.FormValue("intervals"))
		if err != nil {
			return nil, err
		}
		if intervals < 1 {
			return nil, fmt.Errorf("must be > 0 intervals")
		}
	}
	if fz, tz := from.IsZero(), to.IsZero(); fz && tz {
		from = time.Now()
	} else if fz && !tz {
		return nil, fmt.Errorf("cannot specify to without from")
	} else if !fz && tz && intervals > 1 {
		return nil, fmt.Errorf("cannot specify intervals without from and to")
	}
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "tsdbHost = %s\n", schedule.Conf.TSDBHost)
	fmt.Fprintf(&buf, "graphiteHost = %s\n", schedule.Conf.GraphiteHost)
	fmt.Fprintf(&buf, "logstashElasticHost = %s\n", schedule.Conf.LogstashElasticHost)
	fmt.Fprintf(&buf, "smtpHost = %s\n", schedule.Conf.SMTPHost)
	fmt.Fprintf(&buf, "emailFrom = %s\n", schedule.Conf.EmailFrom)
	fmt.Fprintf(&buf, "responseLimit = %d\n", schedule.Conf.ResponseLimit)
	fmt.Fprintf(&buf, "hostname = %s\n", schedule.Conf.Hostname)
	for k, v := range schedule.Conf.Vars {
		if strings.HasPrefix(k, "$") {
			fmt.Fprintf(&buf, "%s=%s\n", k, v)
		}
	}
	for _, v := range schedule.Conf.Notifications {
		fmt.Fprintln(&buf, v.Def)
	}
	fmt.Fprintf(&buf, "%s\n", r.FormValue("template"))
	fmt.Fprintf(&buf, "%s\n", r.FormValue("alert"))
	c, err := conf.New("Test Config", buf.String())
	if err != nil {
		return nil, err
	}
	if len(c.Alerts) != 1 {
		return nil, fmt.Errorf("exactly one alert must be defined")
	}
	var a *conf.Alert
	// Set a to the first alert.
	for _, a = range c.Alerts {
	}
	ch := make(chan int)
	errch := make(chan error, intervals)
	resch := make(chan *ruleResult, intervals)
	var wg sync.WaitGroup
	diff := -from.Sub(to)
	if intervals > 1 {
		diff /= time.Duration(intervals - 1)
	}
	worker := func() {
		wg.Add(1)
		for interval := range ch {
			t.Step(fmt.Sprintf("interval %v", interval), func(t miniprofiler.Timer) {
				now := from.Add(diff * time.Duration(interval))
				res, err := procRule(t, c, a, now, interval != 0, r.FormValue("email"), r.FormValue("template_group"))
				resch <- res
				errch <- err
			})
		}
		defer wg.Done()
	}
	for i := 0; i < 20; i++ {
		go worker()
	}
	for i := 0; i < intervals; i++ {
		ch <- i
	}
	close(ch)
	wg.Wait()
	close(errch)
	close(resch)
	type Result struct {
		Group  expr.AlertKey
		Result *sched.Event
	}
	type Set struct {
		Error, Critical, Warning, Normal int
		Time                             string
		Results                          []*Result `json:",omitempty"`
	}
	type History struct {
		Time, EndTime time.Time
		Status        string
	}
	type Histories struct {
		History []*History
	}
	ret := struct {
		Errors       []string `json:",omitempty"`
		Warnings     []string `json:",omitempty"`
		Sets         []*Set
		AlertHistory map[expr.AlertKey]*Histories
		Body         string      `json:",omitempty"`
		Subject      string      `json:",omitempty"`
		Data         interface{} `json:",omitempty"`
	}{
		AlertHistory: make(map[expr.AlertKey]*Histories),
	}
	for err := range errch {
		if err == nil {
			continue
		}
		ret.Errors = append(ret.Errors, err.Error())
	}
	for res := range resch {
		if res == nil {
			continue
		}
		set := Set{
			Error:    len(res.Errors),
			Critical: len(res.Criticals),
			Warning:  len(res.Warnings),
			Normal:   len(res.Normals),
			Time:     res.Time.Format(tsdbFormatSecs),
		}
		if res.Data != nil {
			ret.Body = res.Body
			ret.Subject = res.Subject
			ret.Data = res.Data
			for k, v := range res.Result {
				set.Results = append(set.Results, &Result{
					Group:  k,
					Result: v,
				})
			}
			slice.Sort(set.Results, func(i, j int) bool {
				a := set.Results[i]
				b := set.Results[j]
				if a.Result.Status != b.Result.Status {
					return a.Result.Status > b.Result.Status
				}
				return a.Group < b.Group
			})
		}
		for k, v := range res.Result {
			if ret.AlertHistory[k] == nil {
				ret.AlertHistory[k] = new(Histories)
			}
			h := ret.AlertHistory[k]
			h.History = append(h.History, &History{
				Time:   v.Time,
				Status: v.Status.String(),
			})
		}
		ret.Sets = append(ret.Sets, &set)
		ret.Warnings = append(ret.Warnings, res.Warning...)
	}
	slice.Sort(ret.Sets, func(i, j int) bool {
		return ret.Sets[i].Time < ret.Sets[j].Time
	})
	for _, histories := range ret.AlertHistory {
		hist := histories.History
		slice.Sort(hist, func(i, j int) bool {
			return hist[i].Time.Before(hist[j].Time)
		})
		for i := 1; i < len(hist); i++ {
			if i < len(hist)-1 && hist[i].Status == hist[i-1].Status {
				hist = append(hist[:i], hist[i+1:]...)
				i--
			}
		}
		for i, h := range hist[:len(hist)-1] {
			h.EndTime = hist[i+1].Time
		}
		histories.History = hist[:len(hist)-1]
	}
	return &ret, nil
}
<commit_msg>cmd/bosun: need to call defer before possible panic<commit_after>package web

import (
	"bytes"
	"fmt"
	"net/http"
	"net/mail"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"bosun.org/_third_party/github.com/MiniProfiler/go/miniprofiler"
	"bosun.org/_third_party/github.com/bradfitz/slice"
	"bosun.org/cmd/bosun/conf"
	"bosun.org/cmd/bosun/expr"
	"bosun.org/cmd/bosun/sched"
	"bosun.org/opentsdb"
)

func Expr(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	e, err := expr.New(r.FormValue("q"), schedule.Conf.Funcs())
	if err != nil {
		return nil, err
	}
	now, err := getTime(r)
	if err != nil {
		return nil, err
	}
	res, queries, err := e.Execute(schedule.Conf.TSDBCacheContext(), schedule.Conf.GraphiteContext(), schedule.Conf.LogstashElasticHost, t, now, 0, false, schedule.Search, nil)
	if err != nil {
		return nil, err
	}
	for _, r := range res.Results {
		if r.Computations == nil {
			r.Computations = make(expr.Computations, 0)
		}
	}
	ret := struct {
		Type    string
		Results []*expr.Result
		Queries map[string]opentsdb.Request
	}{
		e.Tree.Root.Return().String(),
		res.Results,
		make(map[string]opentsdb.Request),
	}
	for _, q := range queries {
		if e, err := url.QueryUnescape(q.String()); err == nil {
			ret.Queries[e] = q
		}
	}
	return ret, nil
}

func getTime(r *http.Request) (now time.Time, err error) {
	now = time.Now().UTC()
	if fd := r.FormValue("date"); len(fd) > 0 {
		if ft := r.FormValue("time"); len(ft) > 0 {
			fd += " " + ft
		} else {
			fd += " " + now.Format("15:04")
		}
		now, err = time.Parse("2006-01-02 15:04", fd)
	}
	return
}

type Res struct {
	*sched.Event
	Key expr.AlertKey
}

func procRule(t miniprofiler.Timer, c *conf.Conf, a *conf.Alert, now time.Time, summary bool, email string, template_group string) (*ruleResult, error) {
	s := &sched.Schedule{}
	s.Init(c)
	s.Metadata = schedule.Metadata
	s.Search = schedule.Search
	rh := s.NewRunHistory(now)
	if _, err := s.CheckExpr(t, rh, a, a.Warn, sched.StWarning, nil); err != nil {
		return nil, err
	}
	if _, err := s.CheckExpr(t, rh, a, a.Crit, sched.StCritical, nil); err != nil {
		return nil, err
	}
	keys := make(expr.AlertKeys, len(rh.Events))
	errors, criticals, warnings, normals := make([]expr.AlertKey, 0), make([]expr.AlertKey, 0), make([]expr.AlertKey, 0), make([]expr.AlertKey, 0)
	i := 0
	for k, v := range rh.Events {
		v.Time = now
		keys[i] = k
		i++
		switch v.Status {
		case sched.StNormal:
			normals = append(normals, k)
		case sched.StWarning:
			warnings = append(warnings, k)
		case sched.StCritical:
			criticals = append(criticals, k)
		case sched.StError:
			errors = append(errors, k)
		default:
			return nil, fmt.Errorf("unknown state type %v", v.Status)
		}
	}
	sort.Sort(keys)
	body := new(bytes.Buffer)
	subject := new(bytes.Buffer)
	var data interface{}
	warning := make([]string, 0)
	if !summary && len(keys) > 0 {
		var instance *sched.State
		if template_group != "" {
			ts, err := opentsdb.ParseTags(template_group)
			if err != nil {
				return nil, err
			}
			for _, ak := range keys {
				if ak.Group().Subset(ts) {
					instance = s.Status(ak)
					instance.History = []sched.Event{*rh.Events[ak]}
					break
				}
			}
		}
		if instance == nil {
			instance = s.Status(keys[0])
			instance.History = []sched.Event{*rh.Events[keys[0]]}
			if template_group != "" {
				warning = append(warning, fmt.Sprintf("template group %s was not a subset of any result", template_group))
			}
		}
		var b_err, s_err error
		func() {
			defer func() {
				if err := recover(); err != nil {
					s := fmt.Sprint(err)
					warning = append(warning, s)
					b_err = fmt.Errorf(s)
				}
			}()
			if _, b_err = s.ExecuteBody(body, rh, a, instance, false); b_err != nil {
				warning = append(warning, b_err.Error())
			}
		}()
		func() {
			defer func() {
				if err := recover(); err != nil {
					s := fmt.Sprint(err)
					warning = append(warning, s)
					s_err = fmt.Errorf(s)
				}
			}()
			if s_err = s.ExecuteSubject(subject, rh, a, instance); s_err != nil {
				warning = append(warning, s_err.Error())
			}
		}()
		if s_err != nil || b_err != nil {
			var err error
			subject, _, err = s.ExecuteBadTemplate(s_err, b_err, rh, a, instance)
			if err != nil {
				subject = bytes.NewBufferString(fmt.Sprintf("unable to create tempalate error notification: %v", err))
			}
		} else if email != "" {
			m, err := mail.ParseAddress(email)
			if err != nil {
				return nil, err
			}
			n := conf.Notification{
				Email: []*mail.Address{m},
			}
			email := new(bytes.Buffer)
			attachments, err := s.ExecuteBody(email, rh, a, instance, true)
			if err != nil {
				warning = append(warning, err.Error())
			} else {
				n.DoEmail(subject.Bytes(), email.Bytes(), schedule.Conf, string(instance.AlertKey()), attachments...)
			}
		}
		data = s.Data(rh, instance, a, false)
	}
	return &ruleResult{
		errors,
		criticals,
		warnings,
		normals,
		now,
		body.String(),
		subject.String(),
		data,
		rh.Events,
		warning,
	}, nil
}

type ruleResult struct {
	Errors    []expr.AlertKey
	Criticals []expr.AlertKey
	Warnings  []expr.AlertKey
	Normals   []expr.AlertKey
	Time      time.Time

	Body    string
	Subject string
	Data    interface{}
	Result  map[expr.AlertKey]*sched.Event
	Warning []string
}

func Rule(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
	var from, to time.Time
	var err error
	if f := r.FormValue("from"); len(f) > 0 {
		from, err = time.Parse(tsdbFormatSecs, f)
		if err != nil {
			return nil, err
		}
	}
	if f := r.FormValue("to"); len(f) > 0 {
		to, err = time.Parse(tsdbFormatSecs, f)
		if err != nil {
			return nil, err
		}
	}
	intervals := 1
	if i := r.FormValue("intervals"); len(i) > 0 {
		intervals, err = strconv.Atoi(r.FormValue("intervals"))
		if err != nil {
			return nil, err
		}
		if intervals < 1 {
			return nil, fmt.Errorf("must be > 0 intervals")
		}
	}
	if fz, tz := from.IsZero(), to.IsZero(); fz && tz {
		from = time.Now()
	} else if fz && !tz {
		return nil, fmt.Errorf("cannot specify to without from")
	} else if !fz && tz && intervals > 1 {
		return nil, fmt.Errorf("cannot specify intervals without from and to")
	}
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "tsdbHost = %s\n", schedule.Conf.TSDBHost)
	fmt.Fprintf(&buf, "graphiteHost = %s\n", schedule.Conf.GraphiteHost)
	fmt.Fprintf(&buf, "logstashElasticHost = %s\n", schedule.Conf.LogstashElasticHost)
	fmt.Fprintf(&buf, "smtpHost = %s\n", schedule.Conf.SMTPHost)
	fmt.Fprintf(&buf, "emailFrom = %s\n", schedule.Conf.EmailFrom)
	fmt.Fprintf(&buf, "responseLimit = %d\n", schedule.Conf.ResponseLimit)
	fmt.Fprintf(&buf, "hostname = %s\n", schedule.Conf.Hostname)
	for k, v := range schedule.Conf.Vars {
		if strings.HasPrefix(k, "$") {
			fmt.Fprintf(&buf, "%s=%s\n", k, v)
		}
	}
	for _, v := range schedule.Conf.Notifications {
		fmt.Fprintln(&buf, v.Def)
	}
	fmt.Fprintf(&buf, "%s\n", r.FormValue("template"))
	fmt.Fprintf(&buf, "%s\n", r.FormValue("alert"))
	c, err := conf.New("Test Config", buf.String())
	if err != nil {
		return nil, err
	}
	if len(c.Alerts) != 1 {
		return nil, fmt.Errorf("exactly one alert must be defined")
	}
	var a *conf.Alert
	// Set a to the first alert.
	for _, a = range c.Alerts {
	}
	ch := make(chan int)
	errch := make(chan error, intervals)
	resch := make(chan *ruleResult, intervals)
	var wg sync.WaitGroup
	diff := -from.Sub(to)
	if intervals > 1 {
		diff /= time.Duration(intervals - 1)
	}
	worker := func() {
		wg.Add(1)
		for interval := range ch {
			t.Step(fmt.Sprintf("interval %v", interval), func(t miniprofiler.Timer) {
				now := from.Add(diff * time.Duration(interval))
				res, err := procRule(t, c, a, now, interval != 0, r.FormValue("email"), r.FormValue("template_group"))
				resch <- res
				errch <- err
			})
		}
		defer wg.Done()
	}
	for i := 0; i < 20; i++ {
		go worker()
	}
	for i := 0; i < intervals; i++ {
		ch <- i
	}
	close(ch)
	wg.Wait()
	close(errch)
	close(resch)
	type Result struct {
		Group  expr.AlertKey
		Result *sched.Event
	}
	type Set struct {
		Error, Critical, Warning, Normal int
		Time                             string
		Results                          []*Result `json:",omitempty"`
	}
	type History struct {
		Time, EndTime time.Time
		Status        string
	}
	type Histories struct {
		History []*History
	}
	ret := struct {
		Errors       []string `json:",omitempty"`
		Warnings     []string `json:",omitempty"`
		Sets         []*Set
		AlertHistory map[expr.AlertKey]*Histories
		Body         string      `json:",omitempty"`
		Subject      string      `json:",omitempty"`
		Data         interface{} `json:",omitempty"`
	}{
		AlertHistory: make(map[expr.AlertKey]*Histories),
	}
	for err := range errch {
		if err == nil {
			continue
		}
		ret.Errors = append(ret.Errors, err.Error())
	}
	for res := range resch {
		if res == nil {
			continue
		}
		set := Set{
			Error:    len(res.Errors),
			Critical: len(res.Criticals),
			Warning:  len(res.Warnings),
			Normal:   len(res.Normals),
			Time:     res.Time.Format(tsdbFormatSecs),
		}
		if res.Data != nil {
			ret.Body = res.Body
			ret.Subject = res.Subject
			ret.Data = res.Data
			for k, v := range res.Result {
				set.Results = append(set.Results, &Result{
					Group:  k,
					Result: v,
				})
			}
			slice.Sort(set.Results, func(i, j int) bool {
				a := set.Results[i]
				b := set.Results[j]
				if a.Result.Status != b.Result.Status {
					return a.Result.Status > b.Result.Status
				}
				return a.Group < b.Group
			})
		}
		for k, v := range res.Result {
			if ret.AlertHistory[k] == nil {
				ret.AlertHistory[k] = new(Histories)
			}
			h := ret.AlertHistory[k]
			h.History = append(h.History, &History{
				Time:   v.Time,
				Status: v.Status.String(),
			})
		}
		ret.Sets = append(ret.Sets, &set)
		ret.Warnings = append(ret.Warnings, res.Warning...)
	}
	slice.Sort(ret.Sets, func(i, j int) bool {
		return ret.Sets[i].Time < ret.Sets[j].Time
	})
	for _, histories := range ret.AlertHistory {
		hist := histories.History
		slice.Sort(hist, func(i, j int) bool {
			return hist[i].Time.Before(hist[j].Time)
		})
		for i := 1; i < len(hist); i++ {
			if i < len(hist)-1 && hist[i].Status == hist[i-1].Status {
				hist = append(hist[:i], hist[i+1:]...)
				i--
			}
		}
		for i, h := range hist[:len(hist)-1] {
			h.EndTime = hist[i+1].Time
		}
		histories.History = hist[:len(hist)-1]
	}
	return &ret, nil
}
<|endoftext|>


<commit_before>/*
 * MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
 *
 * 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 cmd

import (
	"context"
	"path"
	"sync"

	"github.com/minio/minio/pkg/sync/errgroup"
)

func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
	disks := er.getDisks()
	// Based on the random shuffling return back randomized disks.
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		if disks[i-1] != nil && disks[i-1].IsLocal() {
			if !disks[i-1].Healing() && disks[i-1].IsOnline() {
				newDisks = append(newDisks, disks[i-1])
			}
		}
	}
	return newDisks
}

func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
	disks := er.getDisks()
	var wg sync.WaitGroup
	var mu sync.Mutex
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			if disks[i-1] == nil {
				return
			}
			di, err := disks[i-1].DiskInfo(context.Background())
			if err != nil || di.Healing {
				// - Do not consume disks which are not reachable
				//   unformatted or simply not accessible for some reason.
				//
				// - Do not consume disks which are being healed
				//
				// - Future: skip busy disks
				return
			}

			mu.Lock()
			newDisks = append(newDisks, disks[i-1])
			mu.Unlock()
		}()
	}
	return newDisks
}

// getLoadBalancedNDisks - fetches load balanced (sufficiently randomized) disk slice
// with N disks online. If ndisks is zero or negative, then it will returns all disks,
// same if ndisks is greater than the number of all disks.
func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAPI) {
	disks := er.getLoadBalancedDisks(ndisks != -1)
	for _, disk := range disks {
		newDisks = append(newDisks, disk)
		ndisks--
		if ndisks == 0 {
			break
		}
	}
	return
}

// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
// ensures to skip disks if they are not healing and online.
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
	disks := er.getDisks()

	if !optimized {
		var newDisks []StorageAPI
		for _, i := range hashOrder(UTCNow().String(), len(disks)) {
			newDisks = append(newDisks, disks[i-1])
		}
		return newDisks
	}

	var wg sync.WaitGroup
	var mu sync.Mutex
	var newDisks = map[uint64][]StorageAPI{}
	// Based on the random shuffling return back randomized disks.
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			if disks[i-1] == nil {
				return
			}
			di, err := disks[i-1].DiskInfo(context.Background())
			if err != nil || di.Healing {
				// - Do not consume disks which are not reachable
				//   unformatted or simply not accessible for some reason.
				//
				// - Do not consume disks which are being healed
				//
				// - Future: skip busy disks
				return
			}

			mu.Lock()
			// Capture disks usage wise upto resolution of MiB
			newDisks[di.Used/1024/1024] = append(newDisks[di.Used/1024/1024], disks[i-1])
			mu.Unlock()
		}()
	}
	wg.Wait()

	var max uint64
	for k := range newDisks {
		if k > max {
			max = k
		}
	}

	// Return disks which have maximum disk usage common.
	return newDisks[max]
}

// This function does the following check, suppose
// object is "a/b/c/d", stat makes sure that objects ""a/b/c""
// "a/b" and "a" do not exist.
func (er erasureObjects) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
	var isParentDirObject func(string) bool
	isParentDirObject = func(p string) bool {
		if p == "." || p == SlashSeparator {
			return false
		}
		if er.isObject(ctx, bucket, p) {
			// If there is already a file at prefix "p", return true.
			return true
		}
		// Check if there is a file as one of the parent paths.
		return isParentDirObject(path.Dir(p))
	}
	return isParentDirObject(parent)
}

// isObject - returns `true` if the prefix is an object i.e if
// `xl.meta` exists at the leaf, false otherwise.
func (er erasureObjects) isObject(ctx context.Context, bucket, prefix string) (ok bool) {
	storageDisks := er.getDisks()

	g := errgroup.WithNErrs(len(storageDisks))

	for index := range storageDisks {
		index := index
		g.Go(func() error {
			if storageDisks[index] == nil {
				return errDiskNotFound
			}
			// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
			return storageDisks[index].CheckFile(ctx, bucket, prefix)
		}, index)
	}

	// NOTE: Observe we are not trying to read `xl.meta` and figure out the actual
	// quorum intentionally, but rely on the default case scenario. Actual quorum
	// verification will happen by top layer by using getObjectInfo() and will be
	// ignored if necessary.
	readQuorum := getReadQuorum(len(storageDisks))

	return reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum) == nil
}
<commit_msg>in getOnlineDisks wait for disks to be populated (#10685)<commit_after>/*
 * MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
 *
 * 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 cmd

import (
	"context"
	"path"
	"sync"

	"github.com/minio/minio/pkg/sync/errgroup"
)

func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
	disks := er.getDisks()
	// Based on the random shuffling return back randomized disks.
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		if disks[i-1] != nil && disks[i-1].IsLocal() {
			if !disks[i-1].Healing() && disks[i-1].IsOnline() {
				newDisks = append(newDisks, disks[i-1])
			}
		}
	}
	return newDisks
}

func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
	disks := er.getDisks()
	var wg sync.WaitGroup
	var mu sync.Mutex
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			if disks[i-1] == nil {
				return
			}
			di, err := disks[i-1].DiskInfo(context.Background())
			if err != nil || di.Healing {
				// - Do not consume disks which are not reachable
				//   unformatted or simply not accessible for some reason.
				//
				// - Do not consume disks which are being healed
				//
				// - Future: skip busy disks
				return
			}

			mu.Lock()
			newDisks = append(newDisks, disks[i-1])
			mu.Unlock()
		}()
	}
	wg.Wait()
	return newDisks
}

// getLoadBalancedNDisks - fetches load balanced (sufficiently randomized) disk slice
// with N disks online. If ndisks is zero or negative, then it will returns all disks,
// same if ndisks is greater than the number of all disks.
func (er erasureObjects) getLoadBalancedNDisks(ndisks int) (newDisks []StorageAPI) {
	disks := er.getLoadBalancedDisks(ndisks != -1)
	for _, disk := range disks {
		newDisks = append(newDisks, disk)
		ndisks--
		if ndisks == 0 {
			break
		}
	}
	return
}

// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
// ensures to skip disks if they are not healing and online.
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
	disks := er.getDisks()

	if !optimized {
		var newDisks []StorageAPI
		for _, i := range hashOrder(UTCNow().String(), len(disks)) {
			newDisks = append(newDisks, disks[i-1])
		}
		return newDisks
	}

	var wg sync.WaitGroup
	var mu sync.Mutex
	var newDisks = map[uint64][]StorageAPI{}
	// Based on the random shuffling return back randomized disks.
	for _, i := range hashOrder(UTCNow().String(), len(disks)) {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			if disks[i-1] == nil {
				return
			}
			di, err := disks[i-1].DiskInfo(context.Background())
			if err != nil || di.Healing {
				// - Do not consume disks which are not reachable
				//   unformatted or simply not accessible for some reason.
				//
				// - Do not consume disks which are being healed
				//
				// - Future: skip busy disks
				return
			}

			mu.Lock()
			// Capture disks usage wise upto resolution of MiB
			newDisks[di.Used/1024/1024] = append(newDisks[di.Used/1024/1024], disks[i-1])
			mu.Unlock()
		}()
	}
	wg.Wait()

	var max uint64
	for k := range newDisks {
		if k > max {
			max = k
		}
	}

	// Return disks which have maximum disk usage common.
	return newDisks[max]
}

// This function does the following check, suppose
// object is "a/b/c/d", stat makes sure that objects ""a/b/c""
// "a/b" and "a" do not exist.
func (er erasureObjects) parentDirIsObject(ctx context.Context, bucket, parent string) bool {
	var isParentDirObject func(string) bool
	isParentDirObject = func(p string) bool {
		if p == "." || p == SlashSeparator {
			return false
		}
		if er.isObject(ctx, bucket, p) {
			// If there is already a file at prefix "p", return true.
			return true
		}
		// Check if there is a file as one of the parent paths.
		return isParentDirObject(path.Dir(p))
	}
	return isParentDirObject(parent)
}

// isObject - returns `true` if the prefix is an object i.e if
// `xl.meta` exists at the leaf, false otherwise.
func (er erasureObjects) isObject(ctx context.Context, bucket, prefix string) (ok bool) {
	storageDisks := er.getDisks()

	g := errgroup.WithNErrs(len(storageDisks))

	for index := range storageDisks {
		index := index
		g.Go(func() error {
			if storageDisks[index] == nil {
				return errDiskNotFound
			}
			// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
			return storageDisks[index].CheckFile(ctx, bucket, prefix)
		}, index)
	}

	// NOTE: Observe we are not trying to read `xl.meta` and figure out the actual
	// quorum intentionally, but rely on the default case scenario. Actual quorum
	// verification will happen by top layer by using getObjectInfo() and will be
	// ignored if necessary.
	readQuorum := getReadQuorum(len(storageDisks))

	return reduceReadQuorumErrs(ctx, g.Wait(), objectOpIgnoredErrs, readQuorum) == nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/nsf/termbox-go"
	"strconv"
	"strings"
)

var (
	//TODO: create a confirmationState, which takes a default (Y, N) and a state object whose commit it should call if accepted.
	STATE_DEFAULT     = &defaultState{}
	STATE_ENTER_MARKS = &enterMarkState{}
	STATE_COMMAND     = &commandState{}
	STATE_CONFIRM     = &confirmState{}
)

type InputState interface {
	//TODO: doesn't it feel weird that every method takes a main model?
	handleInput(m *mainModel, evt termbox.Event) (doQuit bool)
	shouldEnter(m *mainModel) bool
	statusLine(m *mainModel) string
	newCellSelected(m *mainModel)
}

type baseState struct{}

func (s *baseState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyCtrlC:
			return true
		}
	}
	return false
}

func (s *baseState) statusLine(m *mainModel) string {
	return STATUS_DEFAULT
}

func (s *baseState) newCellSelected(m *mainModel) {
	//Do nothing by default.
}

func (s *baseState) shouldEnter(m *mainModel) bool {
	return true
}

type defaultState struct {
	baseState
}

func (s *defaultState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {

	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyArrowDown:
			m.MoveSelectionDown()
		case termbox.KeyArrowLeft:
			m.MoveSelectionLeft()
		case termbox.KeyArrowRight:
			m.MoveSelectionRight()
		case termbox.KeyArrowUp:
			m.MoveSelectionUp()
		default:
			handled = false
		}
		switch evt.Ch {
		case 'c':
			m.EnterState(STATE_COMMAND)
		//TODO: 'h' should give a hint
		//TODO: '?' should print help to console
		case 'm':
			//TODO: ideally Ctrl+Num would work to put in one mark. But termbox doesn't appear to let that work.
			m.EnterState(STATE_ENTER_MARKS)
		//TODO: do this in a more general way related to DIM
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			//TODO: this is a seriously gross way of converting a rune to a string.
			num, err := strconv.Atoi(strings.Replace(strconv.QuoteRuneToASCII(evt.Ch), "'", "", -1))
			if err != nil {
				panic(err)
			}
			m.SetSelectedNumber(num)
		default:
			if !handled {
				//neither handler handled it; defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

type enterMarkState struct {
	baseState
	marksToInput []int
}

func (s *enterMarkState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEnter:
			s.commitMarks(m)
		case termbox.KeyEsc:
			m.EnterState(STATE_DEFAULT)
		default:
			handled = false
		}
		switch evt.Ch {
		//TODO: do this in a more general way related to DIM
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			//TODO: this is a seriously gross way of converting a rune to a string.
			num, err := strconv.Atoi(strings.Replace(strconv.QuoteRuneToASCII(evt.Ch), "'", "", -1))
			if err != nil {
				panic(err)
			}
			s.numberInput(num)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *enterMarkState) numberInput(num int) {
	s.marksToInput = append(s.marksToInput, num)
}

func (s *enterMarkState) commitMarks(m *mainModel) {
	for _, num := range s.marksToInput {
		m.ToggleSelectedMark(num)
	}
	s.marksToInput = nil
	m.EnterState(STATE_DEFAULT)
}

func (s *enterMarkState) shouldEnter(m *mainModel) bool {
	selected := m.Selected()
	if selected != nil {
		if selected.Number() != 0 || selected.Locked() {
			//Dion't enter mark mode.
			return false
		}
	}
	s.marksToInput = make([]int, 0)
	return true
}

func (s *enterMarkState) statusLine(m *mainModel) string {
	return STATUS_MARKING + fmt.Sprint(s.marksToInput) + STATUS_MARKING_POSTFIX
}

func (s *enterMarkState) newCellSelected(m *mainModel) {
	m.EnterState(STATE_DEFAULT)
}

type commandState struct {
	baseState
}

func (s *commandState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEsc:
			m.EnterState(STATE_DEFAULT)
		default:
			handled = false
		}
		switch evt.Ch {
		//TODO: '+' should set marks to add all Possible values that are not currently added
		//TODO: '-' should set marks list to remove any things that are not possible.
		case 'q':
			//TODO: this should use a confirmState, too.
			//...Although it's hard to do this given that we have to return whether or not to quit right now.
			return true
		case 'n':
			m.enterConfirmState("Replace grid with a new one? This is a destructive action.",
				DEFAULT_NO,
				func() {
					m.NewGrid()
				},
				func() {},
			)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *commandState) statusLine(m *mainModel) string {
	return STATUS_COMMAND
}

type defaultOption int

const (
	DEFAULT_YES defaultOption = iota
	DEFAULT_NO
	DEFAULT_NONE
)

type confirmState struct {
	msg           string
	defaultAction defaultOption
	yesAction     func()
	noAction      func()
	baseState
}

func (s *confirmState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEnter:
			switch s.defaultAction {
			case DEFAULT_YES:
				s.yesAction()
				m.EnterState(STATE_DEFAULT)
			case DEFAULT_NO:
				s.noAction()
				m.EnterState(STATE_DEFAULT)
			case DEFAULT_NONE:
				//Don't do anything
			}
		default:
			handled = false
		}
		switch evt.Ch {
		case 'y':
			s.yesAction()
			m.EnterState(STATE_DEFAULT)
		case 'n':
			s.noAction()
			m.EnterState(STATE_DEFAULT)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *confirmState) statusLine(m *mainModel) string {
	confirmMsg := "(y) or (n)"
	if s.defaultAction == DEFAULT_YES {
		confirmMsg = "(Y) or (n)"
	} else if s.defaultAction == DEFAULT_NO {
		confirmMsg = "(y) or (N)"
	}
	return s.msg + "  " + confirmMsg
}
<commit_msg>Removed a completed TODO<commit_after>package main

import (
	"fmt"
	"github.com/nsf/termbox-go"
	"strconv"
	"strings"
)

var (
	STATE_DEFAULT     = &defaultState{}
	STATE_ENTER_MARKS = &enterMarkState{}
	STATE_COMMAND     = &commandState{}
	STATE_CONFIRM     = &confirmState{}
)

type InputState interface {
	//TODO: doesn't it feel weird that every method takes a main model?
	handleInput(m *mainModel, evt termbox.Event) (doQuit bool)
	shouldEnter(m *mainModel) bool
	statusLine(m *mainModel) string
	newCellSelected(m *mainModel)
}

type baseState struct{}

func (s *baseState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyCtrlC:
			return true
		}
	}
	return false
}

func (s *baseState) statusLine(m *mainModel) string {
	return STATUS_DEFAULT
}

func (s *baseState) newCellSelected(m *mainModel) {
	//Do nothing by default.
}

func (s *baseState) shouldEnter(m *mainModel) bool {
	return true
}

type defaultState struct {
	baseState
}

func (s *defaultState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {

	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyArrowDown:
			m.MoveSelectionDown()
		case termbox.KeyArrowLeft:
			m.MoveSelectionLeft()
		case termbox.KeyArrowRight:
			m.MoveSelectionRight()
		case termbox.KeyArrowUp:
			m.MoveSelectionUp()
		default:
			handled = false
		}
		switch evt.Ch {
		case 'c':
			m.EnterState(STATE_COMMAND)
		//TODO: 'h' should give a hint
		//TODO: '?' should print help to console
		case 'm':
			//TODO: ideally Ctrl+Num would work to put in one mark. But termbox doesn't appear to let that work.
			m.EnterState(STATE_ENTER_MARKS)
		//TODO: do this in a more general way related to DIM
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			//TODO: this is a seriously gross way of converting a rune to a string.
			num, err := strconv.Atoi(strings.Replace(strconv.QuoteRuneToASCII(evt.Ch), "'", "", -1))
			if err != nil {
				panic(err)
			}
			m.SetSelectedNumber(num)
		default:
			if !handled {
				//neither handler handled it; defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

type enterMarkState struct {
	baseState
	marksToInput []int
}

func (s *enterMarkState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEnter:
			s.commitMarks(m)
		case termbox.KeyEsc:
			m.EnterState(STATE_DEFAULT)
		default:
			handled = false
		}
		switch evt.Ch {
		//TODO: do this in a more general way related to DIM
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			//TODO: this is a seriously gross way of converting a rune to a string.
			num, err := strconv.Atoi(strings.Replace(strconv.QuoteRuneToASCII(evt.Ch), "'", "", -1))
			if err != nil {
				panic(err)
			}
			s.numberInput(num)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *enterMarkState) numberInput(num int) {
	s.marksToInput = append(s.marksToInput, num)
}

func (s *enterMarkState) commitMarks(m *mainModel) {
	for _, num := range s.marksToInput {
		m.ToggleSelectedMark(num)
	}
	s.marksToInput = nil
	m.EnterState(STATE_DEFAULT)
}

func (s *enterMarkState) shouldEnter(m *mainModel) bool {
	selected := m.Selected()
	if selected != nil {
		if selected.Number() != 0 || selected.Locked() {
			//Dion't enter mark mode.
			return false
		}
	}
	s.marksToInput = make([]int, 0)
	return true
}

func (s *enterMarkState) statusLine(m *mainModel) string {
	return STATUS_MARKING + fmt.Sprint(s.marksToInput) + STATUS_MARKING_POSTFIX
}

func (s *enterMarkState) newCellSelected(m *mainModel) {
	m.EnterState(STATE_DEFAULT)
}

type commandState struct {
	baseState
}

func (s *commandState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEsc:
			m.EnterState(STATE_DEFAULT)
		default:
			handled = false
		}
		switch evt.Ch {
		//TODO: '+' should set marks to add all Possible values that are not currently added
		//TODO: '-' should set marks list to remove any things that are not possible.
		case 'q':
			//TODO: this should use a confirmState, too.
			//...Although it's hard to do this given that we have to return whether or not to quit right now.
			return true
		case 'n':
			m.enterConfirmState("Replace grid with a new one? This is a destructive action.",
				DEFAULT_NO,
				func() {
					m.NewGrid()
				},
				func() {},
			)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *commandState) statusLine(m *mainModel) string {
	return STATUS_COMMAND
}

type defaultOption int

const (
	DEFAULT_YES defaultOption = iota
	DEFAULT_NO
	DEFAULT_NONE
)

type confirmState struct {
	msg           string
	defaultAction defaultOption
	yesAction     func()
	noAction      func()
	baseState
}

func (s *confirmState) handleInput(m *mainModel, evt termbox.Event) (doQuit bool) {
	handled := true
	switch evt.Type {
	case termbox.EventKey:
		switch evt.Key {
		case termbox.KeyEnter:
			switch s.defaultAction {
			case DEFAULT_YES:
				s.yesAction()
				m.EnterState(STATE_DEFAULT)
			case DEFAULT_NO:
				s.noAction()
				m.EnterState(STATE_DEFAULT)
			case DEFAULT_NONE:
				//Don't do anything
			}
		default:
			handled = false
		}
		switch evt.Ch {
		case 'y':
			s.yesAction()
			m.EnterState(STATE_DEFAULT)
		case 'n':
			s.noAction()
			m.EnterState(STATE_DEFAULT)
		default:
			if !handled {
				//Neither of us handled it so defer to base.
				return s.baseState.handleInput(m, evt)
			}
		}
	}
	return false
}

func (s *confirmState) statusLine(m *mainModel) string {
	confirmMsg := "(y) or (n)"
	if s.defaultAction == DEFAULT_YES {
		confirmMsg = "(Y) or (n)"
	} else if s.defaultAction == DEFAULT_NO {
		confirmMsg = "(y) or (N)"
	}
	return s.msg + "  " + confirmMsg
}
<|endoftext|>


<commit_before>/*
Copyright 2019 Google Inc. All rights reserved.

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 (
	"fmt"
	"io"
	"os"

	"github.com/fatih/color"

	"github.com/google/go-jsonnet"
	"github.com/google/go-jsonnet/cmd/internal/cmd"
	"github.com/google/go-jsonnet/internal/formatter"
)

func version(o io.Writer) {
	fmt.Fprintf(o, "Jsonnet reformatter %s\n", jsonnet.Version())
}

func usage(o io.Writer) {
	version(o)
	fmt.Fprintln(o)
	fmt.Fprintln(o, "jsonnetfmt {<option>} { <filename> }")
	fmt.Fprintln(o)
	fmt.Fprintln(o, "Available options:")
	fmt.Fprintln(o, "  -h / --help             This message")
	fmt.Fprintln(o, "  -e / --exec             Treat filename as code")
	fmt.Fprintln(o, "  -o / --output-file <file> Write to the output file rather than stdout")
	fmt.Fprintln(o, "  -c / --create-output-dirs  Automatically creates all parent directories for files")
	fmt.Fprintln(o, "  -i / --in-place         Update the Jsonnet file(s) in place.")
	fmt.Fprintln(o, "  --test                  Exit with failure if reformatting changed the file(s).")
	fmt.Fprintln(o, "  -n / --indent <n>       Number of spaces to indent by (default 2, 0 means no change)")
	fmt.Fprintln(o, "  --max-blank-lines <n>   Max vertical spacing, 0 means no change (default 2)")
	fmt.Fprintln(o, "  --string-style <d|s|l>  Enforce double, single (default) quotes or 'leave'")
	fmt.Fprintln(o, "  --comment-style <h|s|l> # (h), // (s)(default), or 'leave'; never changes she-bang")
	fmt.Fprintln(o, "  --[no-]pretty-field-names Use syntax sugar for fields and indexing (on by default)")
	fmt.Fprintln(o, "  --[no-]pad-arrays       [ 1, 2, 3 ] instead of [1, 2, 3]")
	fmt.Fprintln(o, "  --[no-]pad-objects      { x: 1, y: 2 } instead of {x: 1, y: 2} (on by default)")
	fmt.Fprintln(o, "  --[no-]sort-imports     Sorting of imports (on by default)")
	fmt.Fprintln(o, "  --version               Print version")
	fmt.Fprintln(o)
	fmt.Fprintln(o, "In all cases:")
	fmt.Fprintln(o, "<filename> can be - (stdin)")
	fmt.Fprintln(o, "Multichar options are expanded e.g. -abc becomes -a -b -c.")
	fmt.Fprintln(o, "The -- option suppresses option processing for subsequent arguments.")
	fmt.Fprintln(o, "Note that since filenames and jsonnet programs can begin with -, it is advised to")
	fmt.Fprintln(o, "use -- if the argument is unknown, e.g. jsonnet -- \"$FILENAME\".")
}

type config struct {
	evalCreateOutputDirs bool
	filenameIsCode       bool
	inPlace              bool
	inputFiles           []string
	options              formatter.Options
	outputFile           string
	test                 bool
}

func makeConfig() config {
	return config{
		options: formatter.DefaultOptions(),
	}
}

type processArgsStatus int

const (
	processArgsStatusContinue     = iota
	processArgsStatusSuccessUsage = iota
	processArgsStatusFailureUsage = iota
	processArgsStatusSuccess      = iota
	processArgsStatusFailure      = iota
)

func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArgsStatus, error) {
	args := cmd.SimplifyArgs(givenArgs)
	remainingArgs := make([]string, 0, len(args))
	i := 0

	for ; i < len(args); i++ {
		arg := args[i]
		if arg == "-h" || arg == "--help" {
			return processArgsStatusSuccessUsage, nil
		} else if arg == "-v" || arg == "--version" {
			version(os.Stdout)
			return processArgsStatusSuccess, nil
		} else if arg == "-e" || arg == "--exec" {
			config.filenameIsCode = true
		} else if arg == "-o" || arg == "--output-file" {
			outputFile := cmd.NextArg(&i, args)
			if len(outputFile) == 0 {
				return processArgsStatusFailure, fmt.Errorf("-o argument was empty string")
			}
			config.outputFile = outputFile
		} else if arg == "--" {
			// All subsequent args are not options.
			i++
			for ; i < len(args); i++ {
				remainingArgs = append(remainingArgs, args[i])
			}
			break
		} else if arg == "-i" || arg == "--in-place" {
			config.inPlace = true
		} else if arg == "--test" {
			config.test = true
		} else if arg == "-n" || arg == "--indent" {
			n := cmd.SafeStrToInt(cmd.NextArg(&i, args))
			if n < 0 {
				return processArgsStatusFailure, fmt.Errorf("invalid --indent value: %d", n)
			}
			config.options.Indent = n
		} else if arg == "--max-blank-lines" {
			n := cmd.SafeStrToInt(cmd.NextArg(&i, args))
			if n < 0 {
				return processArgsStatusFailure, fmt.Errorf("invalid --max-blank-lines value: %d", n)
			}
			config.options.MaxBlankLines = n
		} else if arg == "--string-style" {
			str := cmd.NextArg(&i, args)
			switch str {
			case "d":
				config.options.StringStyle = formatter.StringStyleDouble
			case "s":
				config.options.StringStyle = formatter.StringStyleSingle
			case "l":
				config.options.StringStyle = formatter.StringStyleLeave
			default:
				return processArgsStatusFailure, fmt.Errorf("invalid --string-style value: %s", str)
			}
		} else if arg == "--comment-style" {
			str := cmd.NextArg(&i, args)
			switch str {
			case "h":
				config.options.CommentStyle = formatter.CommentStyleHash
			case "s":
				config.options.CommentStyle = formatter.CommentStyleSlash
			case "l":
				config.options.CommentStyle = formatter.CommentStyleLeave
			default:
				return processArgsStatusFailure, fmt.Errorf("invalid --comment-style value: %s", str)
			}
		} else if arg == "--pretty-field-names" {
			config.options.PrettyFieldNames = true
		} else if arg == "--no-pretty-field-names" {
			config.options.PrettyFieldNames = false
		} else if arg == "--pad-arrays" {
			config.options.PadArrays = true
		} else if arg == "--no-pad-arrays" {
			config.options.PadArrays = false
		} else if arg == "--pad-objects" {
			config.options.PadObjects = true
		} else if arg == "--no-pad-objects" {
			config.options.PadObjects = false
		} else if arg == "--sort-imports" {
			config.options.SortImports = true
		} else if arg == "--no-sort-imports" {
			config.options.SortImports = false
		} else if arg == "-c" || arg == "--create-output-dirs" {
			config.evalCreateOutputDirs = true
		} else if len(arg) > 1 && arg[0] == '-' {
			return processArgsStatusFailure, fmt.Errorf("unrecognized argument: %s", arg)
		} else {
			remainingArgs = append(remainingArgs, arg)
		}
	}

	want := "filename"
	if config.filenameIsCode {
		want = "code"
	}
	if len(remainingArgs) == 0 {
		return processArgsStatusFailureUsage, fmt.Errorf("must give %s", want)
	}

	if !config.test && !config.inPlace {
		if len(remainingArgs) > 1 {
			return processArgsStatusFailure, fmt.Errorf("only one %s is allowed", want)
		}
	}

	config.inputFiles = remainingArgs
	return processArgsStatusContinue, nil
}

func main() {
	cmd.StartCPUProfile()
	defer cmd.StopCPUProfile()

	vm := jsonnet.MakeVM()
	vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf)

	config := makeConfig()

	status, err := processArgs(os.Args[1:], &config, vm)
	if err != nil {
		fmt.Fprintln(os.Stderr, "ERROR: "+err.Error())
	}
	switch status {
	case processArgsStatusContinue:
		break
	case processArgsStatusSuccessUsage:
		usage(os.Stdout)
		os.Exit(0)
	case processArgsStatusFailureUsage:
		if err != nil {
			fmt.Fprintln(os.Stderr, "")
		}
		usage(os.Stderr)
		os.Exit(1)
	case processArgsStatusSuccess:
		os.Exit(0)
	case processArgsStatusFailure:
		os.Exit(1)
	}

	if config.inPlace || config.test {
		if len(config.inputFiles) == 0 {
			// Should already have been caught by processArgs.
			panic("Internal error: expected at least one input file.")
		}
		for _, inputFile := range config.inputFiles {
			outputFile := inputFile
			if config.inPlace {
				if inputFile == "-" {
					fmt.Fprintf(os.Stderr, "ERROR: cannot use --in-place with stdin\n")
					os.Exit(1)
				}
				if config.filenameIsCode {
					fmt.Fprintf(os.Stderr, "ERROR: cannot use --in-place with --exec\n")
					os.Exit(1)
				}
			}
			input := cmd.SafeReadInput(config.filenameIsCode, &inputFile)
			output, err := formatter.Format(inputFile, input, config.options)
			cmd.MemProfile()
			if err != nil {
				fmt.Fprintln(os.Stderr, err.Error())
				os.Exit(1)
			}

			if output != input {
				if config.inPlace {
					err := cmd.WriteOutputFile(output, outputFile, false)
					if err != nil {
						fmt.Fprintln(os.Stderr, err.Error())
						os.Exit(1)
					}
				} else {
					os.Exit(2)
				}
			}
		}

	} else {
		if len(config.inputFiles) != 1 {
			// Should already have been caught by processArgs.
			panic("Internal error: expected a single input file.")
		}
		inputFile := config.inputFiles[0]
		input := cmd.SafeReadInput(config.filenameIsCode, &inputFile)
		output, err := formatter.Format(inputFile, input, config.options)
		cmd.MemProfile()
		if err != nil {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}

		err = cmd.WriteOutputFile(output, config.outputFile, true)
		if err != nil {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}
	}

}
<commit_msg>jsonnetfmt --help: Indent subsection and break long lines<commit_after>/*
Copyright 2019 Google Inc. All rights reserved.

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 (
	"fmt"
	"io"
	"os"

	"github.com/fatih/color"

	"github.com/google/go-jsonnet"
	"github.com/google/go-jsonnet/cmd/internal/cmd"
	"github.com/google/go-jsonnet/internal/formatter"
)

func version(o io.Writer) {
	fmt.Fprintf(o, "Jsonnet reformatter %s\n", jsonnet.Version())
}

func usage(o io.Writer) {
	version(o)
	fmt.Fprintln(o)
	fmt.Fprintln(o, "jsonnetfmt {<option>} { <filename> }")
	fmt.Fprintln(o)
	fmt.Fprintln(o, "Available options:")
	fmt.Fprintln(o, "  -h / --help                This message")
	fmt.Fprintln(o, "  -e / --exec                Treat filename as code")
	fmt.Fprintln(o, "  -o / --output-file <file>  Write to the output file rather than stdout")
	fmt.Fprintln(o, "  -c / --create-output-dirs  Automatically creates all parent directories for")
	fmt.Fprintln(o, "                             files")
	fmt.Fprintln(o, "  -i / --in-place            Update the Jsonnet file(s) in place")
	fmt.Fprintln(o, "  --test                     Exit with failure if reformatting changed the")
	fmt.Fprintln(o, "                             file(s)")
	fmt.Fprintln(o, "  -n / --indent <n>          Number of spaces to indent by")
	fmt.Fprintln(o, "                             (default 2, 0 means no change)")
	fmt.Fprintln(o, "  --max-blank-lines <n>      Max vertical spacing (default 2, 0 means no change)")
	fmt.Fprintln(o, "  --string-style <d|s|l>     Enforce double, single (default) quotes or 'leave'")
	fmt.Fprintln(o, "  --comment-style <h|s|l>    # (h), // (s) (default), or 'leave'; never changes")
	fmt.Fprintln(o, "                             she-bang")
	fmt.Fprintln(o, "  --[no-]pretty-field-names  Use syntax sugar for fields and indexing")
	fmt.Fprintln(o, "                             (on by default)")
	fmt.Fprintln(o, "  --[no-]pad-arrays          [ 1, 2, 3 ] instead of [1, 2, 3]")
	fmt.Fprintln(o, "  --[no-]pad-objects         { x: 1, y: 2 } instead of {x: 1, y: 2}")
	fmt.Fprintln(o, "                             (on by default)")
	fmt.Fprintln(o, "  --[no-]sort-imports        Sorting of imports (on by default)")
	fmt.Fprintln(o, "  --version                  Print version")
	fmt.Fprintln(o)
	fmt.Fprintln(o, "In all cases:")
	fmt.Fprintln(o, "  <filename> can be - (stdin)")
	fmt.Fprintln(o, "  Multichar options are expanded e.g. -abc becomes -a -b -c.")
	fmt.Fprintln(o, "  The -- option suppresses option processing for subsequent arguments.")
	fmt.Fprintln(o, "  Note that since filenames and jsonnet programs can begin with -, it is")
	fmt.Fprintln(o, "  advised to use -- if the argument is unknown, e.g. jsonnetfmt -- \"$FILENAME\".")
}

type config struct {
	evalCreateOutputDirs bool
	filenameIsCode       bool
	inPlace              bool
	inputFiles           []string
	options              formatter.Options
	outputFile           string
	test                 bool
}

func makeConfig() config {
	return config{
		options: formatter.DefaultOptions(),
	}
}

type processArgsStatus int

const (
	processArgsStatusContinue     = iota
	processArgsStatusSuccessUsage = iota
	processArgsStatusFailureUsage = iota
	processArgsStatusSuccess      = iota
	processArgsStatusFailure      = iota
)

func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArgsStatus, error) {
	args := cmd.SimplifyArgs(givenArgs)
	remainingArgs := make([]string, 0, len(args))
	i := 0

	for ; i < len(args); i++ {
		arg := args[i]
		if arg == "-h" || arg == "--help" {
			return processArgsStatusSuccessUsage, nil
		} else if arg == "-v" || arg == "--version" {
			version(os.Stdout)
			return processArgsStatusSuccess, nil
		} else if arg == "-e" || arg == "--exec" {
			config.filenameIsCode = true
		} else if arg == "-o" || arg == "--output-file" {
			outputFile := cmd.NextArg(&i, args)
			if len(outputFile) == 0 {
				return processArgsStatusFailure, fmt.Errorf("-o argument was empty string")
			}
			config.outputFile = outputFile
		} else if arg == "--" {
			// All subsequent args are not options.
			i++
			for ; i < len(args); i++ {
				remainingArgs = append(remainingArgs, args[i])
			}
			break
		} else if arg == "-i" || arg == "--in-place" {
			config.inPlace = true
		} else if arg == "--test" {
			config.test = true
		} else if arg == "-n" || arg == "--indent" {
			n := cmd.SafeStrToInt(cmd.NextArg(&i, args))
			if n < 0 {
				return processArgsStatusFailure, fmt.Errorf("invalid --indent value: %d", n)
			}
			config.options.Indent = n
		} else if arg == "--max-blank-lines" {
			n := cmd.SafeStrToInt(cmd.NextArg(&i, args))
			if n < 0 {
				return processArgsStatusFailure, fmt.Errorf("invalid --max-blank-lines value: %d", n)
			}
			config.options.MaxBlankLines = n
		} else if arg == "--string-style" {
			str := cmd.NextArg(&i, args)
			switch str {
			case "d":
				config.options.StringStyle = formatter.StringStyleDouble
			case "s":
				config.options.StringStyle = formatter.StringStyleSingle
			case "l":
				config.options.StringStyle = formatter.StringStyleLeave
			default:
				return processArgsStatusFailure, fmt.Errorf("invalid --string-style value: %s", str)
			}
		} else if arg == "--comment-style" {
			str := cmd.NextArg(&i, args)
			switch str {
			case "h":
				config.options.CommentStyle = formatter.CommentStyleHash
			case "s":
				config.options.CommentStyle = formatter.CommentStyleSlash
			case "l":
				config.options.CommentStyle = formatter.CommentStyleLeave
			default:
				return processArgsStatusFailure, fmt.Errorf("invalid --comment-style value: %s", str)
			}
		} else if arg == "--pretty-field-names" {
			config.options.PrettyFieldNames = true
		} else if arg == "--no-pretty-field-names" {
			config.options.PrettyFieldNames = false
		} else if arg == "--pad-arrays" {
			config.options.PadArrays = true
		} else if arg == "--no-pad-arrays" {
			config.options.PadArrays = false
		} else if arg == "--pad-objects" {
			config.options.PadObjects = true
		} else if arg == "--no-pad-objects" {
			config.options.PadObjects = false
		} else if arg == "--sort-imports" {
			config.options.SortImports = true
		} else if arg == "--no-sort-imports" {
			config.options.SortImports = false
		} else if arg == "-c" || arg == "--create-output-dirs" {
			config.evalCreateOutputDirs = true
		} else if len(arg) > 1 && arg[0] == '-' {
			return processArgsStatusFailure, fmt.Errorf("unrecognized argument: %s", arg)
		} else {
			remainingArgs = append(remainingArgs, arg)
		}
	}

	want := "filename"
	if config.filenameIsCode {
		want = "code"
	}
	if len(remainingArgs) == 0 {
		return processArgsStatusFailureUsage, fmt.Errorf("must give %s", want)
	}

	if !config.test && !config.inPlace {
		if len(remainingArgs) > 1 {
			return processArgsStatusFailure, fmt.Errorf("only one %s is allowed", want)
		}
	}

	config.inputFiles = remainingArgs
	return processArgsStatusContinue, nil
}

func main() {
	cmd.StartCPUProfile()
	defer cmd.StopCPUProfile()

	vm := jsonnet.MakeVM()
	vm.ErrorFormatter.SetColorFormatter(color.New(color.FgRed).Fprintf)

	config := makeConfig()

	status, err := processArgs(os.Args[1:], &config, vm)
	if err != nil {
		fmt.Fprintln(os.Stderr, "ERROR: "+err.Error())
	}
	switch status {
	case processArgsStatusContinue:
		break
	case processArgsStatusSuccessUsage:
		usage(os.Stdout)
		os.Exit(0)
	case processArgsStatusFailureUsage:
		if err != nil {
			fmt.Fprintln(os.Stderr, "")
		}
		usage(os.Stderr)
		os.Exit(1)
	case processArgsStatusSuccess:
		os.Exit(0)
	case processArgsStatusFailure:
		os.Exit(1)
	}

	if config.inPlace || config.test {
		if len(config.inputFiles) == 0 {
			// Should already have been caught by processArgs.
			panic("Internal error: expected at least one input file.")
		}
		for _, inputFile := range config.inputFiles {
			outputFile := inputFile
			if config.inPlace {
				if inputFile == "-" {
					fmt.Fprintf(os.Stderr, "ERROR: cannot use --in-place with stdin\n")
					os.Exit(1)
				}
				if config.filenameIsCode {
					fmt.Fprintf(os.Stderr, "ERROR: cannot use --in-place with --exec\n")
					os.Exit(1)
				}
			}
			input := cmd.SafeReadInput(config.filenameIsCode, &inputFile)
			output, err := formatter.Format(inputFile, input, config.options)
			cmd.MemProfile()
			if err != nil {
				fmt.Fprintln(os.Stderr, err.Error())
				os.Exit(1)
			}

			if output != input {
				if config.inPlace {
					err := cmd.WriteOutputFile(output, outputFile, false)
					if err != nil {
						fmt.Fprintln(os.Stderr, err.Error())
						os.Exit(1)
					}
				} else {
					os.Exit(2)
				}
			}
		}

	} else {
		if len(config.inputFiles) != 1 {
			// Should already have been caught by processArgs.
			panic("Internal error: expected a single input file.")
		}
		inputFile := config.inputFiles[0]
		input := cmd.SafeReadInput(config.filenameIsCode, &inputFile)
		output, err := formatter.Format(inputFile, input, config.options)
		cmd.MemProfile()
		if err != nil {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}

		err = cmd.WriteOutputFile(output, config.outputFile, true)
		if err != nil {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}
	}

}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"launchpad.net/gnuflag"
	. "launchpad.net/gocheck"
	_ "launchpad.net/juju-core/environs/dummy"
	"launchpad.net/juju-core/testing"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	stdtesting "testing"
)

func TestPackage(t *stdtesting.T) {
	testing.MgoTestPackage(t)
}

type MainSuite struct{}

var _ = Suite(&MainSuite{})

var flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")

// Reentrancy point for testing (something as close as possible to) the juju
// tool itself.
func TestRunMain(t *stdtesting.T) {
	if *flagRunMain {
		Main(flag.Args())
	}
}

func badrun(c *C, exit int, cmd ...string) string {
	args := append([]string{"-test.run", "TestRunMain", "-run-main", "--", "juju"}, cmd...)
	ps := exec.Command(os.Args[0], args...)
	output, err := ps.CombinedOutput()
	if exit != 0 {
		c.Assert(err, ErrorMatches, fmt.Sprintf("exit status %d", exit))
	}
	return string(output)
}

func deployHelpText() string {
	buff := &bytes.Buffer{}
	dc := &DeployCommand{}
	info := dc.Info()
	info.Name = "juju deploy"
	f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
	dc.SetFlags(f)
	buff.Write(info.Help(f))
	return buff.String()
}

var runMainTests = []struct {
	summary string
	args    []string
	code    int
	out     string
}{
	{
		summary: "no params shows help",
		args:    []string{},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help is the same as juju",
		args:    []string{"help"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju --help works too",
		args:    []string{"--help"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help basics is the same as juju",
		args:    []string{"help", "basics"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help foo doesn't exist",
		args:    []string{"help", "foo"},
		code:    1,
		out:     "error: unknown command or topic for foo\n",
	}, {
		summary: "juju help deploy shows the default help without global options",
		args:    []string{"help", "deploy"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "juju --help deploy shows the same help as 'help deploy'",
		args:    []string{"--help", "deploy"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "juju deploy --help shows the same help as 'help deploy'",
		args:    []string{"deploy", "--help"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "unknown command",
		args:    []string{"discombobulate"},
		code:    2,
		out:     "error: unrecognized command: juju discombobulate\n",
	}, {
		summary: "unknown option before command",
		args:    []string{"--cheese", "bootstrap"},
		code:    2,
		out:     "error: flag provided but not defined: --cheese\n",
	}, {
		summary: "unknown option after command",
		args:    []string{"bootstrap", "--cheese"},
		code:    2,
		out:     "error: flag provided but not defined: --cheese\n",
	}, {
		summary: "known option, but specified before command",
		args:    []string{"--environment", "blah", "bootstrap"},
		code:    2,
		out:     "error: flag provided but not defined: --environment\n",
	},
}

func (s *MainSuite) TestRunMain(c *C) {
	for i, t := range runMainTests {
		c.Logf("test %d: %s", i, t.summary)
		out := badrun(c, t.code, t.args...)
		c.Assert(out, Equals, t.out)
	}
}

var brokenConfig = `
environments:
    one:
        type: dummy
        state-server: false
        authorized-keys: i-am-a-key
        broken: %s
`

// breakJuju forces the dummy environment to return an error
// when environMethod is called.
func breakJuju(c *C, environMethod string) (msg string) {
	yaml := fmt.Sprintf(brokenConfig, environMethod)
	err := ioutil.WriteFile(testing.HomePath(".juju", "environments.yaml"), []byte(yaml), 0666)
	c.Assert(err, IsNil)

	return fmt.Sprintf("dummy.%s is broken", environMethod)
}

func (s *MainSuite) TestActualRunJujuArgsBeforeCommand(c *C) {
	defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()
	// Check global args work when specified before command
	msg := breakJuju(c, "Bootstrap")
	logpath := filepath.Join(c.MkDir(), "log")
	out := badrun(c, 1, "--log-file", logpath, "--verbose", "--debug", "bootstrap")
	c.Assert(out, Equals, "error: "+msg+"\n")
	content, err := ioutil.ReadFile(logpath)
	c.Assert(err, IsNil)
	fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
	c.Assert(string(content), Matches, fullmsg)
}

func (s *MainSuite) TestActualRunJujuArgsAfterCommand(c *C) {
	defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()

	// Check global args work when specified after command
	msg := breakJuju(c, "Bootstrap")
	logpath := filepath.Join(c.MkDir(), "log")
	out := badrun(c, 1, "bootstrap", "--log-file", logpath, "--verbose", "--debug")
	c.Assert(out, Equals, "error: "+msg+"\n")
	content, err := ioutil.ReadFile(logpath)
	c.Assert(err, IsNil)
	fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
	c.Assert(string(content), Matches, fullmsg)
}

var commandNames = []string{
	"add-relation",
	"add-unit",
	"bootstrap",
	"deploy",
	"destroy-environment",
	"destroy-machine",
	"destroy-relation",
	"destroy-service",
	"destroy-unit",
	"expose",
	"generate-config", // alias for init
	"get",
	"get-constraints",
	"help",
	"init",
	"remove-relation", // alias for destroy-relation
	"remove-unit",     // alias for destroy-unit
	"resolved",
	"scp",
	"set",
	"set-constraints",
	"ssh",
	"stat", // alias for status
	"status",
	"terminate-machine", // alias for destroy-machine
	"unexpose",
	"upgrade-juju",
	"version",
}

func (s *MainSuite) TestHelpCommands(c *C) {
	// Check that we have correctly registered all the commands
	// by checking the help output.
	out := badrun(c, 0, "help", "commands")
	lines := strings.Split(out, "\n")
	var names []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 {
			continue
		}
		names = append(names, f[0])
	}
	// The names should be output in alphabetical order, so don't sort.
	c.Assert(names, DeepEquals, commandNames)
}

var topicNames = []string{
	"basics",
	"commands",
	"global-options",
	"topics",
}

func (s *MainSuite) TestHelpTopics(c *C) {
	// Check that we have correctly registered all the topics
	// by checking the help output.
	out := badrun(c, 0, "help", "topics")
	lines := strings.Split(out, "\n")
	var names []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 {
			continue
		}
		names = append(names, f[0])
	}
	// The names should be output in alphabetical order, so don't sort.
	c.Assert(names, DeepEquals, topicNames)
}

var globalFlags = []string{
	"--debug .*",
	"-h, --help .*",
	"--log-file .*",
	"-v, --verbose .*",
}

func (s *MainSuite) TestHelpGlobalOptions(c *C) {
	// Check that we have correctly registered all the topics
	// by checking the help output.
	out := badrun(c, 0, "help", "global-options")
	c.Assert(out, Matches, `Global Options

These options may be used with any command, and may appear in front of any
command\.(.|\n)*`)
	lines := strings.Split(out, "\n")
	var flags []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 || line[0] != '-' {
			continue
		}
		flags = append(flags, line)
	}
	c.Assert(len(flags), Equals, len(globalFlags))
	for i, line := range flags {
		c.Assert(line, Matches, globalFlags[i])
	}
}
<commit_msg>responding to review feedback<commit_after>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"launchpad.net/gnuflag"
	. "launchpad.net/gocheck"
	_ "launchpad.net/juju-core/environs/dummy"
	"launchpad.net/juju-core/testing"
	"launchpad.net/juju-core/version"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	stdtesting "testing"
)

func TestPackage(t *stdtesting.T) {
	testing.MgoTestPackage(t)
}

type MainSuite struct{}

var _ = Suite(&MainSuite{})

var flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")

// Reentrancy point for testing (something as close as possible to) the juju
// tool itself.
func TestRunMain(t *stdtesting.T) {
	if *flagRunMain {
		Main(flag.Args())
	}
}

func badrun(c *C, exit int, cmd ...string) string {
	args := append([]string{"-test.run", "TestRunMain", "-run-main", "--", "juju"}, cmd...)
	ps := exec.Command(os.Args[0], args...)
	output, err := ps.CombinedOutput()
	if exit != 0 {
		c.Assert(err, ErrorMatches, fmt.Sprintf("exit status %d", exit))
	}
	return string(output)
}

func deployHelpText() string {
	buff := &bytes.Buffer{}
	dc := &DeployCommand{}
	info := dc.Info()
	info.Name = "juju deploy"
	f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
	dc.SetFlags(f)
	buff.Write(info.Help(f))
	return buff.String()
}

var runMainTests = []struct {
	summary string
	args    []string
	code    int
	out     string
}{
	{
		summary: "no params shows help",
		args:    []string{},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help is the same as juju",
		args:    []string{"help"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju --help works too",
		args:    []string{"--help"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help basics is the same as juju",
		args:    []string{"help", "basics"},
		code:    0,
		out:     strings.TrimLeft(helpBasics, "\n"),
	}, {
		summary: "juju help foo doesn't exist",
		args:    []string{"help", "foo"},
		code:    1,
		out:     "error: unknown command or topic for foo\n",
	}, {
		summary: "juju help deploy shows the default help without global options",
		args:    []string{"help", "deploy"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "juju --help deploy shows the same help as 'help deploy'",
		args:    []string{"--help", "deploy"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "juju deploy --help shows the same help as 'help deploy'",
		args:    []string{"deploy", "--help"},
		code:    0,
		out:     deployHelpText(),
	}, {
		summary: "unknown command",
		args:    []string{"discombobulate"},
		code:    2,
		out:     "error: unrecognized command: juju discombobulate\n",
	}, {
		summary: "unknown option before command",
		args:    []string{"--cheese", "bootstrap"},
		code:    2,
		out:     "error: flag provided but not defined: --cheese\n",
	}, {
		summary: "unknown option after command",
		args:    []string{"bootstrap", "--cheese"},
		code:    2,
		out:     "error: flag provided but not defined: --cheese\n",
	}, {
		summary: "known option, but specified before command",
		args:    []string{"--environment", "blah", "bootstrap"},
		code:    2,
		out:     "error: flag provided but not defined: --environment\n",
	}, {
		summary: "check version command registered properly",
		args:    []string{"version"},
		code:    0,
		out:     version.Current.String() + "\n",
	},
}

func (s *MainSuite) TestRunMain(c *C) {
	for i, t := range runMainTests {
		c.Logf("test %d: %s", i, t.summary)
		out := badrun(c, t.code, t.args...)
		c.Assert(out, Equals, t.out)
	}
}

var brokenConfig = `
environments:
    one:
        type: dummy
        state-server: false
        authorized-keys: i-am-a-key
        broken: %s
`

// breakJuju forces the dummy environment to return an error
// when environMethod is called.
func breakJuju(c *C, environMethod string) (msg string) {
	yaml := fmt.Sprintf(brokenConfig, environMethod)
	err := ioutil.WriteFile(testing.HomePath(".juju", "environments.yaml"), []byte(yaml), 0666)
	c.Assert(err, IsNil)

	return fmt.Sprintf("dummy.%s is broken", environMethod)
}

func (s *MainSuite) TestActualRunJujuArgsBeforeCommand(c *C) {
	defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()
	// Check global args work when specified before command
	msg := breakJuju(c, "Bootstrap")
	logpath := filepath.Join(c.MkDir(), "log")
	out := badrun(c, 1, "--log-file", logpath, "--verbose", "--debug", "bootstrap")
	c.Assert(out, Equals, "error: "+msg+"\n")
	content, err := ioutil.ReadFile(logpath)
	c.Assert(err, IsNil)
	fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
	c.Assert(string(content), Matches, fullmsg)
}

func (s *MainSuite) TestActualRunJujuArgsAfterCommand(c *C) {
	defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()

	// Check global args work when specified after command
	msg := breakJuju(c, "Bootstrap")
	logpath := filepath.Join(c.MkDir(), "log")
	out := badrun(c, 1, "bootstrap", "--log-file", logpath, "--verbose", "--debug")
	c.Assert(out, Equals, "error: "+msg+"\n")
	content, err := ioutil.ReadFile(logpath)
	c.Assert(err, IsNil)
	fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
	c.Assert(string(content), Matches, fullmsg)
}

var commandNames = []string{
	"add-relation",
	"add-unit",
	"bootstrap",
	"deploy",
	"destroy-environment",
	"destroy-machine",
	"destroy-relation",
	"destroy-service",
	"destroy-unit",
	"expose",
	"generate-config", // alias for init
	"get",
	"get-constraints",
	"help",
	"init",
	"remove-relation", // alias for destroy-relation
	"remove-unit",     // alias for destroy-unit
	"resolved",
	"scp",
	"set",
	"set-constraints",
	"ssh",
	"stat", // alias for status
	"status",
	"terminate-machine", // alias for destroy-machine
	"unexpose",
	"upgrade-juju",
	"version",
}

func (s *MainSuite) TestHelpCommands(c *C) {
	// Check that we have correctly registered all the commands
	// by checking the help output.
	out := badrun(c, 0, "help", "commands")
	lines := strings.Split(out, "\n")
	var names []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 {
			continue
		}
		names = append(names, f[0])
	}
	// The names should be output in alphabetical order, so don't sort.
	c.Assert(names, DeepEquals, commandNames)
}

var topicNames = []string{
	"basics",
	"commands",
	"global-options",
	"topics",
}

func (s *MainSuite) TestHelpTopics(c *C) {
	// Check that we have correctly registered all the topics
	// by checking the help output.
	out := badrun(c, 0, "help", "topics")
	lines := strings.Split(out, "\n")
	var names []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 {
			continue
		}
		names = append(names, f[0])
	}
	// The names should be output in alphabetical order, so don't sort.
	c.Assert(names, DeepEquals, topicNames)
}

var globalFlags = []string{
	"--debug .*",
	"-h, --help .*",
	"--log-file .*",
	"-v, --verbose .*",
}

func (s *MainSuite) TestHelpGlobalOptions(c *C) {
	// Check that we have correctly registered all the topics
	// by checking the help output.
	out := badrun(c, 0, "help", "global-options")
	c.Assert(out, Matches, `Global Options

These options may be used with any command, and may appear in front of any
command\.(.|\n)*`)
	lines := strings.Split(out, "\n")
	var flags []string
	for _, line := range lines {
		f := strings.Fields(line)
		if len(f) == 0 || line[0] != '-' {
			continue
		}
		flags = append(flags, line)
	}
	c.Assert(len(flags), Equals, len(globalFlags))
	for i, line := range flags {
		c.Assert(line, Matches, globalFlags[i])
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Rook Authors. All rights reserved.

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 rook

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"text/tabwriter"
	"time"

	"github.com/coreos/pkg/capnslog"
	"github.com/rook/rook/pkg/model"
	"github.com/rook/rook/pkg/rook/client"
	"github.com/rook/rook/pkg/util/flags"
	"github.com/spf13/cobra"
)

var (
	APIServerEndpoint string
	logLevelRaw       string
)

const (
	outputPadding  = 3
	outputMinWidth = 10
	outputTabWidth = 0
	outputPadChar  = ' '
	timeoutSecs    = 10
)

var RootCmd = &cobra.Command{
	Use:   "rook",
	Short: "A command line client for working with a rook cluster",
	Long: `A command line client for working with a rook cluster.
https://github.com/rook/rook`,
}

func init() {
	defaultEndpoint := fmt.Sprintf("127.0.0.1:%d", model.Port)
	RootCmd.PersistentFlags().StringVar(&APIServerEndpoint, "api-server-endpoint", defaultEndpoint, "IP endpoint of API server instance (required)")
	RootCmd.PersistentFlags().StringVar(&logLevelRaw, "log-level", "WARNING", "logging level for logging/tracing output (valid values: CRITICAL,ERROR,WARNING,NOTICE,INFO,DEBUG,TRACE)")

	RootCmd.MarkFlagRequired("api-server-endpoint")

	// load the environment variables
	flags.SetFlagsFromEnv(RootCmd.PersistentFlags(), "ROOK")
}

func SetupLogging() {
	// parse the given log level and set it at a global level
	logLevel, err := capnslog.ParseLevel(logLevelRaw)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	capnslog.SetGlobalLogLevel(logLevel)
}

func NewRookNetworkRestClient() client.RookRestClient {
	return NewRookNetworkRestClientWithTimeout(time.Duration(timeoutSecs * time.Second))
}

func NewRookNetworkRestClientWithTimeout(timeout time.Duration) client.RookRestClient {
	httpClient := http.DefaultClient
	httpClient.Timeout = timeout
	return client.NewRookNetworkRestClient(client.GetRestURL(APIServerEndpoint), httpClient)
}

func NewTableWriter(buffer io.Writer) *tabwriter.Writer {
	return tabwriter.NewWriter(buffer, outputMinWidth, outputTabWidth, outputPadding, outputPadChar, 0)
}
<commit_msg>pick up the rook connection environment vars<commit_after>/*
Copyright 2016 The Rook Authors. All rights reserved.

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 rook

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"text/tabwriter"
	"time"

	"github.com/coreos/pkg/capnslog"
	"github.com/rook/rook/pkg/model"
	"github.com/rook/rook/pkg/rook/client"
	"github.com/rook/rook/pkg/util/flags"
	"github.com/spf13/cobra"
)

var (
	APIServerEndpoint string
	logLevelRaw       string
)

const (
	outputPadding  = 3
	outputMinWidth = 10
	outputTabWidth = 0
	outputPadChar  = ' '
	timeoutSecs    = 10
)

var RootCmd = &cobra.Command{
	Use:   "rook",
	Short: "A command line client for working with a rook cluster",
	Long: `A command line client for working with a rook cluster.
https://github.com/rook/rook`,
}

func init() {
	defaultHost := os.Getenv("ROOK_REST_API_SERVICE_HOST")
	if defaultHost == "" {
		defaultHost = "127.0.0.1"
	}
	defaultPort := os.Getenv("ROOK_REST_API_SERVICE_PORT")
	if defaultPort == "" {
		defaultPort = strconv.Itoa(model.Port)
	}
	defaultEndpoint := fmt.Sprintf("%s:%s", defaultHost, defaultPort)

	RootCmd.PersistentFlags().StringVar(&APIServerEndpoint, "api-server-endpoint", defaultEndpoint, "IP endpoint of API server instance (required)")
	RootCmd.PersistentFlags().StringVar(&logLevelRaw, "log-level", "WARNING", "logging level for logging/tracing output (valid values: CRITICAL,ERROR,WARNING,NOTICE,INFO,DEBUG,TRACE)")

	RootCmd.MarkFlagRequired("api-server-endpoint")

	// load the environment variables
	flags.SetFlagsFromEnv(RootCmd.PersistentFlags(), "ROOK")
}

func SetupLogging() {
	// parse the given log level and set it at a global level
	logLevel, err := capnslog.ParseLevel(logLevelRaw)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	capnslog.SetGlobalLogLevel(logLevel)
}

func NewRookNetworkRestClient() client.RookRestClient {
	return NewRookNetworkRestClientWithTimeout(time.Duration(timeoutSecs * time.Second))
}

func NewRookNetworkRestClientWithTimeout(timeout time.Duration) client.RookRestClient {
	httpClient := http.DefaultClient
	httpClient.Timeout = timeout
	return client.NewRookNetworkRestClient(client.GetRestURL(APIServerEndpoint), httpClient)
}

func NewTableWriter(buffer io.Writer) *tabwriter.Writer {
	return tabwriter.NewWriter(buffer, outputMinWidth, outputTabWidth, outputPadding, outputPadChar, 0)
}
<|endoftext|>