<commit_before>package mgoauth

import (
	"code.google.com/p/go.crypto/bcrypt"
	"encoding/base64"
	"errors"
	"github.com/gorilla/securecookie"
	"github.com/kidstuff/auth/authmodel"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
	"strings"
	"time"
)

var (
	ErrNoResult = errors.New("mgoauth: no result")
)

type User struct {
	Id             bson.ObjectId `bson:"_id"`
	authmodel.User `bson:",inline"`
}

type MgoManager struct {
	MinimumOnlineThreshold time.Duration
	GroupColl              *mgo.Collection
	UserColl               *mgo.Collection
	LoginColl              *mgo.Collection
	Formater               authmodel.FormatChecker
	DefaultLimit           int
}

func NewMgoManager(db *mgo.Database) *MgoManager {
	mngr := &MgoManager{
		GroupColl:              db.C("mgoauth_group"),
		UserColl:               db.C("mgoauth_user"),
		LoginColl:              db.C("mgoauth_login"),
		MinimumOnlineThreshold: time.Minute * 5,
		DefaultLimit:           500,
	}

	mngr.Formater, _ = authmodel.NewSimpleChecker(9)

	return mngr
}

func hashPwd(pwd string) (authmodel.Password, error) {
	p := authmodel.Password{}
	p.InitAt = time.Now()
	p.Salt = securecookie.GenerateRandomKey(32)

	pwdBytes := []byte(pwd)
	tmp := make([]byte, len(pwdBytes)+len(p.Salt))
	copy(tmp, pwdBytes)
	tmp = append(tmp, p.Salt...)
	b, err := bcrypt.GenerateFromPassword(tmp, bcrypt.DefaultCost)
	p.Hashed = b

	return p, err
}

func (m *MgoManager) newUser(email, pwd string, app bool) (*User, error) {
	if !m.Formater.EmailValidate(email) {
		return nil, authmodel.ErrInvalidEmail
	}

	if !m.Formater.PasswordValidate(pwd) {
		return nil, authmodel.ErrInvalidPassword
	}

	u := &User{}
	u.Id = bson.NewObjectId()
	sid := u.Id.Hex()
	u.User.Id = &sid
	u.Email = &email

	p, err := hashPwd(pwd)
	if err != nil {
		return nil, err
	}

	u.Pwd = &p

	u.Approved = &app
	if !app {
		u.ConfirmCodes = map[string]string{
			"activate": strings.Trim(base64.URLEncoding.
				EncodeToString(securecookie.GenerateRandomKey(64)), "="),
		}
	}

	return u, nil
}

func (m *MgoManager) insertUser(u *User) error {
	now := time.Now()
	u.LastActivity = &now
	if u.Profile == nil {
		u.Profile = &authmodel.Profile{}
	}
	u.Profile.JoinDay = u.LastActivity

	err := m.UserColl.Insert(u)
	if err != nil {
		if mgo.IsDup(err) {
			return authmodel.ErrDuplicateEmail
		}
		return err
	}

	return nil
}

func (m *MgoManager) AddUser(email, pwd string, app bool) (*authmodel.User,
	error) {
	u, err := m.newUser(email, pwd, app)
	if err != nil {
		return nil, err
	}

	err = m.insertUser(u)
	if err != nil {
		return nil, err
	}

	return &u.User, nil
}

func (m *MgoManager) AddUserDetail(email, pwd string, app bool, pri []string,
	code map[string]string, profile *authmodel.Profile, groupIds []string) (*authmodel.User, error) {
	u, err := m.newUser(email, pwd, app)
	if err != nil {
		return nil, err
	}
	u.Privileges = pri
	u.ConfirmCodes = code
	u.Profile = profile
	if groupIds != nil {
		groups, err := m.FindSomeGroup(groupIds, []string{"Id", "Name"})
		if err == nil {
			u.Groups = groups
		}
	}

	err = m.insertUser(u)
	if err != nil {
		return nil, err
	}

	return &u.User, nil
}

func (m *MgoManager) UpdateUserDetail(id string, pwd *string, app *bool, pri []string,
	code map[string]string, profile *authmodel.Profile, groupIds []string) error {
	if !bson.IsObjectIdHex(id) {
		return authmodel.ErrInvalidId
	}
	oid := bson.ObjectIdHex(id)

	changes := make(bson.M)
	if pri != nil {
		changes["Privilege"] = pri
	}
	if code != nil {
		changes["ConfirmCodes"] = code
	}
	if groupIds != nil {
		groups, err := m.FindSomeGroup(groupIds, []string{"Id", "Name"})
		if err == nil {
			changes["Groups"] = groups
		}
	}
	if app != nil {
		changes["Approved"] = *app
	}
	if profile != nil {
		changes["Profile"] = profile
	}
	if pwd != nil {
		changes["Pwd"] = *pwd
	}

	return m.UserColl.UpdateId(oid, bson.M{"$set": changes})
}

func (m *MgoManager) DeleteUser(id string) error {
	oid, err := getId(id)
	if err != nil {
		return err
	}

	return m.UserColl.RemoveId(oid)
}

func (m *MgoManager) FindUser(id string) (*authmodel.User, error) {
	oid, err := getId(id)
	if err != nil {
		return nil, err
	}

	user := &authmodel.User{}
	err = m.UserColl.FindId(oid).One(user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) FindUserByEmail(email string) (*authmodel.User, error) {
	if !m.Formater.EmailValidate(email) {
		return nil, authmodel.ErrInvalidEmail
	}

	user := &authmodel.User{}
	err := m.UserColl.Find(bson.M{"Email": email}).One(user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) findAll(limit int, offsetId string, fields []string,
	filter bson.M) ([]*authmodel.User, error) {
	if limit == 0 {
		return nil, ErrNoResult
	}

	if limit > m.DefaultLimit || limit < 0 {
		limit = m.DefaultLimit
	}

	if filter == nil {
		filter = bson.M{}
	}

	if bson.IsObjectIdHex(offsetId) {
		oid, err := getId(offsetId)
		if err == nil {
			filter["_id"] = bson.M{"$gt": oid}
		}
	}

	query := m.UserColl.Find(filter)
	if len(fields) > 0 {
		selector := make(bson.M)
		for _, f := range fields {
			selector[f] = 1
		}
		query.Select(selector)
	}

	var users []*authmodel.User
	if limit > 0 {
		query.Limit(limit)
		users = make([]*authmodel.User, 0, limit)
	} else {
		users = []*authmodel.User{}
	}

	err := query.All(&users)
	if err != nil {
		return nil, err
	}

	return users, nil
}

func (m *MgoManager) FindAllUser(limit int, offsetId string, fields []string,
	groupIds []string) ([]*authmodel.User, error) {
	var filter bson.M
	if groupIds != nil {
		filter = bson.M{"Groups.Id": bson.M{"$in": groupIds}}
	}

	return m.findAll(limit, offsetId, fields, filter)
}

func (m *MgoManager) FindAllUserOnline(limit int, offsetId string, fields []string) (
	[]*authmodel.User, error) {
	return m.findAll(limit, offsetId, fields, bson.M{
		"LastActivity": bson.M{"$lt": time.Now().Add(m.MinimumOnlineThreshold)},
	})
}

func (m *MgoManager) updateLastActivity(id bson.ObjectId) (*authmodel.User, error) {
	user := &authmodel.User{}
	err := m.UserColl.FindId(id).One(user)
	if err != nil {
		return nil, err
	}

	now := time.Now()
	user.LastActivity = &now
	// ??? should we ignore the error return here?
	err = m.UserColl.UpdateId(id, bson.M{
		"$set": bson.M{"LastActivity": *user.LastActivity},
	})
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) GetUser(token string) (*authmodel.User, error) {
	state := LoginState{}
	err := m.LoginColl.FindId(token).One(&state)
	if err != nil {
		if err == mgo.ErrNotFound {
			return nil, authmodel.ErrNotLogged
		}
		return nil, err
	}

	if !state.ExpiredOn.After(time.Now()) {
		m.LoginColl.RemoveId(token)
	}

	return m.updateLastActivity(state.UserId)
}

func (m *MgoManager) Login(id string, stay time.Duration) (string, error) {
	if stay < m.MinimumOnlineThreshold {
		stay = m.MinimumOnlineThreshold
	}

	oid, err := getId(id)
	if err != nil {
		return "", err
	}

	state := LoginState{
		ExpiredOn: time.Now().Add(stay),
		UserId:    oid,
		Token: oid.Hex() + base64.URLEncoding.
			EncodeToString(securecookie.GenerateRandomKey(64)),
	}

	err = m.LoginColl.Insert(&state)
	if err != nil {
		return "", err
	}

	return state.Token, nil
}

func (m *MgoManager) Logout(token string) error {
	return m.LoginColl.RemoveId(token)
}

func (m *MgoManager) ComparePassword(ps string, pwd *authmodel.Password) error {
	pwdBytes := []byte(ps)
	tmp := make([]byte, len(pwdBytes)+len(pwd.Salt))
	copy(tmp, pwdBytes)
	tmp = append(tmp, pwd.Salt...)
	if err := bcrypt.CompareHashAndPassword(pwd.Hashed, tmp); err != nil {
		return err
	}

	return nil
}
<commit_msg>fix error when remove the only group of user<commit_after>package mgoauth

import (
	"code.google.com/p/go.crypto/bcrypt"
	"encoding/base64"
	"errors"
	"github.com/gorilla/securecookie"
	"github.com/kidstuff/auth/authmodel"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
	"strings"
	"time"
)

var (
	ErrNoResult = errors.New("mgoauth: no result")
)

type User struct {
	Id             bson.ObjectId `bson:"_id"`
	authmodel.User `bson:",inline"`
}

type MgoManager struct {
	MinimumOnlineThreshold time.Duration
	GroupColl              *mgo.Collection
	UserColl               *mgo.Collection
	LoginColl              *mgo.Collection
	Formater               authmodel.FormatChecker
	DefaultLimit           int
}

func NewMgoManager(db *mgo.Database) *MgoManager {
	mngr := &MgoManager{
		GroupColl:              db.C("mgoauth_group"),
		UserColl:               db.C("mgoauth_user"),
		LoginColl:              db.C("mgoauth_login"),
		MinimumOnlineThreshold: time.Minute * 5,
		DefaultLimit:           500,
	}

	mngr.Formater, _ = authmodel.NewSimpleChecker(9)

	return mngr
}

func hashPwd(pwd string) (authmodel.Password, error) {
	p := authmodel.Password{}
	p.InitAt = time.Now()
	p.Salt = securecookie.GenerateRandomKey(32)

	pwdBytes := []byte(pwd)
	tmp := make([]byte, len(pwdBytes)+len(p.Salt))
	copy(tmp, pwdBytes)
	tmp = append(tmp, p.Salt...)
	b, err := bcrypt.GenerateFromPassword(tmp, bcrypt.DefaultCost)
	p.Hashed = b

	return p, err
}

func (m *MgoManager) newUser(email, pwd string, app bool) (*User, error) {
	if !m.Formater.EmailValidate(email) {
		return nil, authmodel.ErrInvalidEmail
	}

	if !m.Formater.PasswordValidate(pwd) {
		return nil, authmodel.ErrInvalidPassword
	}

	u := &User{}
	u.Id = bson.NewObjectId()
	sid := u.Id.Hex()
	u.User.Id = &sid
	u.Email = &email

	p, err := hashPwd(pwd)
	if err != nil {
		return nil, err
	}

	u.Pwd = &p

	u.Approved = &app
	if !app {
		u.ConfirmCodes = map[string]string{
			"activate": strings.Trim(base64.URLEncoding.
				EncodeToString(securecookie.GenerateRandomKey(64)), "="),
		}
	}

	return u, nil
}

func (m *MgoManager) insertUser(u *User) error {
	now := time.Now()
	u.LastActivity = &now
	if u.Profile == nil {
		u.Profile = &authmodel.Profile{}
	}
	u.Profile.JoinDay = u.LastActivity

	err := m.UserColl.Insert(u)
	if err != nil {
		if mgo.IsDup(err) {
			return authmodel.ErrDuplicateEmail
		}
		return err
	}

	return nil
}

func (m *MgoManager) AddUser(email, pwd string, app bool) (*authmodel.User,
	error) {
	u, err := m.newUser(email, pwd, app)
	if err != nil {
		return nil, err
	}

	err = m.insertUser(u)
	if err != nil {
		return nil, err
	}

	return &u.User, nil
}

func (m *MgoManager) AddUserDetail(email, pwd string, app bool, pri []string,
	code map[string]string, profile *authmodel.Profile, groupIds []string) (*authmodel.User, error) {
	u, err := m.newUser(email, pwd, app)
	if err != nil {
		return nil, err
	}
	u.Privileges = pri
	u.ConfirmCodes = code
	u.Profile = profile
	if groupIds != nil {
		groups, err := m.FindSomeGroup(groupIds, []string{"Id", "Name"})
		if err == nil {
			u.Groups = groups
		}
	}

	err = m.insertUser(u)
	if err != nil {
		return nil, err
	}

	return &u.User, nil
}

func (m *MgoManager) UpdateUserDetail(id string, pwd *string, app *bool, pri []string,
	code map[string]string, profile *authmodel.Profile, groupIds []string) error {
	if !bson.IsObjectIdHex(id) {
		return authmodel.ErrInvalidId
	}
	oid := bson.ObjectIdHex(id)

	changes := make(bson.M)
	if pri != nil {
		changes["Privilege"] = pri
	}
	if code != nil {
		changes["ConfirmCodes"] = code
	}
	if groupIds != nil {
		if len(groupIds) == 0 {
			changes["Groups"] = []*authmodel.Group{}
		} else {
			groups, err := m.FindSomeGroup(groupIds, []string{"Id", "Name"})
			if err == nil {
				changes["Groups"] = groups
			}
		}
	}
	if app != nil {
		changes["Approved"] = *app
	}
	if profile != nil {
		changes["Profile"] = profile
	}
	if pwd != nil {
		changes["Pwd"] = *pwd
	}

	return m.UserColl.UpdateId(oid, bson.M{"$set": changes})
}

func (m *MgoManager) DeleteUser(id string) error {
	oid, err := getId(id)
	if err != nil {
		return err
	}

	return m.UserColl.RemoveId(oid)
}

func (m *MgoManager) FindUser(id string) (*authmodel.User, error) {
	oid, err := getId(id)
	if err != nil {
		return nil, err
	}

	user := &authmodel.User{}
	err = m.UserColl.FindId(oid).One(user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) FindUserByEmail(email string) (*authmodel.User, error) {
	if !m.Formater.EmailValidate(email) {
		return nil, authmodel.ErrInvalidEmail
	}

	user := &authmodel.User{}
	err := m.UserColl.Find(bson.M{"Email": email}).One(user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) findAll(limit int, offsetId string, fields []string,
	filter bson.M) ([]*authmodel.User, error) {
	if limit == 0 {
		return nil, ErrNoResult
	}

	if limit > m.DefaultLimit || limit < 0 {
		limit = m.DefaultLimit
	}

	if filter == nil {
		filter = bson.M{}
	}

	if bson.IsObjectIdHex(offsetId) {
		oid, err := getId(offsetId)
		if err == nil {
			filter["_id"] = bson.M{"$gt": oid}
		}
	}

	query := m.UserColl.Find(filter)
	if len(fields) > 0 {
		selector := make(bson.M)
		for _, f := range fields {
			selector[f] = 1
		}
		query.Select(selector)
	}

	var users []*authmodel.User
	if limit > 0 {
		query.Limit(limit)
		users = make([]*authmodel.User, 0, limit)
	} else {
		users = []*authmodel.User{}
	}

	err := query.All(&users)
	if err != nil {
		return nil, err
	}

	return users, nil
}

func (m *MgoManager) FindAllUser(limit int, offsetId string, fields []string,
	groupIds []string) ([]*authmodel.User, error) {
	var filter bson.M
	if groupIds != nil {
		filter = bson.M{"Groups.Id": bson.M{"$in": groupIds}}
	}

	return m.findAll(limit, offsetId, fields, filter)
}

func (m *MgoManager) FindAllUserOnline(limit int, offsetId string, fields []string) (
	[]*authmodel.User, error) {
	return m.findAll(limit, offsetId, fields, bson.M{
		"LastActivity": bson.M{"$lt": time.Now().Add(m.MinimumOnlineThreshold)},
	})
}

func (m *MgoManager) updateLastActivity(id bson.ObjectId) (*authmodel.User, error) {
	user := &authmodel.User{}
	err := m.UserColl.FindId(id).One(user)
	if err != nil {
		return nil, err
	}

	now := time.Now()
	user.LastActivity = &now
	// ??? should we ignore the error return here?
	err = m.UserColl.UpdateId(id, bson.M{
		"$set": bson.M{"LastActivity": *user.LastActivity},
	})
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (m *MgoManager) GetUser(token string) (*authmodel.User, error) {
	state := LoginState{}
	err := m.LoginColl.FindId(token).One(&state)
	if err != nil {
		if err == mgo.ErrNotFound {
			return nil, authmodel.ErrNotLogged
		}
		return nil, err
	}

	if !state.ExpiredOn.After(time.Now()) {
		m.LoginColl.RemoveId(token)
	}

	return m.updateLastActivity(state.UserId)
}

func (m *MgoManager) Login(id string, stay time.Duration) (string, error) {
	if stay < m.MinimumOnlineThreshold {
		stay = m.MinimumOnlineThreshold
	}

	oid, err := getId(id)
	if err != nil {
		return "", err
	}

	state := LoginState{
		ExpiredOn: time.Now().Add(stay),
		UserId:    oid,
		Token: oid.Hex() + base64.URLEncoding.
			EncodeToString(securecookie.GenerateRandomKey(64)),
	}

	err = m.LoginColl.Insert(&state)
	if err != nil {
		return "", err
	}

	return state.Token, nil
}

func (m *MgoManager) Logout(token string) error {
	return m.LoginColl.RemoveId(token)
}

func (m *MgoManager) ComparePassword(ps string, pwd *authmodel.Password) error {
	pwdBytes := []byte(ps)
	tmp := make([]byte, len(pwdBytes)+len(pwd.Salt))
	copy(tmp, pwdBytes)
	tmp = append(tmp, pwd.Salt...)
	if err := bcrypt.CompareHashAndPassword(pwd.Hashed, tmp); err != nil {
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>/*
Send events to apinalytics.io asynchronously in batches.
*/
package apinalytics_client

import (
	"encoding/json"
	"log"
	"net/http"
	"strings"
	"time"
)

const (
	// Server URL
	// url string = "http://127.0.0.1:7998/1/event/"
	// The size of the queue to the background goroutine
	channel_size int = 100
	// The background routine will send batches of events up to this size
	send_threshold int = 90
)

// Type for queuing events to the background
type AnalyticsEvent struct {
	Timestamp  int64             `json:"timestamp"`
	ConsumerId string            `json:"consumer_id"`
	Method     string            `json:"method"`
	Url        string            `json:"url"`
	Function   string            `json:"function",omitempty`
	ResponseUS int               `json:"response_us"`
	StatusCode int               `json:"status_code"`
	Data       map[string]string `json:"data",omitempty`
}

type Sender struct {
	applicationId string
	url           string               // The url to post events too, including project details
	events        []*AnalyticsEvent    // For batching events as we pull them off the channel
	count         int                  // Number of events batched and ready to send
	channel       chan *AnalyticsEvent // For queuing events to the background
	done          chan bool            // For clean exiting
}

/*
Create a new Sender.

This creates a background goroutine to aggregate and send your events.
*/
func NewSender(applicationId, url string) *Sender {
	sender := &Sender{
		applicationId: applicationId,
		channel:       make(chan *AnalyticsEvent, channel_size),
		done:          make(chan bool),
	}
	sender.url = url
	sender.reset()
	go sender.run()
	return sender
}

/*
Queue events to be sent to Keen.io

info can be anything that is JSON serializable.  Events are immediately queued to a background goroutine for sending.  The
background routine will send everything that's queued to it in a batch, then wait for new data.

The upshot is that if you send events slowly they will be sent immediately and individually, but if you send events quickly they will be batched
*/
func (sender *Sender) Queue(event *AnalyticsEvent) {
	sender.channel <- event
}

/*
Close the sender and wait for queued events to be sent
*/
func (sender *Sender) Close() {
	// Closing the channel signals the background thread to exit
	close(sender.channel)
	// Wait for the background thread to signal it has flushed all events and exited
	<-sender.done
}

// Add an event to the map that's used to batch events
func (sender *Sender) add(event *AnalyticsEvent) bool {
	if event == nil {
		// nil event, don't add
		return false
	}
	sender.events = append(sender.events, event)
	sender.count++

	if sender.count > send_threshold {
		sender.send()
	}
	return true
}

// Reset the event map that's used to batch events
func (sender *Sender) reset() {
	sender.events = make([]*AnalyticsEvent, 0, 10)
	sender.count = 0
}

// Send the events currently in sender.events
func (sender *Sender) send() {
	if sender.count == 0 {
		return
	}
	// Whether we can send the events or not, we dump them before exiting this function
	defer sender.reset()

	// Convert data to JSON
	data, err := json.Marshal(sender.events)
	if err != nil {
		log.Printf("Couldn't marshal json for analytics. %v\n", err)
		return
	}

	start := time.Now()
	req, err := http.NewRequest("POST", sender.url, strings.NewReader(string(data)))
	if err != nil {
		log.Printf("Failed to build analytics POST. %v", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Auth-User", sender.applicationId)
	rsp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Printf("Failed to post analytics events.  %v\n", err)
		return
	}
	defer rsp.Body.Close()

	if rsp.StatusCode != http.StatusOK {
		log.Printf("Failure return for analytics post.  %d, %s\n", rsp.StatusCode, rsp.Status)
	} else {
		// TODO: remove once analytics has bedded in
		log.Printf("analytics sent in %v\n", time.Since(start))
	}
}

func (sender *Sender) run() {
	var event *AnalyticsEvent

	// Block for the first event, once we have one event we try to drain everthing left
	for event = range sender.channel {
		sender.add(event)

		// Select with a default case is essentially a non-blocking read from the channel
	Loop:
		for {
			select {
			case event = <-sender.channel:
				// Add the event to those we are batching
				if !sender.add(event) {
					break Loop
				}

			default:
				// Nothing to batch at present.  Send our events if we have any, then go back to block until something
				// shows up
				break Loop
			}
		}
		// Send what we have batched
		sender.send()
	}

	// Indicate that this thread is over
	sender.done <- true
	log.Printf("Analytics exited\n")
}
<commit_msg>Additional comments<commit_after>/*
Send events to apinalytics.io asynchronously in batches.
*/
package apinalytics_client

import (
	"encoding/json"
	"log"
	"net/http"
	"strings"
	"time"
)

const (
	// Server URL
	// url string = "http://127.0.0.1:7998/1/event/"
	// The size of the queue to the background goroutine
	channel_size int = 100
	// The background routine will send batches of events up to this size
	send_threshold int = 90
)

// Type for queuing events to the background
type AnalyticsEvent struct {
	// Timestamp for this event in seconds since 1 Jan 1970 UTC
	Timestamp int64 `json:"timestamp"`
	// Identifier for the API consumer
	ConsumerId string `json:"consumer_id"`
	// HTTP Method used ("GET", "POST", etc.)
	Method string `json:"method"`
	// Url used (including parameters)
	Url string `json:"url"`
	// Name of the function invoked.
	Function string `json:"function",omitempty`
	// API response time in microseconds
	ResponseUS int `json:"response_us"`
	// HTTP status code
	StatusCode int `json:"status_code"`
	// Arbitrary key, value pairs to report.  Not yet implemented
	Data map[string]string `json:"data",omitempty`
}

type Sender struct {
	applicationId string
	url           string               // The url to post events too, including project details
	events        []*AnalyticsEvent    // For batching events as we pull them off the channel
	count         int                  // Number of events batched and ready to send
	channel       chan *AnalyticsEvent // For queuing events to the background
	done          chan bool            // For clean exiting
}

/*
Create a new Sender.

This creates a background goroutine to aggregate and send your events.

 applicationId - Identifies the application generating the events.
 url           - URL of the Apinalytics service
*/
func NewSender(applicationId, url string) *Sender {
	sender := &Sender{
		applicationId: applicationId,
		channel:       make(chan *AnalyticsEvent, channel_size),
		done:          make(chan bool),
	}
	sender.url = url
	sender.reset()
	go sender.run()
	return sender
}

/*
Queue events to be sent to Apinalytics

info can be anything that is JSON serializable.  Events are immediately queued to a background goroutine for sending.  The
background routine will send everything that's queued to it in a batch, then wait for new data.

The upshot is that if you send events slowly they will be sent immediately and individually, but if you send events quickly they will be batched
*/
func (sender *Sender) Queue(event *AnalyticsEvent) {
	sender.channel <- event
}

/*
Close the sender and wait for queued events to be sent
*/
func (sender *Sender) Close() {
	// Closing the channel signals the background thread to exit
	close(sender.channel)
	// Wait for the background thread to signal it has flushed all events and exited
	<-sender.done
}

// Add an event to the map that's used to batch events
func (sender *Sender) add(event *AnalyticsEvent) bool {
	if event == nil {
		// nil event, don't add
		return false
	}
	sender.events = append(sender.events, event)
	sender.count++

	if sender.count > send_threshold {
		sender.send()
	}
	return true
}

// Reset the event map that's used to batch events
func (sender *Sender) reset() {
	sender.events = make([]*AnalyticsEvent, 0, 10)
	sender.count = 0
}

// Send the events currently in sender.events
func (sender *Sender) send() {
	if sender.count == 0 {
		return
	}
	// Whether we can send the events or not, we dump them before exiting this function
	defer sender.reset()

	// Convert data to JSON
	data, err := json.Marshal(sender.events)
	if err != nil {
		log.Printf("Couldn't marshal json for analytics. %v\n", err)
		return
	}

	start := time.Now()
	req, err := http.NewRequest("POST", sender.url, strings.NewReader(string(data)))
	if err != nil {
		log.Printf("Failed to build analytics POST. %v", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Auth-User", sender.applicationId)
	rsp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Printf("Failed to post analytics events.  %v\n", err)
		return
	}
	defer rsp.Body.Close()

	if rsp.StatusCode != http.StatusOK {
		log.Printf("Failure return for analytics post.  %d, %s\n", rsp.StatusCode, rsp.Status)
	} else {
		// TODO: remove once analytics has bedded in
		log.Printf("analytics sent in %v\n", time.Since(start))
	}
}

func (sender *Sender) run() {
	var event *AnalyticsEvent

	// Block for the first event, once we have one event we try to drain everthing left
	for event = range sender.channel {
		sender.add(event)

		// Select with a default case is essentially a non-blocking read from the channel
	Loop:
		for {
			select {
			case event = <-sender.channel:
				// Add the event to those we are batching
				if !sender.add(event) {
					break Loop
				}

			default:
				// Nothing to batch at present.  Send our events if we have any, then go back to block until something
				// shows up
				break Loop
			}
		}
		// Send what we have batched
		sender.send()
	}

	// Indicate that this thread is over
	sender.done <- true
	log.Printf("Analytics exited\n")
}
<|endoftext|>


<commit_before>// +build !darwin
// +build !windows

package command

import (
	"github.com/variadico/noti/service"
	"github.com/variadico/noti/service/freedesktop"
)

func getBanner(title, message, _ string) service.Notification {
	return &freedesktop.Notification{
		Summary:       title,
		Body:          message,
		ExpireTimeout: 500,
		AppIcon:       "utilities-terminal",
	}
}
<commit_msg>Fix Linux banner<commit_after>// +build !darwin
// +build !windows

package command

import (
	"github.com/spf13/viper"
	"github.com/variadico/noti/service"
	"github.com/variadico/noti/service/freedesktop"
)

func setBannerDefaults(v *viper.Viper) {
	// No banner defaults.
}

func getBanner(title, message, _ string) service.Notification {
	return &freedesktop.Notification{
		Summary:       title,
		Body:          message,
		ExpireTimeout: 500,
		AppIcon:       "utilities-terminal",
	}
}
<|endoftext|>


<commit_before>//Package logging Functions to set unique configuration for use with the logrus logger
package logging

/*
 Copyright 2019 - 2020 Crunchy Data Solutions, 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.
*/

import (
	"fmt"
	"os"
	"regexp"
	"runtime"

	msgs "github.com/crunchydata/postgres-operator/pkg/apiservermsgs"
	log "github.com/sirupsen/logrus"
)

func SetParameters() LogValues {
	var logval LogValues

	logval.version = msgs.PGO_VERSION

	return logval
}

//LogValues holds the standard log value types
type LogValues struct {
	version string
}

// formatter adds default fields to each log entry.
type formatter struct {
	fields log.Fields
	lf     log.Formatter
}

// Format satisfies the logrus.Formatter interface.
func (f *formatter) Format(e *log.Entry) ([]byte, error) {
	for k, v := range f.fields {
		e.Data[k] = v
	}
	return f.lf.Format(e)
}

//CrunchyLogger adds the customized logging fields to the logrus instance context
func CrunchyLogger(logDetails LogValues) {
	//Sets calling method as a field
	log.SetReportCaller(true)

	crunchyTextFormatter := &log.TextFormatter{
		CallerPrettyfier: func(f *runtime.Frame) (string, string) {
			filename := f.File
			function := f.Function
			re1 := regexp.MustCompile(`postgres-operator/(.*go)`)
			result1 := re1.FindStringSubmatch(f.File)
			if len(result1) > 1 {
				filename = result1[1]
			}

			re2 := regexp.MustCompile(`postgres-operator/(.*)`)
			result2 := re2.FindStringSubmatch(f.Function)
			if len(result2) > 1 {
				function = result2[1]
			}
			return fmt.Sprintf("%s()", function), fmt.Sprintf("%s:%d", filename, f.Line)
		},
		FullTimestamp: true,
	}

	log.SetFormatter(&formatter{
		fields: log.Fields{
			"version": logDetails.version,
		},
		lf: crunchyTextFormatter,
	})

	// Output to stdout instead of the default stderr
	// Can be any io.Writer, see below for File example
	log.SetOutput(os.Stdout)

	// Only log the debug severity or above.
	log.SetLevel(log.DebugLevel)
}
<commit_msg>Logger no Longer Defaults to "debug" Log Level<commit_after>//Package logging Functions to set unique configuration for use with the logrus logger
package logging

/*
 Copyright 2019 - 2020 Crunchy Data Solutions, 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.
*/

import (
	"fmt"
	"os"
	"regexp"
	"runtime"

	msgs "github.com/crunchydata/postgres-operator/pkg/apiservermsgs"
	log "github.com/sirupsen/logrus"
)

func SetParameters() LogValues {
	var logval LogValues

	logval.version = msgs.PGO_VERSION

	return logval
}

//LogValues holds the standard log value types
type LogValues struct {
	version string
}

// formatter adds default fields to each log entry.
type formatter struct {
	fields log.Fields
	lf     log.Formatter
}

// Format satisfies the logrus.Formatter interface.
func (f *formatter) Format(e *log.Entry) ([]byte, error) {
	for k, v := range f.fields {
		e.Data[k] = v
	}
	return f.lf.Format(e)
}

//CrunchyLogger adds the customized logging fields to the logrus instance context
func CrunchyLogger(logDetails LogValues) {
	//Sets calling method as a field
	log.SetReportCaller(true)

	crunchyTextFormatter := &log.TextFormatter{
		CallerPrettyfier: func(f *runtime.Frame) (string, string) {
			filename := f.File
			function := f.Function
			re1 := regexp.MustCompile(`postgres-operator/(.*go)`)
			result1 := re1.FindStringSubmatch(f.File)
			if len(result1) > 1 {
				filename = result1[1]
			}

			re2 := regexp.MustCompile(`postgres-operator/(.*)`)
			result2 := re2.FindStringSubmatch(f.Function)
			if len(result2) > 1 {
				function = result2[1]
			}
			return fmt.Sprintf("%s()", function), fmt.Sprintf("%s:%d", filename, f.Line)
		},
		FullTimestamp: true,
	}

	log.SetFormatter(&formatter{
		fields: log.Fields{
			"version": logDetails.version,
		},
		lf: crunchyTextFormatter,
	})

	// Output to stdout instead of the default stderr
	// Can be any io.Writer, see below for File example
	log.SetOutput(os.Stdout)
}
<|endoftext|>


<commit_before>/*

	patchtree is a simple library that knows how to interpret a folder
	structure of jd diffs and apply them on top of a base.

		test/
		  base.json
		  after_move/
		    modification.patch
		  sanitization/
		    modification.patch
		    hidden/
		      modification.patch
		    nonempty/
		      modification.patch

	Given a path relative ot the current binary, it walks backwards up the
	folder, ensuring that a modification.patch exists in each directory until
	it finds a base.json. Then it applies forward all of the
	modification.patches to give you the final composed json blob result.

*/
package patchtree

import (
	"errors"
	jd "github.com/jkomoros/jd/lib"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
)

const BASE_JSON = "base.json"
const PATCH = "modification.patch"

//JSON returns the patched json blob impplied by that directory structure or
//an error if something doesn't work. See the package doc for more.
func JSON(path string) ([]byte, error) {

	if strings.HasSuffix(path, "/") {
		path = strings.TrimSuffix(path, "/")
	}

	result, err := processDirectory(path)

	if err != nil {
		return nil, err
	}

	return []byte(result.Json()), nil

}

//MustJSON is the same as JSON, but if it would have returned an error, panics istead.
func MustJSON(path string) []byte {
	result, err := JSON(path)
	if err != nil {
		panic(err)
	}
	return result
}

//ExpandTree expands all of the nodes in the patchtree, applying the chains of
//modification and created an node.expanded.json in each node. Used in a
//workflow to modify base.json: run this commeand, then modify base.json, then
//run ContractTree.
func ExpandTree(rootPath string) (affectedFiles int, err error) {

	baseJsonPath := filepath.Clean(rootPath + "/" + BASE_JSON)

	if _, err := os.Stat(baseJsonPath); os.IsNotExist(err) {
		return 0, errors.New("Base json file did not exist: " + err.Error())
	}

	node, err := jd.ReadJsonFile(baseJsonPath)

	if err != nil {
		return 0, errors.New("Couldn't parse base json file: " + err.Error())
	}

	files, err := ioutil.ReadDir(rootPath)

	if err != nil {
		return 0, errors.New("Couldn't read base directory: " + err.Error())
	}

	numAffectedFiles := 0

	for _, file := range files {
		if !file.IsDir() {
			continue
		}
		affectedFiles, err := expandTreeProcessDirectory(filepath.Clean(rootPath+"/"+file.Name()), node)
		if err != nil {
			return 0, errors.New("Couldn't process file: " + err.Error())
		}
		numAffectedFiles += affectedFiles
	}

	return numAffectedFiles, nil

}

func expandTreeProcessDirectory(directory string, node jd.JsonNode) (int, error) {

	//TODO: a lot of this code is duplicated in ExpandTree, which is basically
	//the same, just gets started in a different way.

	files, err := ioutil.ReadDir(directory)

	if err != nil {
		return 0, errors.New("Couldn't read directory: " + err.Error())
	}

	diffFileName := filepath.Clean(directory + "/" + PATCH)

	if _, err := os.Stat(diffFileName); os.IsNotExist(err) {
		//TODO: it's weird to print to log when this condition is hit.
		log.Println(diffFileName + " did not exist; skipping that directory and all beneath it.")
		return 0, nil
	}

	diff, err := jd.ReadDiffFile(diffFileName)

	if err != nil {
		return 0, errors.New(diffFileName + " could not be loaded as patch file: " + err.Error())
	}

	expandedNode, err := node.Patch(diff)

	if err != nil {
		return 0, errors.New(diffFileName + " could not be applied: " + err.Error())
	}

	//We just affected a file!
	numAffectedFiles := 1

	for _, file := range files {
		if !file.IsDir() {
			continue
		}
		subDirectory := filepath.Clean(directory + "/" + file.Name())

		subAffectedFiles, err := expandTreeProcessDirectory(subDirectory, expandedNode)
		if err != nil {
			return numAffectedFiles, err
		}
		numAffectedFiles += subAffectedFiles
	}

	return numAffectedFiles, nil
}

//ContractTree goes through each node in the parse tree and where it finds a
//node.expanded,json, re-derives and overwrites the "modification.patch". Used
//as part of a workflow to modify base.json: run ExpandTree, modify base.json,
//then ContractTree.
func ContractTree(rootPath string) (numAffectedFiles int, err error) {
	return 0, errors.New("Not yet implemented")
}

func processDirectory(path string) (jd.JsonNode, error) {

	//If no more path pieces error
	if path == "" || path == "/" || path == "./" {
		return nil, errors.New("Didn't find a base.json anywhere in the given directory structure")
	}

	//TODO: check if the directory exists...

	baseJsonPath := filepath.Clean(path + "/" + BASE_JSON)

	if _, err := os.Stat(baseJsonPath); err == nil {
		//Found the directory with base.json!
		node, err := jd.ReadJsonFile(baseJsonPath)
		if err != nil {
			return nil, errors.New(path + " had error reading base.json: " + err.Error())
		}
		return node, nil
	}

	modificationPatchPath := filepath.Clean(path + "/" + PATCH)

	if _, err := os.Stat(modificationPatchPath); err == nil {

		//Recurse, with the sub-directory.
		baseJson, err := processDirectory(filepath.Dir(path))

		if err != nil {
			return nil, err
		}

		diff, err := jd.ReadDiffFile(modificationPatchPath)

		if err != nil {
			return nil, errors.New("Error reading diff file at " + modificationPatchPath + ": " + err.Error())
		}

		composed, err := baseJson.Patch(diff)

		if err != nil {
			return nil, errors.New(path + " had error diffing " + err.Error())
		}

		return composed, nil
	}

	//Path had neither base.json or modification.patch, which is an error
	return nil, errors.New("In " + path + " didn't have either " + BASE_JSON + " or " + PATCH)

}
<commit_msg>Actually write the expanded node file in ExpandTree. Part of #607.<commit_after>/*

	patchtree is a simple library that knows how to interpret a folder
	structure of jd diffs and apply them on top of a base.

		test/
		  base.json
		  after_move/
		    modification.patch
		  sanitization/
		    modification.patch
		    hidden/
		      modification.patch
		    nonempty/
		      modification.patch

	Given a path relative ot the current binary, it walks backwards up the
	folder, ensuring that a modification.patch exists in each directory until
	it finds a base.json. Then it applies forward all of the
	modification.patches to give you the final composed json blob result.

*/
package patchtree

import (
	"errors"
	jd "github.com/jkomoros/jd/lib"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
)

const BASE_JSON = "base.json"
const PATCH = "modification.patch"
const EXPANDED_JSON_NAME = "node.expanded.json"

//JSON returns the patched json blob impplied by that directory structure or
//an error if something doesn't work. See the package doc for more.
func JSON(path string) ([]byte, error) {

	if strings.HasSuffix(path, "/") {
		path = strings.TrimSuffix(path, "/")
	}

	result, err := processDirectory(path)

	if err != nil {
		return nil, err
	}

	return []byte(result.Json()), nil

}

//MustJSON is the same as JSON, but if it would have returned an error, panics istead.
func MustJSON(path string) []byte {
	result, err := JSON(path)
	if err != nil {
		panic(err)
	}
	return result
}

//ExpandTree expands all of the nodes in the patchtree, applying the chains of
//modification and created an node.expanded.json in each node. Used in a
//workflow to modify base.json: run this commeand, then modify base.json, then
//run ContractTree.
func ExpandTree(rootPath string) (affectedFiles int, err error) {

	baseJsonPath := filepath.Clean(rootPath + "/" + BASE_JSON)

	if _, err := os.Stat(baseJsonPath); os.IsNotExist(err) {
		return 0, errors.New("Base json file did not exist: " + err.Error())
	}

	node, err := jd.ReadJsonFile(baseJsonPath)

	if err != nil {
		return 0, errors.New("Couldn't parse base json file: " + err.Error())
	}

	files, err := ioutil.ReadDir(rootPath)

	if err != nil {
		return 0, errors.New("Couldn't read base directory: " + err.Error())
	}

	numAffectedFiles := 0

	for _, file := range files {
		if !file.IsDir() {
			continue
		}
		affectedFiles, err := expandTreeProcessDirectory(filepath.Clean(rootPath+"/"+file.Name()), node)
		if err != nil {
			return 0, errors.New("Couldn't process file: " + err.Error())
		}
		numAffectedFiles += affectedFiles
	}

	return numAffectedFiles, nil

}

func expandTreeProcessDirectory(directory string, node jd.JsonNode) (int, error) {

	//TODO: a lot of this code is duplicated in ExpandTree, which is basically
	//the same, just gets started in a different way.

	files, err := ioutil.ReadDir(directory)

	if err != nil {
		return 0, errors.New("Couldn't read directory: " + err.Error())
	}

	diffFileName := filepath.Clean(directory + "/" + PATCH)

	if _, err := os.Stat(diffFileName); os.IsNotExist(err) {
		//TODO: it's weird to print to log when this condition is hit.
		log.Println(diffFileName + " did not exist; skipping that directory and all beneath it.")
		return 0, nil
	}

	diff, err := jd.ReadDiffFile(diffFileName)

	if err != nil {
		return 0, errors.New(diffFileName + " could not be loaded as patch file: " + err.Error())
	}

	expandedNode, err := node.Patch(diff)

	if err != nil {
		return 0, errors.New(diffFileName + " could not be applied: " + err.Error())
	}

	expandedNodeFileName := filepath.Clean(directory + "/" + EXPANDED_JSON_NAME)

	data := expandedNode.Json()

	if err := ioutil.WriteFile(expandedNodeFileName, []byte(data), 0644); err != nil {
		return 0, errors.New("Couldn't write " + expandedNodeFileName + ": " + err.Error())
	}

	//We just affected a file!
	numAffectedFiles := 1

	for _, file := range files {
		if !file.IsDir() {
			continue
		}
		subDirectory := filepath.Clean(directory + "/" + file.Name())

		subAffectedFiles, err := expandTreeProcessDirectory(subDirectory, expandedNode)
		if err != nil {
			return numAffectedFiles, err
		}
		numAffectedFiles += subAffectedFiles
	}

	return numAffectedFiles, nil
}

//ContractTree goes through each node in the parse tree and where it finds a
//node.expanded,json, re-derives and overwrites the "modification.patch". Used
//as part of a workflow to modify base.json: run ExpandTree, modify base.json,
//then ContractTree.
func ContractTree(rootPath string) (numAffectedFiles int, err error) {
	return 0, errors.New("Not yet implemented")
}

func processDirectory(path string) (jd.JsonNode, error) {

	//If no more path pieces error
	if path == "" || path == "/" || path == "./" {
		return nil, errors.New("Didn't find a base.json anywhere in the given directory structure")
	}

	//TODO: check if the directory exists...

	baseJsonPath := filepath.Clean(path + "/" + BASE_JSON)

	if _, err := os.Stat(baseJsonPath); err == nil {
		//Found the directory with base.json!
		node, err := jd.ReadJsonFile(baseJsonPath)
		if err != nil {
			return nil, errors.New(path + " had error reading base.json: " + err.Error())
		}
		return node, nil
	}

	modificationPatchPath := filepath.Clean(path + "/" + PATCH)

	if _, err := os.Stat(modificationPatchPath); err == nil {

		//Recurse, with the sub-directory.
		baseJson, err := processDirectory(filepath.Dir(path))

		if err != nil {
			return nil, err
		}

		diff, err := jd.ReadDiffFile(modificationPatchPath)

		if err != nil {
			return nil, errors.New("Error reading diff file at " + modificationPatchPath + ": " + err.Error())
		}

		composed, err := baseJson.Patch(diff)

		if err != nil {
			return nil, errors.New(path + " had error diffing " + err.Error())
		}

		return composed, nil
	}

	//Path had neither base.json or modification.patch, which is an error
	return nil, errors.New("In " + path + " didn't have either " + BASE_JSON + " or " + PATCH)

}
<|endoftext|>


<commit_before>package brew

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"text/template"

	"github.com/apex/log"
	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/goreleaser/goreleaser/internal/client"
	"github.com/goreleaser/goreleaser/internal/commitauthor"
	"github.com/goreleaser/goreleaser/internal/pipe"
	"github.com/goreleaser/goreleaser/internal/tmpl"
	"github.com/goreleaser/goreleaser/pkg/config"
	"github.com/goreleaser/goreleaser/pkg/context"
)

const brewConfigExtra = "BrewConfig"

var (
	// ErrNoArchivesFound happens when 0 archives are found.
	ErrNoArchivesFound = errors.New("no linux/macos archives found")

	// ErrMultipleArchivesSameOS happens when the config yields multiple archives
	// for linux or windows.
	ErrMultipleArchivesSameOS = errors.New("one tap can handle only archive of an OS/Arch combination. Consider using ids in the brew section")
)

// Pipe for brew deployment.
type Pipe struct{}

func (Pipe) String() string                 { return "homebrew tap formula" }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Brews) == 0 }

func (Pipe) Default(ctx *context.Context) error {
	for i := range ctx.Config.Brews {
		brew := &ctx.Config.Brews[i]

		brew.CommitAuthor = commitauthor.Default(brew.CommitAuthor)

		if brew.CommitMessageTemplate == "" {
			brew.CommitMessageTemplate = "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
		}
		if brew.Name == "" {
			brew.Name = ctx.Config.ProjectName
		}
		if brew.Goarm == "" {
			brew.Goarm = "6"
		}
		if brew.Goamd64 == "" {
			brew.Goamd64 = "v1"
		}
	}

	return nil
}

func (Pipe) Run(ctx *context.Context) error {
	cli, err := client.New(ctx)
	if err != nil {
		return err
	}

	return runAll(ctx, cli)
}

// Publish brew formula.
func (Pipe) Publish(ctx *context.Context) error {
	cli, err := client.New(ctx)
	if err != nil {
		return err
	}
	return publishAll(ctx, cli)
}

func runAll(ctx *context.Context, cli client.Client) error {
	for _, brew := range ctx.Config.Brews {
		err := doRun(ctx, brew, cli)
		if err != nil {
			return err
		}
	}
	return nil
}

func publishAll(ctx *context.Context, cli client.Client) error {
	// even if one of them skips, we run them all, and then show return the skips all at once.
	// this is needed so we actually create the `dist/foo.rb` file, which is useful for debugging.
	skips := pipe.SkipMemento{}
	for _, formula := range ctx.Artifacts.Filter(artifact.ByType(artifact.BrewTap)).List() {
		err := doPublish(ctx, formula, cli)
		if err != nil && pipe.IsSkip(err) {
			skips.Remember(err)
			continue
		}
		if err != nil {
			return err
		}
	}
	return skips.Evaluate()
}

func doPublish(ctx *context.Context, formula *artifact.Artifact, cl client.Client) error {
	brew := formula.Extra[brewConfigExtra].(config.Homebrew)
	var err error
	cl, err = client.NewIfToken(ctx, cl, brew.Tap.Token)
	if err != nil {
		return err
	}

	if strings.TrimSpace(brew.SkipUpload) == "true" {
		return pipe.Skip("brew.skip_upload is set")
	}

	if strings.TrimSpace(brew.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish")
	}

	repo := client.RepoFromRef(brew.Tap)

	gpath := buildFormulaPath(brew.Folder, formula.Name)
	log.WithField("formula", gpath).
		WithField("repo", repo.String()).
		Info("pushing")

	msg, err := tmpl.New(ctx).Apply(brew.CommitMessageTemplate)
	if err != nil {
		return err
	}

	author, err := commitauthor.Get(ctx, brew.CommitAuthor)
	if err != nil {
		return err
	}

	content, err := os.ReadFile(formula.Path)
	if err != nil {
		return err
	}

	return cl.CreateFile(ctx, author, repo, content, gpath, msg)
}

func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
	if brew.Tap.Name == "" {
		return pipe.Skip("brew tap name is not set")
	}

	filters := []artifact.Filter{
		artifact.Or(
			artifact.ByGoos("darwin"),
			artifact.ByGoos("linux"),
		),
		artifact.Or(
			artifact.And(
				artifact.ByGoarch("amd64"),
				artifact.ByGoamd64(brew.Goamd64),
			),
			artifact.ByGoarch("arm64"),
			artifact.ByGoarch("all"),
			artifact.And(
				artifact.ByGoarch("arm"),
				artifact.ByGoarm(brew.Goarm),
			),
		),
		artifact.Or(
			artifact.And(
				artifact.ByFormats("zip", "tar.gz"),
				artifact.ByType(artifact.UploadableArchive),
			),
			artifact.ByType(artifact.UploadableBinary),
		),
		artifact.OnlyReplacingUnibins,
	}
	if len(brew.IDs) > 0 {
		filters = append(filters, artifact.ByIDs(brew.IDs...))
	}

	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
	if len(archives) == 0 {
		return ErrNoArchivesFound
	}

	name, err := tmpl.New(ctx).Apply(brew.Name)
	if err != nil {
		return err
	}
	brew.Name = name

	tapOwner, err := tmpl.New(ctx).Apply(brew.Tap.Owner)
	if err != nil {
		return err
	}
	brew.Tap.Owner = tapOwner

	tapName, err := tmpl.New(ctx).Apply(brew.Tap.Name)
	if err != nil {
		return err
	}
	brew.Tap.Name = tapName

	skipUpload, err := tmpl.New(ctx).Apply(brew.SkipUpload)
	if err != nil {
		return err
	}
	brew.SkipUpload = skipUpload

	content, err := buildFormula(ctx, brew, cl, archives)
	if err != nil {
		return err
	}

	filename := brew.Name + ".rb"
	path := filepath.Join(ctx.Config.Dist, filename)
	log.WithField("formula", path).Info("writing")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil { //nolint: gosec
		return fmt.Errorf("failed to write brew formula: %w", err)
	}

	ctx.Artifacts.Add(&artifact.Artifact{
		Name: filename,
		Path: path,
		Type: artifact.BrewTap,
		Extra: map[string]interface{}{
			brewConfigExtra: brew,
		},
	})

	return nil
}

func buildFormulaPath(folder, filename string) string {
	return path.Join(folder, filename)
}

func buildFormula(ctx *context.Context, brew config.Homebrew, client client.Client, artifacts []*artifact.Artifact) (string, error) {
	data, err := dataFor(ctx, brew, client, artifacts)
	if err != nil {
		return "", err
	}
	return doBuildFormula(ctx, data)
}

func doBuildFormula(ctx *context.Context, data templateData) (string, error) {
	t, err := template.
		New(data.Name).
		Funcs(template.FuncMap{
			"join": strings.Join,
		}).
		Parse(formulaTemplate)
	if err != nil {
		return "", err
	}
	var out bytes.Buffer
	if err := t.Execute(&out, data); err != nil {
		return "", err
	}

	content, err := tmpl.New(ctx).Apply(out.String())
	if err != nil {
		return "", err
	}
	out.Reset()

	// Sanitize the template output and get rid of trailing whitespace.
	var (
		r = strings.NewReader(content)
		s = bufio.NewScanner(r)
	)
	for s.Scan() {
		l := strings.TrimRight(s.Text(), " ")
		_, _ = out.WriteString(l)
		_ = out.WriteByte('\n')
	}
	if err := s.Err(); err != nil {
		return "", err
	}

	return out.String(), nil
}

func installs(cfg config.Homebrew, art *artifact.Artifact) []string {
	if cfg.Install != "" {
		return split(cfg.Install)
	}

	install := map[string]bool{}
	switch art.Type {
	case artifact.UploadableBinary:
		name := art.Name
		bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
		install[fmt.Sprintf("bin.install %q => %q", name, bin)] = true
	case artifact.UploadableArchive:
		for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
			install[fmt.Sprintf("bin.install %q", bin)] = true
		}
	}

	result := keys(install)
	sort.Strings(result)
	log.Warnf("guessing install to be %q", strings.Join(result, ", "))
	return result
}

func keys(m map[string]bool) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

func dataFor(ctx *context.Context, cfg config.Homebrew, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
	result := templateData{
		Name:          formulaNameFor(cfg.Name),
		Desc:          cfg.Description,
		Homepage:      cfg.Homepage,
		Version:       ctx.Version,
		License:       cfg.License,
		Caveats:       split(cfg.Caveats),
		Dependencies:  cfg.Dependencies,
		Conflicts:     cfg.Conflicts,
		Plist:         cfg.Plist,
		Service:       split(cfg.Service),
		PostInstall:   split(cfg.PostInstall),
		Tests:         split(cfg.Test),
		CustomRequire: cfg.CustomRequire,
		CustomBlock:   split(cfg.CustomBlock),
	}

	counts := map[string]int{}
	for _, art := range artifacts {
		sum, err := art.Checksum("sha256")
		if err != nil {
			return result, err
		}

		if cfg.URLTemplate == "" {
			url, err := cl.ReleaseURLTemplate(ctx)
			if err != nil {
				return result, err
			}
			cfg.URLTemplate = url
		}

		url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate)
		if err != nil {
			return result, err
		}

		pkg := releasePackage{
			DownloadURL:      url,
			SHA256:           sum,
			OS:               art.Goos,
			Arch:             art.Goarch,
			DownloadStrategy: cfg.DownloadStrategy,
			Install:          installs(cfg, art),
		}

		counts[pkg.OS+pkg.Arch]++

		switch pkg.OS {
		case "darwin":
			result.MacOSPackages = append(result.MacOSPackages, pkg)
		case "linux":
			result.LinuxPackages = append(result.LinuxPackages, pkg)
		}
	}

	for _, v := range counts {
		if v > 1 {
			return result, ErrMultipleArchivesSameOS
		}
	}

	if len(result.MacOSPackages) == 1 && result.MacOSPackages[0].Arch == "amd64" {
		result.HasOnlyAmd64MacOsPkg = true
	}

	sort.Slice(result.LinuxPackages, lessFnFor(result.LinuxPackages))
	sort.Slice(result.MacOSPackages, lessFnFor(result.MacOSPackages))
	return result, nil
}

func lessFnFor(list []releasePackage) func(i, j int) bool {
	return func(i, j int) bool { return list[i].OS > list[j].OS && list[i].Arch > list[j].Arch }
}

func split(s string) []string {
	strings := strings.Split(strings.TrimSpace(s), "\n")
	if len(strings) == 1 && strings[0] == "" {
		return []string{}
	}
	return strings
}

// formulaNameFor transforms the formula name into a form
// that more resembles a valid Ruby class name
// e.g. foo_bar@v6.0.0-rc is turned into FooBarATv6_0_0RC
// The order of these replacements is important
func formulaNameFor(name string) string {
	name = strings.ReplaceAll(name, "-", " ")
	name = strings.ReplaceAll(name, "_", " ")
	name = strings.ReplaceAll(name, ".", "_")
	name = strings.ReplaceAll(name, "@", "AT")
	return strings.ReplaceAll(strings.Title(name), " ", "")
}
<commit_msg>chore: ignore deprecated strings.Title warn<commit_after>package brew

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"text/template"

	"github.com/apex/log"
	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/goreleaser/goreleaser/internal/client"
	"github.com/goreleaser/goreleaser/internal/commitauthor"
	"github.com/goreleaser/goreleaser/internal/pipe"
	"github.com/goreleaser/goreleaser/internal/tmpl"
	"github.com/goreleaser/goreleaser/pkg/config"
	"github.com/goreleaser/goreleaser/pkg/context"
)

const brewConfigExtra = "BrewConfig"

var (
	// ErrNoArchivesFound happens when 0 archives are found.
	ErrNoArchivesFound = errors.New("no linux/macos archives found")

	// ErrMultipleArchivesSameOS happens when the config yields multiple archives
	// for linux or windows.
	ErrMultipleArchivesSameOS = errors.New("one tap can handle only archive of an OS/Arch combination. Consider using ids in the brew section")
)

// Pipe for brew deployment.
type Pipe struct{}

func (Pipe) String() string                 { return "homebrew tap formula" }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Brews) == 0 }

func (Pipe) Default(ctx *context.Context) error {
	for i := range ctx.Config.Brews {
		brew := &ctx.Config.Brews[i]

		brew.CommitAuthor = commitauthor.Default(brew.CommitAuthor)

		if brew.CommitMessageTemplate == "" {
			brew.CommitMessageTemplate = "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
		}
		if brew.Name == "" {
			brew.Name = ctx.Config.ProjectName
		}
		if brew.Goarm == "" {
			brew.Goarm = "6"
		}
		if brew.Goamd64 == "" {
			brew.Goamd64 = "v1"
		}
	}

	return nil
}

func (Pipe) Run(ctx *context.Context) error {
	cli, err := client.New(ctx)
	if err != nil {
		return err
	}

	return runAll(ctx, cli)
}

// Publish brew formula.
func (Pipe) Publish(ctx *context.Context) error {
	cli, err := client.New(ctx)
	if err != nil {
		return err
	}
	return publishAll(ctx, cli)
}

func runAll(ctx *context.Context, cli client.Client) error {
	for _, brew := range ctx.Config.Brews {
		err := doRun(ctx, brew, cli)
		if err != nil {
			return err
		}
	}
	return nil
}

func publishAll(ctx *context.Context, cli client.Client) error {
	// even if one of them skips, we run them all, and then show return the skips all at once.
	// this is needed so we actually create the `dist/foo.rb` file, which is useful for debugging.
	skips := pipe.SkipMemento{}
	for _, formula := range ctx.Artifacts.Filter(artifact.ByType(artifact.BrewTap)).List() {
		err := doPublish(ctx, formula, cli)
		if err != nil && pipe.IsSkip(err) {
			skips.Remember(err)
			continue
		}
		if err != nil {
			return err
		}
	}
	return skips.Evaluate()
}

func doPublish(ctx *context.Context, formula *artifact.Artifact, cl client.Client) error {
	brew := formula.Extra[brewConfigExtra].(config.Homebrew)
	var err error
	cl, err = client.NewIfToken(ctx, cl, brew.Tap.Token)
	if err != nil {
		return err
	}

	if strings.TrimSpace(brew.SkipUpload) == "true" {
		return pipe.Skip("brew.skip_upload is set")
	}

	if strings.TrimSpace(brew.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" {
		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish")
	}

	repo := client.RepoFromRef(brew.Tap)

	gpath := buildFormulaPath(brew.Folder, formula.Name)
	log.WithField("formula", gpath).
		WithField("repo", repo.String()).
		Info("pushing")

	msg, err := tmpl.New(ctx).Apply(brew.CommitMessageTemplate)
	if err != nil {
		return err
	}

	author, err := commitauthor.Get(ctx, brew.CommitAuthor)
	if err != nil {
		return err
	}

	content, err := os.ReadFile(formula.Path)
	if err != nil {
		return err
	}

	return cl.CreateFile(ctx, author, repo, content, gpath, msg)
}

func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
	if brew.Tap.Name == "" {
		return pipe.Skip("brew tap name is not set")
	}

	filters := []artifact.Filter{
		artifact.Or(
			artifact.ByGoos("darwin"),
			artifact.ByGoos("linux"),
		),
		artifact.Or(
			artifact.And(
				artifact.ByGoarch("amd64"),
				artifact.ByGoamd64(brew.Goamd64),
			),
			artifact.ByGoarch("arm64"),
			artifact.ByGoarch("all"),
			artifact.And(
				artifact.ByGoarch("arm"),
				artifact.ByGoarm(brew.Goarm),
			),
		),
		artifact.Or(
			artifact.And(
				artifact.ByFormats("zip", "tar.gz"),
				artifact.ByType(artifact.UploadableArchive),
			),
			artifact.ByType(artifact.UploadableBinary),
		),
		artifact.OnlyReplacingUnibins,
	}
	if len(brew.IDs) > 0 {
		filters = append(filters, artifact.ByIDs(brew.IDs...))
	}

	archives := ctx.Artifacts.Filter(artifact.And(filters...)).List()
	if len(archives) == 0 {
		return ErrNoArchivesFound
	}

	name, err := tmpl.New(ctx).Apply(brew.Name)
	if err != nil {
		return err
	}
	brew.Name = name

	tapOwner, err := tmpl.New(ctx).Apply(brew.Tap.Owner)
	if err != nil {
		return err
	}
	brew.Tap.Owner = tapOwner

	tapName, err := tmpl.New(ctx).Apply(brew.Tap.Name)
	if err != nil {
		return err
	}
	brew.Tap.Name = tapName

	skipUpload, err := tmpl.New(ctx).Apply(brew.SkipUpload)
	if err != nil {
		return err
	}
	brew.SkipUpload = skipUpload

	content, err := buildFormula(ctx, brew, cl, archives)
	if err != nil {
		return err
	}

	filename := brew.Name + ".rb"
	path := filepath.Join(ctx.Config.Dist, filename)
	log.WithField("formula", path).Info("writing")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil { //nolint: gosec
		return fmt.Errorf("failed to write brew formula: %w", err)
	}

	ctx.Artifacts.Add(&artifact.Artifact{
		Name: filename,
		Path: path,
		Type: artifact.BrewTap,
		Extra: map[string]interface{}{
			brewConfigExtra: brew,
		},
	})

	return nil
}

func buildFormulaPath(folder, filename string) string {
	return path.Join(folder, filename)
}

func buildFormula(ctx *context.Context, brew config.Homebrew, client client.Client, artifacts []*artifact.Artifact) (string, error) {
	data, err := dataFor(ctx, brew, client, artifacts)
	if err != nil {
		return "", err
	}
	return doBuildFormula(ctx, data)
}

func doBuildFormula(ctx *context.Context, data templateData) (string, error) {
	t, err := template.
		New(data.Name).
		Funcs(template.FuncMap{
			"join": strings.Join,
		}).
		Parse(formulaTemplate)
	if err != nil {
		return "", err
	}
	var out bytes.Buffer
	if err := t.Execute(&out, data); err != nil {
		return "", err
	}

	content, err := tmpl.New(ctx).Apply(out.String())
	if err != nil {
		return "", err
	}
	out.Reset()

	// Sanitize the template output and get rid of trailing whitespace.
	var (
		r = strings.NewReader(content)
		s = bufio.NewScanner(r)
	)
	for s.Scan() {
		l := strings.TrimRight(s.Text(), " ")
		_, _ = out.WriteString(l)
		_ = out.WriteByte('\n')
	}
	if err := s.Err(); err != nil {
		return "", err
	}

	return out.String(), nil
}

func installs(cfg config.Homebrew, art *artifact.Artifact) []string {
	if cfg.Install != "" {
		return split(cfg.Install)
	}

	install := map[string]bool{}
	switch art.Type {
	case artifact.UploadableBinary:
		name := art.Name
		bin := art.ExtraOr(artifact.ExtraBinary, art.Name).(string)
		install[fmt.Sprintf("bin.install %q => %q", name, bin)] = true
	case artifact.UploadableArchive:
		for _, bin := range art.ExtraOr(artifact.ExtraBinaries, []string{}).([]string) {
			install[fmt.Sprintf("bin.install %q", bin)] = true
		}
	}

	result := keys(install)
	sort.Strings(result)
	log.Warnf("guessing install to be %q", strings.Join(result, ", "))
	return result
}

func keys(m map[string]bool) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

func dataFor(ctx *context.Context, cfg config.Homebrew, cl client.Client, artifacts []*artifact.Artifact) (templateData, error) {
	result := templateData{
		Name:          formulaNameFor(cfg.Name),
		Desc:          cfg.Description,
		Homepage:      cfg.Homepage,
		Version:       ctx.Version,
		License:       cfg.License,
		Caveats:       split(cfg.Caveats),
		Dependencies:  cfg.Dependencies,
		Conflicts:     cfg.Conflicts,
		Plist:         cfg.Plist,
		Service:       split(cfg.Service),
		PostInstall:   split(cfg.PostInstall),
		Tests:         split(cfg.Test),
		CustomRequire: cfg.CustomRequire,
		CustomBlock:   split(cfg.CustomBlock),
	}

	counts := map[string]int{}
	for _, art := range artifacts {
		sum, err := art.Checksum("sha256")
		if err != nil {
			return result, err
		}

		if cfg.URLTemplate == "" {
			url, err := cl.ReleaseURLTemplate(ctx)
			if err != nil {
				return result, err
			}
			cfg.URLTemplate = url
		}

		url, err := tmpl.New(ctx).WithArtifact(art, map[string]string{}).Apply(cfg.URLTemplate)
		if err != nil {
			return result, err
		}

		pkg := releasePackage{
			DownloadURL:      url,
			SHA256:           sum,
			OS:               art.Goos,
			Arch:             art.Goarch,
			DownloadStrategy: cfg.DownloadStrategy,
			Install:          installs(cfg, art),
		}

		counts[pkg.OS+pkg.Arch]++

		switch pkg.OS {
		case "darwin":
			result.MacOSPackages = append(result.MacOSPackages, pkg)
		case "linux":
			result.LinuxPackages = append(result.LinuxPackages, pkg)
		}
	}

	for _, v := range counts {
		if v > 1 {
			return result, ErrMultipleArchivesSameOS
		}
	}

	if len(result.MacOSPackages) == 1 && result.MacOSPackages[0].Arch == "amd64" {
		result.HasOnlyAmd64MacOsPkg = true
	}

	sort.Slice(result.LinuxPackages, lessFnFor(result.LinuxPackages))
	sort.Slice(result.MacOSPackages, lessFnFor(result.MacOSPackages))
	return result, nil
}

func lessFnFor(list []releasePackage) func(i, j int) bool {
	return func(i, j int) bool { return list[i].OS > list[j].OS && list[i].Arch > list[j].Arch }
}

func split(s string) []string {
	strings := strings.Split(strings.TrimSpace(s), "\n")
	if len(strings) == 1 && strings[0] == "" {
		return []string{}
	}
	return strings
}

// formulaNameFor transforms the formula name into a form
// that more resembles a valid Ruby class name
// e.g. foo_bar@v6.0.0-rc is turned into FooBarATv6_0_0RC
// The order of these replacements is important
func formulaNameFor(name string) string {
	name = strings.ReplaceAll(name, "-", " ")
	name = strings.ReplaceAll(name, "_", " ")
	name = strings.ReplaceAll(name, ".", "_")
	name = strings.ReplaceAll(name, "@", "AT")
	return strings.ReplaceAll(strings.Title(name), " ", "") // nolint:staticcheck
}
<|endoftext|>


<commit_before>// Copyright 2014 The Serviced 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 servicedefinition

import (
	"github.com/control-center/serviced/domain"
	"github.com/control-center/serviced/utils"
	"github.com/zenoss/glog"

	"encoding/json"
	"errors"
	"strings"
	"time"
)

//ServiceDefinition is the definition of a service hierarchy.
type ServiceDefinition struct {
	Name              string                 // Name of the defined service
	Title             string                 // Title is a label used when describing this service in the context of a service tree
	Version           string                 // Version of the defined service
	Command           string                 // Command which runs the service
	Description       string                 // Description of the service
	Tags              []string               // Searchable service tags
	ImageID           string                 // Docker image hosting the service
	Instances         domain.MinMax          // Constraints on the number of instances
	ChangeOptions     []string               // Control options for what happens when a running service is changed
	Launch            string                 // Must be "AUTO", the default, or "MANUAL"
	HostPolicy        HostPolicy             // Policy for starting up instances
	Hostname          string                 // Optional hostname which should be set on run
	Privileged        bool                   // Whether to run the container with extended privileges
	ConfigFiles       map[string]ConfigFile  // Config file templates
	Context           map[string]interface{} // Context information for the service
	Endpoints         []EndpointDefinition   // Comms endpoints used by the service
	Services          []ServiceDefinition    // Supporting subservices
	Tasks             []Task                 // Scheduled tasks for celery to find
	LogFilters        map[string]string      // map of log filter name to log filter definitions
	Volumes           []Volume               // list of volumes to bind into containers
	LogConfigs        []LogConfig
	Snapshot          SnapshotCommands              // Snapshot quiesce info for the service: Pause/Resume bash commands
	RAMCommitment     utils.EngNotation             // expected RAM commitment to use for scheduling
	CPUCommitment     uint64                        // expected CPU commitment (#cores) to use for scheduling
	DisableShell      bool                          // disables shell commands on the service
	Runs              map[string]string             // FIXME: This field is deprecated. Remove when possible.
	Commands          map[string]domain.Command     // Map of commands that can be executed with 'serviced run ...'
	Actions           map[string]string             // Map of commands that can be executed with 'serviced action ...'
	HealthChecks      map[string]domain.HealthCheck // HealthChecks for a service.
	Prereqs           []domain.Prereq               // Optional list of scripts that must be successfully run before kicking off the service command.
	MonitoringProfile domain.MonitorProfile         // An optional list of queryable metrics, graphs, and thresholds
	MemoryLimit       float64
	CPUShares         int64
	PIDFile           string // An optional path or command to generate a path for a PID file to which signals are relayed.
}

// SnapshotCommands commands to be called during and after a snapshot
type SnapshotCommands struct {
	Pause  string // bash command to pause the volume  (quiesce)
	Resume string // bash command to resume the volume (unquiesce)
}

// EndpointDefinition An endpoint that a Service exposes.
type EndpointDefinition struct {
	Name                string // Human readable name of the endpoint. Unique per service definition
	Purpose             string
	Protocol            string
	PortNumber          uint16
	PortTemplate        string // A template which, if specified, is used to calculate the port number
	VirtualAddress      string // An address by which an imported endpoint may be accessed within the container, e.g. "mysqlhost:1234"
	Application         string
	ApplicationTemplate string
	AddressConfig       AddressResourceConfig
	VHosts              []string // VHost is used to request named vhost for this endpoint. Should be the name of a
	// subdomain, i.e "myapplication"  not "myapplication.host.com"
	VHostList []VHost // VHost is used to request named vhost(s) for this endpoint.
	PortList  []Port
}

// VHost is the configuration for an application endpoint that wants an http VHost endpoint provided by Control Center
type VHost struct {
	Name    string // name of the vhost subdomain subdomain, i.e "myapplication"  not "myapplication.host.com
	Enabled bool   // whether the vhost should be enabled or disabled.
}

// Port is the configuration for an application endpoint port.
type Port struct {
	PortAddr   string // which port number to use for this endpoint
	Enabled    bool   // whether the port should be enabled or disabled.
}

// Task A scheduled task
type Task struct {
	Name          string
	Schedule      string
	Command       string
	LastRunAt     time.Time
	TotalRunCount int
}

// Volume import defines a file system directory underneath an export directory
type Volume struct {
	Owner             string //Resource Path Owner
	Permission        string //Resource Path permissions, eg what you pass to chmod
	ResourcePath      string //Resource Pool Path, shared across all hosts in a resource pool
	ContainerPath     string //Container bind-mount path
	Type              string //Path use, i.e. "dfs" or "tmp"
	InitContainerPath string //Path to initialize the volume from at creation time, optional
}

// ConfigFile config file for a service
type ConfigFile struct {
	Filename    string // complete path of file
	Owner       string // owner of file within the container, root:root or 0:0 for root owned file, what you would pass to chown
	Permissions string // permission of file, eg 0664, what you would pass to chmod
	Content     string // content of config file
}

//AddressResourceConfig defines an external facing port for a service definition
type AddressResourceConfig struct {
	Port     uint16
	Protocol string
}

// LogConfig represents the configuration for a logfile for a service.
type LogConfig struct {
	Path    string   // The location on the container's filesystem of the log, can be a directory
	Type    string   // Arbitrary string that identifies the "types" of logs that come from this source. This will be
	Filters []string // A list of filters that must be contained in either the LogFilters or a parent's LogFilter,
	LogTags []LogTag // Key value pair of tags that are sent to logstash for all entries coming out of this logfile
}

// LogTag  no clue what this is. Maybe someone actually reads this
type LogTag struct {
	Name  string
	Value string
}

// HostPolicy represents the optional policy used to determine which hosts on
// which to run instances of a service. Default is to run on the available
// host with the most uncommitted RAM.
type HostPolicy string

const (
	// DEFAULT policy for scheduling a service instance
	DEFAULT HostPolicy = ""
	// LeastCommitted run on host w/ least committed resources
	LeastCommitted = "LEAST_COMMITTED"
	// Balance is a synonym for LeastCommitted
	Balance = "balance"
	// Pack runs instance on eligible host with most committed resources
	Pack = "pack"
	// PreferSeparate attempt to schedule instances of a service on separate hosts
	PreferSeparate = "PREFER_SEPARATE"
	// RequireSeparate schedule instances of a service on separate hosts
	RequireSeparate = "REQUIRE_SEPARATE"
)

// UnmarshalText implements the encoding/TextUnmarshaler interface
func (p *HostPolicy) UnmarshalText(b []byte) error {
	s := strings.Trim(string(b), `"`)
	switch s {
	case LeastCommitted, PreferSeparate, RequireSeparate:
		*p = HostPolicy(s)
	case "":
		*p = DEFAULT
	default:
		return errors.New("Invalid HostPolicy: " + s)
	}
	return nil
}

type serviceDefinition ServiceDefinition

func (s *ServiceDefinition) UnmarshalJSON(b []byte) error {
	sd := serviceDefinition{}
	if err := json.Unmarshal(b, &sd); err == nil {
		*s = ServiceDefinition(sd)
	} else {
		return err
	}
	if len(s.Commands) > 0 {
		s.Runs = nil
		return nil
	}
	if len(s.Runs) > 0 {
		s.Commands = make(map[string]domain.Command)
		for k, v := range s.Runs {
			s.Commands[k] = domain.Command{
				Command:         v,
				CommitOnSuccess: true,
			}
		}
		s.Runs = nil
	}
	return nil
}

// private for dealing with unmarshal recursion
type endpointDefinition EndpointDefinition

// UnmarshalJSON implements the encoding/json/Unmarshaler interface used to convert deprecated vhosts list to VHostList
func (e *EndpointDefinition) UnmarshalJSON(b []byte) error {
	epd := endpointDefinition{}
	if err := json.Unmarshal(b, &epd); err == nil {
		*e = EndpointDefinition(epd)
	} else {
		return err
	}
	glog.V(4).Infof("EndpointDefintion UnmarshalJSON %#v", e)
	if len(e.VHostList) > 0 {
		//VHostList is defined, keep it and unset deprecated field if set
		e.VHosts = nil
		return nil
	}
	if len(e.VHosts) > 0 {
		// no VHostsList but vhosts is defined. Convert to VHostsList
		glog.Warningf("EndpointDefinition VHosts field is deprecated, see VHostList: %#v", e.VHosts)
		for _, vhost := range e.VHosts {
			e.VHostList = append(e.VHostList, VHost{Name: vhost, Enabled: true})
		}
		glog.Infof("VHostList %#v converted from VHosts %#v", e.VHostList, e.VHosts)
		e.VHosts = nil
	}
	return nil
}

func (s ServiceDefinition) String() string {
	return s.Name
}

//BuildFromPath given a path will create a ServiceDefintion
func BuildFromPath(path string) (*ServiceDefinition, error) {
	sd, err := getServiceDefinition(path)
	if err != nil {
		return nil, err
	}
	return sd, sd.ValidEntity()
}
<commit_msg>CC-1843: Raise log level of other spammy msgs<commit_after>// Copyright 2014 The Serviced 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 servicedefinition

import (
	"github.com/control-center/serviced/domain"
	"github.com/control-center/serviced/utils"
	"github.com/zenoss/glog"

	"encoding/json"
	"errors"
	"strings"
	"time"
)

//ServiceDefinition is the definition of a service hierarchy.
type ServiceDefinition struct {
	Name              string                 // Name of the defined service
	Title             string                 // Title is a label used when describing this service in the context of a service tree
	Version           string                 // Version of the defined service
	Command           string                 // Command which runs the service
	Description       string                 // Description of the service
	Tags              []string               // Searchable service tags
	ImageID           string                 // Docker image hosting the service
	Instances         domain.MinMax          // Constraints on the number of instances
	ChangeOptions     []string               // Control options for what happens when a running service is changed
	Launch            string                 // Must be "AUTO", the default, or "MANUAL"
	HostPolicy        HostPolicy             // Policy for starting up instances
	Hostname          string                 // Optional hostname which should be set on run
	Privileged        bool                   // Whether to run the container with extended privileges
	ConfigFiles       map[string]ConfigFile  // Config file templates
	Context           map[string]interface{} // Context information for the service
	Endpoints         []EndpointDefinition   // Comms endpoints used by the service
	Services          []ServiceDefinition    // Supporting subservices
	Tasks             []Task                 // Scheduled tasks for celery to find
	LogFilters        map[string]string      // map of log filter name to log filter definitions
	Volumes           []Volume               // list of volumes to bind into containers
	LogConfigs        []LogConfig
	Snapshot          SnapshotCommands              // Snapshot quiesce info for the service: Pause/Resume bash commands
	RAMCommitment     utils.EngNotation             // expected RAM commitment to use for scheduling
	CPUCommitment     uint64                        // expected CPU commitment (#cores) to use for scheduling
	DisableShell      bool                          // disables shell commands on the service
	Runs              map[string]string             // FIXME: This field is deprecated. Remove when possible.
	Commands          map[string]domain.Command     // Map of commands that can be executed with 'serviced run ...'
	Actions           map[string]string             // Map of commands that can be executed with 'serviced action ...'
	HealthChecks      map[string]domain.HealthCheck // HealthChecks for a service.
	Prereqs           []domain.Prereq               // Optional list of scripts that must be successfully run before kicking off the service command.
	MonitoringProfile domain.MonitorProfile         // An optional list of queryable metrics, graphs, and thresholds
	MemoryLimit       float64
	CPUShares         int64
	PIDFile           string // An optional path or command to generate a path for a PID file to which signals are relayed.
}

// SnapshotCommands commands to be called during and after a snapshot
type SnapshotCommands struct {
	Pause  string // bash command to pause the volume  (quiesce)
	Resume string // bash command to resume the volume (unquiesce)
}

// EndpointDefinition An endpoint that a Service exposes.
type EndpointDefinition struct {
	Name                string // Human readable name of the endpoint. Unique per service definition
	Purpose             string
	Protocol            string
	PortNumber          uint16
	PortTemplate        string // A template which, if specified, is used to calculate the port number
	VirtualAddress      string // An address by which an imported endpoint may be accessed within the container, e.g. "mysqlhost:1234"
	Application         string
	ApplicationTemplate string
	AddressConfig       AddressResourceConfig
	VHosts              []string // VHost is used to request named vhost for this endpoint. Should be the name of a
	// subdomain, i.e "myapplication"  not "myapplication.host.com"
	VHostList []VHost // VHost is used to request named vhost(s) for this endpoint.
	PortList  []Port
}

// VHost is the configuration for an application endpoint that wants an http VHost endpoint provided by Control Center
type VHost struct {
	Name    string // name of the vhost subdomain subdomain, i.e "myapplication"  not "myapplication.host.com
	Enabled bool   // whether the vhost should be enabled or disabled.
}

// Port is the configuration for an application endpoint port.
type Port struct {
	PortAddr string // which port number to use for this endpoint
	Enabled  bool   // whether the port should be enabled or disabled.
}

// Task A scheduled task
type Task struct {
	Name          string
	Schedule      string
	Command       string
	LastRunAt     time.Time
	TotalRunCount int
}

// Volume import defines a file system directory underneath an export directory
type Volume struct {
	Owner             string //Resource Path Owner
	Permission        string //Resource Path permissions, eg what you pass to chmod
	ResourcePath      string //Resource Pool Path, shared across all hosts in a resource pool
	ContainerPath     string //Container bind-mount path
	Type              string //Path use, i.e. "dfs" or "tmp"
	InitContainerPath string //Path to initialize the volume from at creation time, optional
}

// ConfigFile config file for a service
type ConfigFile struct {
	Filename    string // complete path of file
	Owner       string // owner of file within the container, root:root or 0:0 for root owned file, what you would pass to chown
	Permissions string // permission of file, eg 0664, what you would pass to chmod
	Content     string // content of config file
}

//AddressResourceConfig defines an external facing port for a service definition
type AddressResourceConfig struct {
	Port     uint16
	Protocol string
}

// LogConfig represents the configuration for a logfile for a service.
type LogConfig struct {
	Path    string   // The location on the container's filesystem of the log, can be a directory
	Type    string   // Arbitrary string that identifies the "types" of logs that come from this source. This will be
	Filters []string // A list of filters that must be contained in either the LogFilters or a parent's LogFilter,
	LogTags []LogTag // Key value pair of tags that are sent to logstash for all entries coming out of this logfile
}

// LogTag  no clue what this is. Maybe someone actually reads this
type LogTag struct {
	Name  string
	Value string
}

// HostPolicy represents the optional policy used to determine which hosts on
// which to run instances of a service. Default is to run on the available
// host with the most uncommitted RAM.
type HostPolicy string

const (
	// DEFAULT policy for scheduling a service instance
	DEFAULT HostPolicy = ""
	// LeastCommitted run on host w/ least committed resources
	LeastCommitted = "LEAST_COMMITTED"
	// Balance is a synonym for LeastCommitted
	Balance = "balance"
	// Pack runs instance on eligible host with most committed resources
	Pack = "pack"
	// PreferSeparate attempt to schedule instances of a service on separate hosts
	PreferSeparate = "PREFER_SEPARATE"
	// RequireSeparate schedule instances of a service on separate hosts
	RequireSeparate = "REQUIRE_SEPARATE"
)

// UnmarshalText implements the encoding/TextUnmarshaler interface
func (p *HostPolicy) UnmarshalText(b []byte) error {
	s := strings.Trim(string(b), `"`)
	switch s {
	case LeastCommitted, PreferSeparate, RequireSeparate:
		*p = HostPolicy(s)
	case "":
		*p = DEFAULT
	default:
		return errors.New("Invalid HostPolicy: " + s)
	}
	return nil
}

type serviceDefinition ServiceDefinition

func (s *ServiceDefinition) UnmarshalJSON(b []byte) error {
	sd := serviceDefinition{}
	if err := json.Unmarshal(b, &sd); err == nil {
		*s = ServiceDefinition(sd)
	} else {
		return err
	}
	if len(s.Commands) > 0 {
		s.Runs = nil
		return nil
	}
	if len(s.Runs) > 0 {
		s.Commands = make(map[string]domain.Command)
		for k, v := range s.Runs {
			s.Commands[k] = domain.Command{
				Command:         v,
				CommitOnSuccess: true,
			}
		}
		s.Runs = nil
	}
	return nil
}

// private for dealing with unmarshal recursion
type endpointDefinition EndpointDefinition

// UnmarshalJSON implements the encoding/json/Unmarshaler interface used to convert deprecated vhosts list to VHostList
func (e *EndpointDefinition) UnmarshalJSON(b []byte) error {
	epd := endpointDefinition{}
	if err := json.Unmarshal(b, &epd); err == nil {
		*e = EndpointDefinition(epd)
	} else {
		return err
	}
	glog.V(4).Infof("EndpointDefintion UnmarshalJSON %#v", e)
	if len(e.VHostList) > 0 {
		//VHostList is defined, keep it and unset deprecated field if set
		e.VHosts = nil
		return nil
	}
	if len(e.VHosts) > 0 {
		// no VHostsList but vhosts is defined. Convert to VHostsList
		if glog.V(2) {
			glog.Warningf("EndpointDefinition VHosts field is deprecated, see VHostList: %#v", e.VHosts)
		}
		for _, vhost := range e.VHosts {
			e.VHostList = append(e.VHostList, VHost{Name: vhost, Enabled: true})
		}
		glog.V(2).Infof("VHostList %#v converted from VHosts %#v", e.VHostList, e.VHosts)
		e.VHosts = nil
	}
	return nil
}

func (s ServiceDefinition) String() string {
	return s.Name
}

//BuildFromPath given a path will create a ServiceDefintion
func BuildFromPath(path string) (*ServiceDefinition, error) {
	sd, err := getServiceDefinition(path)
	if err != nil {
		return nil, err
	}
	return sd, sd.ValidEntity()
}
<|endoftext|>


<commit_before>package util

import (
	"os"
	"testing"
)

func TestTerminalWidth(t *testing.T) {
	stdout := int(os.Stdout.Fd())
	width, err := TerminalWidth(stdout)

	// Expectation: if running in a CI environment, stdout is not a terminal,
	// so err should be non-nil. Otherwise we assume stdout is a terminal, and
	// err should be nil. If you're manually running tests that redirect stdout
	// to a file, be sure to set env var CI=true to avoid test failure here.
	ci := os.Getenv("CI")
	if ci == "" || ci == "0" || ci == "false" {
		if err != nil || width <= 0 {
			t.Errorf("Expected TerminalWidth to return non-zero and nil err; instead found %d,%v", width, err)
		}
	} else if err == nil || width > 0 {
		t.Errorf("In CI environment, expected STDOUT to not be a terminal, but TerminalWidth returned %d,%v", width, err)
	}
}

func TestWrapStringWithPadding(t *testing.T) {
	cases := []struct {
		Input          string
		Width          int
		Padding        string
		ExpectedOutput string
	}{
		{"hello world\nhow are things?", 0, "   ", "hello world\nhow are things?"},
		{"hello world\nhow are things?", 3, "   ", "hello world\nhow are things?"},
		{"hello world\nhow are things?", 8, "   ", "hello\n   world\n   how\n   are\n   things?"},
		{"hello world\nhow are things?", 12, "x  ", "hello\nx  world\nx  how are\nx  things?"},
		{"hello world\nhow are things?", 80, "   ", "hello world\n   how are things?"},
	}

	for _, testcase := range cases {
		actual := WrapStringWithPadding(testcase.Input, testcase.Width, testcase.Padding)
		if actual != testcase.ExpectedOutput {
			t.Errorf("Unexpected return from WrapStringWithPadding(%q, %d, %q): expected %q, found %q", testcase.Input, testcase.Width, testcase.Padding, testcase.ExpectedOutput, actual)
		}
	}
}
<commit_msg>util subpackage: fix cranky unit test<commit_after>package util

import (
	"os"
	"testing"
)

func TestTerminalWidth(t *testing.T) {
	stdout := int(os.Stdout.Fd())
	width, err := TerminalWidth(stdout)

	// Expectation: if running in a CI environment, stdout is not a terminal,
	// so err should be non-nil. Otherwise, stdout may or may not be a terminal;
	// we can only confirm that the two return values are consistent with each
	// other.
	ci := os.Getenv("CI")
	if ci == "" || ci == "0" || ci == "false" {
		if (err == nil && width <= 0) || (err != nil && width > 0) {
			t.Errorf("Expected TerminalWidth to return non-zero and nil err, OR zero and non-nil err; instead found %d,%v", width, err)
		}
	} else if err == nil || width > 0 {
		t.Errorf("In CI environment, expected STDOUT to not be a terminal, but TerminalWidth returned %d,%v", width, err)
	}
}

func TestWrapStringWithPadding(t *testing.T) {
	cases := []struct {
		Input          string
		Width          int
		Padding        string
		ExpectedOutput string
	}{
		{"hello world\nhow are things?", 0, "   ", "hello world\nhow are things?"},
		{"hello world\nhow are things?", 3, "   ", "hello world\nhow are things?"},
		{"hello world\nhow are things?", 8, "   ", "hello\n   world\n   how\n   are\n   things?"},
		{"hello world\nhow are things?", 12, "x  ", "hello\nx  world\nx  how are\nx  things?"},
		{"hello world\nhow are things?", 80, "   ", "hello world\n   how are things?"},
	}

	for _, testcase := range cases {
		actual := WrapStringWithPadding(testcase.Input, testcase.Width, testcase.Padding)
		if actual != testcase.ExpectedOutput {
			t.Errorf("Unexpected return from WrapStringWithPadding(%q, %d, %q): expected %q, found %q", testcase.Input, testcase.Width, testcase.Padding, testcase.ExpectedOutput, actual)
		}
	}
}
<|endoftext|>