<commit_before>// Copyright 2014 The Dename 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 client

import (
	"bytes"
	"code.google.com/p/go.net/proxy"
	_ "crypto/sha512" // for tls
	"crypto/tls"
	"encoding/binary"
	"fmt"
	"github.com/agl/ed25519"
	. "github.com/andres-erbsen/dename/protocol"
	"github.com/gogo/protobuf/proto"
	"io"
	"net"
	"time"
)

var true_ = true
var pad_to uint64 = 4 << 10

type serverInfo struct {
	pk        *Profile_PublicKey
	address   string
	timeout   time.Duration
	tlsConfig *tls.Config
}

type Client struct {
	freshnessThreshold        time.Duration
	freshnessNumConfirmations int
	consensusNumConfirmations int
	dialer                    proxy.Dialer
	servers                   map[uint64]*serverInfo
}

func (c *Client) connect(s *serverInfo) (net.Conn, error) {
	var plainconn net.Conn
	plainconn, err := c.dialer.Dial("tcp", s.address)
	if err != nil {
		return nil, err
	}
	conn := tls.Client(plainconn, s.tlsConfig)
	conn.SetDeadline(time.Now().Add(s.timeout))
	return conn, conn.Handshake()
}

func (c *Client) atSomeServer(f func(net.Conn) (bool, error)) (err error) {
next_server:
	for _, server := range c.servers {
		var conn net.Conn
		conn, err = c.connect(server)
		if err != nil {
			continue next_server
		}
		var done bool
		done, err = f(conn)
		if done {
			conn.Close()
			return err
		}
		conn.Close()
	}
	return err
}

// Lookup retrieves the profile that corresponds to name from any server in the
// client's config. It is guaranteed that at least NumConfirmations of the
// servers have confirmed the correctness of the (name, profile) mapping and
// that Freshness.NumConfirmations have done this within Freshness.Threshold.
func (c *Client) Lookup(name string) (profile *Profile, err error) {
	err = c.atSomeServer(func(conn net.Conn) (bool, error) {
		rq := &ClientMessage{PeekState: &true_, ResolveName: []byte(name), PadReplyTo: &pad_to}
		if _, err = conn.Write(Frame(Pad(PBEncode(rq), 256))); err != nil {
			return false, err
		}
		var reply *ClientReply
		if reply, err = readReply(conn); err != nil {
			return false, err
		}
		var root []byte
		root, err = c.VerifyConsensus(reply.StateConfirmations)
		if err != nil {
			return true, ErrCouldntVerify
		}
		profileBs, err := VerifyResolveAgainstRoot(root, name, reply.LookupNodes)
		if err != nil {
			return true, ErrCouldntVerify
		}
		if profileBs == nil {
			profile = nil
		} else {
			profile = new(Profile)
			err = proto.Unmarshal(profileBs, profile)
			if err != nil {
				return true, err
			}
		}
		return true, nil
	})
	return
}

var (
	ErrRegistrationDisabled = fmt.Errorf("registration disabled")
	ErrInviteInvalid        = fmt.Errorf("invite not valid")
	ErrInviteUsed           = fmt.Errorf("invite already used")
	ErrNotAuthorized        = fmt.Errorf("not authorized")
	ErrCouldntVerify        = fmt.Errorf("could not verify the correctness of the response")
)

// Enact is a low-level function that completes an already complete profile
// operation at any known server. You probably want to use Register, Modify or
// Transfer instead.
func (c *Client) Enact(op *SignedProfileOperation, invite []byte) (err error) {
	err = c.atSomeServer(func(conn net.Conn) (bool, error) {
		msg := &ClientMessage{ModifyProfile: op, InviteCode: invite}
		_, err = conn.Write(Frame(Pad(PBEncode(msg), 4<<10)))
		if err != nil {
			return false, err
		}
		var reply *ClientReply
		reply, err = readReply(conn)
		if err != nil {
			return false, err
		}
		switch reply.GetStatus() {
		case ClientReply_OK:
			return true, nil
		case ClientReply_REGISTRATION_DISABLED:
			return false, ErrRegistrationDisabled
		case ClientReply_INVITE_INVALID:
			return false, ErrInviteInvalid
		case ClientReply_INVITE_USED:
			return false, ErrInviteUsed
		case ClientReply_NOT_AUTHORIZED:
			return false, ErrNotAuthorized
		default:
			return false, fmt.Errorf("unknown status code")
		}
	})
	return
}

type byteReader struct{ io.Reader }

func (r byteReader) ReadByte() (byte, error) {
	var ret [1]byte
	_, err := io.ReadFull(r, ret[:])
	return ret[0], err
}

func readReply(conn net.Conn) (reply *ClientReply, err error) {
	var size uint64
	size, err = binary.ReadUvarint(byteReader{conn})
	if err != nil {
		return
	}
	if size > 5<<10 {
		return nil, fmt.Errorf("reply too big")
	}
	buf := make([]byte, size)
	if _, err = io.ReadFull(conn, buf); err != nil {
		return
	}
	reply = new(ClientReply)
	err = proto.Unmarshal(Unpad(buf), reply)
	return
}

// Low-level convenience function to create a SignedProfileOperation with no
// signatures. You probably want to use Register, Modify or Transfer instead.
func MakeOperation(name string, profile *Profile) *SignedProfileOperation {
	return &SignedProfileOperation{
		ProfileOperation: PBEncode(&SignedProfileOperation_ProfileOperationT{
			Name:       []byte(name),
			NewProfile: PBEncode(profile),
		}),
	}
}

// Creates a signed operation structure to transfer name to profile. To make
// this change take effect, the recipient has to call AcceptTransfer with the
// secret key whose public counterpart is in profile.
func TransferProposal(sk *[ed25519.PrivateKeySize]byte, name string,
	profile *Profile) *SignedProfileOperation {
	return OldSign(sk, MakeOperation(name, profile))
}

// Gives the old owner's signature for op using sk
func OldSign(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) *SignedProfileOperation {
	msg := append([]byte("ModifyProfileOld\x00"), op.ProfileOperation...)
	op.OldProfileSignature = ed25519.Sign(sk, msg)[:]
	return op
}

// Gives the new owner's signature for op using sk
func NewSign(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) *SignedProfileOperation {
	msg := append([]byte("ModifyProfileNew\x00"), op.ProfileOperation...)
	op.NewProfileSignature = ed25519.Sign(sk, msg)[:]
	return op
}

// Register associates a profile with a name. The invite is used to convince
// the server that we are indeed allowed a new name, it is not associated with
// the profile in any way. If profile.Version is set, it must be 0.
func (c *Client) Register(sk *[ed25519.PrivateKeySize]byte, name string, profile *Profile, invite []byte) error {
	return c.Enact(NewSign(sk, MakeOperation(name, profile)), invite)
}

// AcceptTransfer uses the new secret key and the transfer operation generated
// using the old secret key to associate the op.Name with op.NewProfile.
func (c *Client) AcceptTransfer(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) error {
	return c.Enact(NewSign(sk, op), nil)
}

// Modify uses a secret key to associate name with profile. The caller must
// ensure that profile.Version is strictly greater than the version of the
// currently registered profile; it is usually good practice to increase the
// version by exactly one.
func (c *Client) Modify(sk *[ed25519.PrivateKeySize]byte, name string, profile *Profile) error {
	return c.Enact(NewSign(sk, OldSign(sk, MakeOperation(name, profile))), nil)
}

// VerifiyConsensus performs the low-level checks to see whether a set of
// statements made by the servers is sufficient to consider the state contained
// by them to be canonical.
func (c *Client) VerifyConsensus(signedHashOfStateMsgs []*SignedServerMessage) (
	rootHash []byte, err error) {
	consensusServers := make(map[uint64]struct{})
	freshnessServers := make(map[uint64]struct{})
	for _, signedMsg := range signedHashOfStateMsgs {
		msg := new(SignedServerMessage_ServerMessage)
		if err = proto.Unmarshal(signedMsg.Message, msg); err != nil {
			continue
		}
		server, ok := c.servers[*msg.Server]
		if !ok || server.pk.Ed25519 == nil {
			continue
		}
		var pk_ed [ed25519.PublicKeySize]byte
		copy(pk_ed[:], server.pk.Ed25519)
		var sig_ed [ed25519.SignatureSize]byte
		copy(sig_ed[:], signedMsg.Signature)
		if !ed25519.Verify(&pk_ed, append([]byte("msg\x00"), signedMsg.Message...), &sig_ed) {
			continue
		}
		if rootHash == nil {
			rootHash = msg.HashOfState
		} else if !bytes.Equal(rootHash, msg.HashOfState) {
			return nil, fmt.Errorf("verifyConsensus: state hashes differ")
		}
		consensusServers[*msg.Server] = struct{}{}
		if !time.Unix(int64(*msg.Time), 0).Add(c.freshnessThreshold).After(server.tlsConfig.Time()) {
			continue
		}
		freshnessServers[*msg.Server] = struct{}{}
	}
	if len(consensusServers) < c.consensusNumConfirmations {
		return nil, fmt.Errorf("not enough valid signatures for consensus (%d out of %d)", len(consensusServers), c.consensusNumConfirmations)
	}
	if len(freshnessServers) < c.freshnessNumConfirmations {
		return nil, fmt.Errorf("not enough fresh signatures (%d out of %d)", len(freshnessServers), c.freshnessNumConfirmations)
	}
	return rootHash, nil
}
<commit_msg>added expiration warning<commit_after>// Copyright 2014 The Dename 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 client

import (
	"bytes"
	"code.google.com/p/go.net/proxy"
	_ "crypto/sha512" // for tls
	"crypto/tls"
	"encoding/binary"
	"fmt"
	"github.com/agl/ed25519"
	. "github.com/andres-erbsen/dename/protocol"
	"github.com/gogo/protobuf/proto"
	"io"
	"net"
	"time"
)

var true_ = true
var pad_to uint64 = 4 << 10

type serverInfo struct {
	pk        *Profile_PublicKey
	address   string
	timeout   time.Duration
	tlsConfig *tls.Config
}

type Client struct {
	freshnessThreshold        time.Duration
	freshnessNumConfirmations int
	consensusNumConfirmations int
	dialer                    proxy.Dialer
	servers                   map[uint64]*serverInfo
}

func (c *Client) connect(s *serverInfo) (net.Conn, error) {
	var plainconn net.Conn
	plainconn, err := c.dialer.Dial("tcp", s.address)
	if err != nil {
		return nil, err
	}
	conn := tls.Client(plainconn, s.tlsConfig)
	conn.SetDeadline(time.Now().Add(s.timeout))
	return conn, conn.Handshake()
}

func (c *Client) atSomeServer(f func(net.Conn) (bool, error)) (err error) {
next_server:
	for _, server := range c.servers {
		var conn net.Conn
		conn, err = c.connect(server)
		if err != nil {
			continue next_server
		}
		var done bool
		done, err = f(conn)
		if done {
			conn.Close()
			return err
		}
		conn.Close()
	}
	return err
}

// Lookup retrieves the profile that corresponds to name from any server in the
// client's config. It is guaranteed that at least NumConfirmations of the
// servers have confirmed the correctness of the (name, profile) mapping and
// that Freshness.NumConfirmations have done this within Freshness.Threshold.
func (c *Client) Lookup(name string) (profile *Profile, err error) {
	err = c.atSomeServer(func(conn net.Conn) (bool, error) {
		rq := &ClientMessage{PeekState: &true_, ResolveName: []byte(name), PadReplyTo: &pad_to}
		if _, err = conn.Write(Frame(Pad(PBEncode(rq), 256))); err != nil {
			return false, err
		}
		var reply *ClientReply
		if reply, err = readReply(conn); err != nil {
			return false, err
		}
		var root []byte
		root, err = c.VerifyConsensus(reply.StateConfirmations)
		if err != nil {
			return true, ErrCouldntVerify
		}
		profileBs, err := VerifyResolveAgainstRoot(root, name, reply.LookupNodes)
		if err != nil {
			return true, ErrCouldntVerify
		}
		if profileBs == nil {
			profile = nil
			return true, nil
		}
		profile = new(Profile)
		err = proto.Unmarshal(profileBs, profile)
		if err != nil {
			return true, err
		}
		expirationTime := time.Unix(int64(*profile.ExpirationTime), 0)
		if expirationTime.Before(time.Now().Add(MAX_VALIDITY_PERIOD * time.Second / 2)) {
			return true, fmt.Errorf("the profile is out of date and will be erased completely on %s", expirationTime)
		}
		return true, nil
	})
	return
}

var (
	ErrRegistrationDisabled = fmt.Errorf("registration disabled")
	ErrInviteInvalid        = fmt.Errorf("invite not valid")
	ErrInviteUsed           = fmt.Errorf("invite already used")
	ErrNotAuthorized        = fmt.Errorf("not authorized")
	ErrCouldntVerify        = fmt.Errorf("could not verify the correctness of the response")
)

// Enact is a low-level function that completes an already complete profile
// operation at any known server. You probably want to use Register, Modify or
// Transfer instead.
func (c *Client) Enact(op *SignedProfileOperation, invite []byte) (err error) {
	err = c.atSomeServer(func(conn net.Conn) (bool, error) {
		msg := &ClientMessage{ModifyProfile: op, InviteCode: invite}
		_, err = conn.Write(Frame(Pad(PBEncode(msg), 4<<10)))
		if err != nil {
			return false, err
		}
		var reply *ClientReply
		reply, err = readReply(conn)
		if err != nil {
			return false, err
		}
		switch reply.GetStatus() {
		case ClientReply_OK:
			return true, nil
		case ClientReply_REGISTRATION_DISABLED:
			return false, ErrRegistrationDisabled
		case ClientReply_INVITE_INVALID:
			return false, ErrInviteInvalid
		case ClientReply_INVITE_USED:
			return false, ErrInviteUsed
		case ClientReply_NOT_AUTHORIZED:
			return false, ErrNotAuthorized
		default:
			return false, fmt.Errorf("unknown status code")
		}
	})
	return
}

type byteReader struct{ io.Reader }

func (r byteReader) ReadByte() (byte, error) {
	var ret [1]byte
	_, err := io.ReadFull(r, ret[:])
	return ret[0], err
}

func readReply(conn net.Conn) (reply *ClientReply, err error) {
	var size uint64
	size, err = binary.ReadUvarint(byteReader{conn})
	if err != nil {
		return
	}
	if size > 5<<10 {
		return nil, fmt.Errorf("reply too big")
	}
	buf := make([]byte, size)
	if _, err = io.ReadFull(conn, buf); err != nil {
		return
	}
	reply = new(ClientReply)
	err = proto.Unmarshal(Unpad(buf), reply)
	return
}

// Low-level convenience function to create a SignedProfileOperation with no
// signatures. You probably want to use Register, Modify or Transfer instead.
func MakeOperation(name string, profile *Profile) *SignedProfileOperation {
	return &SignedProfileOperation{
		ProfileOperation: PBEncode(&SignedProfileOperation_ProfileOperationT{
			Name:       []byte(name),
			NewProfile: PBEncode(profile),
		}),
	}
}

// Creates a signed operation structure to transfer name to profile. To make
// this change take effect, the recipient has to call AcceptTransfer with the
// secret key whose public counterpart is in profile.
func TransferProposal(sk *[ed25519.PrivateKeySize]byte, name string,
	profile *Profile) *SignedProfileOperation {
	return OldSign(sk, MakeOperation(name, profile))
}

// Gives the old owner's signature for op using sk
func OldSign(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) *SignedProfileOperation {
	msg := append([]byte("ModifyProfileOld\x00"), op.ProfileOperation...)
	op.OldProfileSignature = ed25519.Sign(sk, msg)[:]
	return op
}

// Gives the new owner's signature for op using sk
func NewSign(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) *SignedProfileOperation {
	msg := append([]byte("ModifyProfileNew\x00"), op.ProfileOperation...)
	op.NewProfileSignature = ed25519.Sign(sk, msg)[:]
	return op
}

// Register associates a profile with a name. The invite is used to convince
// the server that we are indeed allowed a new name, it is not associated with
// the profile in any way. If profile.Version is set, it must be 0.
func (c *Client) Register(sk *[ed25519.PrivateKeySize]byte, name string, profile *Profile, invite []byte) error {
	return c.Enact(NewSign(sk, MakeOperation(name, profile)), invite)
}

// AcceptTransfer uses the new secret key and the transfer operation generated
// using the old secret key to associate the op.Name with op.NewProfile.
func (c *Client) AcceptTransfer(sk *[ed25519.PrivateKeySize]byte, op *SignedProfileOperation) error {
	return c.Enact(NewSign(sk, op), nil)
}

// Modify uses a secret key to associate name with profile. The caller must
// ensure that profile.Version is strictly greater than the version of the
// currently registered profile; it is usually good practice to increase the
// version by exactly one.
func (c *Client) Modify(sk *[ed25519.PrivateKeySize]byte, name string, profile *Profile) error {
	return c.Enact(NewSign(sk, OldSign(sk, MakeOperation(name, profile))), nil)
}

// VerifiyConsensus performs the low-level checks to see whether a set of
// statements made by the servers is sufficient to consider the state contained
// by them to be canonical.
func (c *Client) VerifyConsensus(signedHashOfStateMsgs []*SignedServerMessage) (
	rootHash []byte, err error) {
	consensusServers := make(map[uint64]struct{})
	freshnessServers := make(map[uint64]struct{})
	for _, signedMsg := range signedHashOfStateMsgs {
		msg := new(SignedServerMessage_ServerMessage)
		if err = proto.Unmarshal(signedMsg.Message, msg); err != nil {
			continue
		}
		server, ok := c.servers[*msg.Server]
		if !ok || server.pk.Ed25519 == nil {
			continue
		}
		var pk_ed [ed25519.PublicKeySize]byte
		copy(pk_ed[:], server.pk.Ed25519)
		var sig_ed [ed25519.SignatureSize]byte
		copy(sig_ed[:], signedMsg.Signature)
		if !ed25519.Verify(&pk_ed, append([]byte("msg\x00"), signedMsg.Message...), &sig_ed) {
			continue
		}
		if rootHash == nil {
			rootHash = msg.HashOfState
		} else if !bytes.Equal(rootHash, msg.HashOfState) {
			return nil, fmt.Errorf("verifyConsensus: state hashes differ")
		}
		consensusServers[*msg.Server] = struct{}{}
		if !time.Unix(int64(*msg.Time), 0).Add(c.freshnessThreshold).After(server.tlsConfig.Time()) {
			continue
		}
		freshnessServers[*msg.Server] = struct{}{}
	}
	if len(consensusServers) < c.consensusNumConfirmations {
		return nil, fmt.Errorf("not enough valid signatures for consensus (%d out of %d)", len(consensusServers), c.consensusNumConfirmations)
	}
	if len(freshnessServers) < c.freshnessNumConfirmations {
		return nil, fmt.Errorf("not enough fresh signatures (%d out of %d)", len(freshnessServers), c.freshnessNumConfirmations)
	}
	return rootHash, nil
}
<|endoftext|>


<commit_before>package client

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"

	"github.com/Sirupsen/logrus"
	"github.com/amalgam8/controller/rules"
	"github.com/amalgam8/registry/api/env"
)

type RuleResponse struct {
	Rules    []rules.Rule `json:"rules"`
	Revision int64        `json:"revision"`
}

// Config stores the configurable attributes of the client.
type Config struct {

	// URL of the controller server.
	URL string

	// AuthToken is the token to be used for authentication with the controller.
	// If left empty, no authentication is used.
	AuthToken string

	// HTTPClient can be used to customize the underlying HTTP client behavior,
	// such as enabling TLS, setting timeouts, etc.
	// If left nil, a default HTTP client will be used.
	HTTPClient *http.Client
}

type Client interface {
	GetRules(rules.Filter) (RuleResponse, error)
}

// New controller client.
func New(conf Config) (Client, error) {
	if err := normalizeConfig(&conf); err != nil {
		return nil, err
	}

	return &client{
		url:        conf.URL,
		authToken:  conf.AuthToken,
		httpClient: conf.HTTPClient,
	}, nil
}

// normalizeConfig validates and sets defaults for the client configuration.
func normalizeConfig(conf *Config) error {
	u, err := url.Parse(conf.URL)
	if err != nil {
		return err
	}

	if u.Scheme != "http" && u.Scheme != "https" {
		// TODO: custom error type
		return fmt.Errorf("client: Unsupported protocol scheme %v", u.Scheme)
	}

	if conf.HTTPClient == nil {
		conf.HTTPClient = &http.Client{}
	}

	return nil
}

type client struct {
	url        string
	authToken  string
	httpClient *http.Client
}

func (c *client) GetRules(filter rules.Filter) (RuleResponse, error) {
	var ruleResponse RuleResponse

	u, err := url.Parse(c.url + "/v1/rules")
	if err != nil {
		return ruleResponse, err
	}

	query := u.Query()
	for _, id := range filter.IDs {
		query.Add("id", id)
	}

	for _, tag := range filter.Tags {
		query.Add("tag", tag)
	}
	u.RawQuery = query.Encode()

	req, err := http.NewRequest("GET", u.String(), nil)
	if err != nil {
		logrus.WithError(err).Warn("Error building request to get rules from controller")
		return ruleResponse, err
	}
	c.setAuthHeader(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		logrus.WithError(err).Warn("Failed to retrieve rules from controller")
		return ruleResponse, err
	}

	requestID := resp.Header.Get(env.RequestID)

	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		respBytes, _ := ioutil.ReadAll(resp.Body)

		logrus.WithFields(logrus.Fields{
			"status_code": resp.StatusCode,
			"request_id":  requestID,
			"body":        string(respBytes),
		}).Warn("Controller returned bad response code")
		return ruleResponse, errors.New("Controller returned bad response code") // FIXME: custom error?
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logrus.WithError(err).WithFields(logrus.Fields{
			"request_id": requestID,
		}).Warn("Error reading rules JSON from controller")
		return ruleResponse, err
	}

	if err = json.Unmarshal(body, &ruleResponse); err != nil {
		logrus.WithError(err).WithFields(logrus.Fields{
			"request_id": requestID,
		}).Warn("Error reading rules JSON from controller")
		return ruleResponse, err
	}

	return ruleResponse, nil
}

func (c *client) setAuthHeader(req *http.Request) {
	// If the token is empty we assume no authentication is enabled on the controller and do not add the
	// authorization header.
	if c.authToken != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.authToken))
	}
}
<commit_msg>Added comments to client<commit_after>package client

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"

	"github.com/Sirupsen/logrus"
	"github.com/amalgam8/controller/rules"
	"github.com/amalgam8/registry/api/env"
)

// RuleResponse is the information returned from a rule query.
type RuleResponse struct {
	// Rules that matched the filter.
	Rules []rules.Rule `json:"rules"`

	// Revision of the rules for this namespace.
	Revision int64 `json:"revision"`
}

// Config stores the configurable attributes of the client.
type Config struct {

	// URL of the controller server.
	URL string

	// AuthToken is the token to be used for authentication with the controller.
	// If left empty, no authentication is used.
	AuthToken string

	// HTTPClient can be used to customize the underlying HTTP client behavior,
	// such as enabling TLS, setting timeouts, etc.
	// If left nil, a default HTTP client will be used.
	HTTPClient *http.Client
}

// Client for the controller.
type Client interface {
	// GetRules returns the rules for this namespace that match the filter.
	GetRules(f rules.Filter) (RuleResponse, error)
}

// New constructs a new controller client.
func New(conf Config) (Client, error) {
	if err := normalizeConfig(&conf); err != nil {
		return nil, err
	}

	return &client{
		url:        conf.URL,
		authToken:  conf.AuthToken,
		httpClient: conf.HTTPClient,
	}, nil
}

// normalizeConfig validates and sets defaults for the client configuration.
func normalizeConfig(conf *Config) error {
	u, err := url.Parse(conf.URL)
	if err != nil {
		return err
	}

	if u.Scheme != "http" && u.Scheme != "https" {
		// TODO: custom error type
		return fmt.Errorf("client: Unsupported protocol scheme %v", u.Scheme)
	}

	if conf.HTTPClient == nil {
		conf.HTTPClient = &http.Client{}
	}

	return nil
}

type client struct {
	url        string
	authToken  string
	httpClient *http.Client
}

func (c *client) GetRules(filter rules.Filter) (RuleResponse, error) {
	var ruleResponse RuleResponse

	u, err := url.Parse(c.url + "/v1/rules")
	if err != nil {
		return ruleResponse, err
	}

	query := u.Query()
	for _, id := range filter.IDs {
		query.Add("id", id)
	}

	for _, tag := range filter.Tags {
		query.Add("tag", tag)
	}
	u.RawQuery = query.Encode()

	req, err := http.NewRequest("GET", u.String(), nil)
	if err != nil {
		logrus.WithError(err).Warn("Error building request to get rules from controller")
		return ruleResponse, err
	}
	c.setAuthHeader(req)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		logrus.WithError(err).Warn("Failed to retrieve rules from controller")
		return ruleResponse, err
	}
	defer resp.Body.Close()

	requestID := resp.Header.Get(env.RequestID)

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logrus.WithError(err).WithFields(logrus.Fields{
			"request_id": requestID,
		}).Warn("Error reading response from controller")
		return ruleResponse, err
	}

	if resp.StatusCode != http.StatusOK {
		logrus.WithFields(logrus.Fields{
			"status_code": resp.StatusCode,
			"request_id":  requestID,
			"body":        string(data),
		}).Warn("Controller returned unexpected response code")
		return ruleResponse, errors.New("client: received unexpected response code") // FIXME: custom error?
	}

	if err = json.Unmarshal(data, &ruleResponse); err != nil {
		logrus.WithError(err).WithFields(logrus.Fields{
			"request_id": requestID,
		}).Warn("Error reading rules JSON from controller")
		return ruleResponse, err
	}

	return ruleResponse, nil
}

// setAuthHeader optionally sets an authorization header. If the token is empty we assume no authentication is enabled
// on the controller and do not add the header.
func (c *client) setAuthHeader(req *http.Request) {
	if c.authToken != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", c.authToken))
	}
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/skygeario/skygear-server/pkg/core/redis"

	"github.com/joho/godotenv"
	"github.com/kelseyhightower/envconfig"

	"github.com/skygeario/skygear-server/pkg/auth"
	authTemplate "github.com/skygeario/skygear-server/pkg/auth/template"
	"github.com/skygeario/skygear-server/pkg/core/async"
	"github.com/skygeario/skygear-server/pkg/core/db"
	"github.com/skygeario/skygear-server/pkg/core/template"

	"github.com/skygeario/skygear-server/pkg/auth/handler"
	forgotpwdhandler "github.com/skygeario/skygear-server/pkg/auth/handler/forgotpwd"
	mfaHandler "github.com/skygeario/skygear-server/pkg/auth/handler/mfa"
	"github.com/skygeario/skygear-server/pkg/auth/handler/session"
	ssohandler "github.com/skygeario/skygear-server/pkg/auth/handler/sso"
	userverifyhandler "github.com/skygeario/skygear-server/pkg/auth/handler/userverify"
	"github.com/skygeario/skygear-server/pkg/auth/task"
	"github.com/skygeario/skygear-server/pkg/core/config"
	"github.com/skygeario/skygear-server/pkg/core/logging"
	"github.com/skygeario/skygear-server/pkg/core/middleware"
	"github.com/skygeario/skygear-server/pkg/core/server"
)

type configuration struct {
	Standalone                        bool
	StandaloneTenantConfigurationFile string                      `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
	Host                              string                      `envconfig:"SERVER_HOST" default:"localhost:3000"`
	ValidHosts                        string                      `envconfig:"VALID_HOSTS"`
	Redis                             redis.Configuration         `envconfig:"REDIS"`
	UseInsecureCookie                 bool                        `envconfig:"INSECURE_COOKIE"`
	Default                           config.DefaultConfiguration `envconfig:"DEFAULT"`
}

/*
	@API Auth Gear
	@Version 1.0.0
	@Server {base_url}/_auth
		Auth Gear URL
		@Variable base_url https://my_app.skygearapis.com
			Skygear App URL

	@SecuritySchemeAPIKey access_key header X-Skygear-API-Key
		Access key used by client app
	@SecuritySchemeAPIKey master_key header X-Skygear-API-Key
		Master key used by admins, can perform administrative operations.
		Can be used as access key as well.
	@SecuritySchemeHTTP access_token Bearer JWT
		Access token of user
	@SecurityRequirement access_key

	@Tag User
		User information
	@Tag User Verification
		Login IDs verification
	@Tag Forgot Password
		Password recovery process
	@Tag Administration
		Administrative operation
	@Tag SSO
		Single sign-on
*/
func main() {
	// logging initialization
	logging.SetModule("auth")
	loggerFactory := logging.NewFactory(logging.NewDefaultLogHook(nil))
	logger := loggerFactory.NewLogger("auth")

	envErr := godotenv.Load()
	if envErr != nil {
		logger.WithError(envErr).Debug("Cannot load .env file")
	}

	configuration := configuration{}
	envconfig.Process("", &configuration)
	if configuration.ValidHosts == "" {
		configuration.ValidHosts = configuration.Host
	}

	// default template initialization
	templateEngine := template.NewEngine()
	authTemplate.RegisterDefaultTemplates(templateEngine)

	dbPool := db.NewPool()
	redisPool, err := redis.NewPool(configuration.Redis)
	if err != nil {
		logger.Fatalf("fail to create redis pool: %v", err.Error())
	}
	asyncTaskExecutor := async.NewExecutor(dbPool)
	authDependency := auth.DependencyMap{
		AsyncTaskExecutor:    asyncTaskExecutor,
		TemplateEngine:       templateEngine,
		UseInsecureCookie:    configuration.UseInsecureCookie,
		DefaultConfiguration: configuration.Default,
	}

	task.AttachVerifyCodeSendTask(asyncTaskExecutor, authDependency)
	task.AttachPwHousekeeperTask(asyncTaskExecutor, authDependency)
	task.AttachWelcomeEmailSendTask(asyncTaskExecutor, authDependency)

	var srv server.Server
	if configuration.Standalone {
		filename := configuration.StandaloneTenantConfigurationFile
		reader, err := os.Open(filename)
		if err != nil {
			logger.WithError(err).Error("Cannot open standalone config")
		}
		tenantConfig, err := config.NewTenantConfigurationFromYAML(reader)
		if err != nil {
			logger.WithError(err).Fatal("Cannot parse standalone config")
		}

		serverOption := server.DefaultOption()
		serverOption.GearPathPrefix = "/_auth"
		srv = server.NewServerWithOption(configuration.Host, authDependency, serverOption)
		srv.Use(middleware.WriteTenantConfigMiddleware{
			ConfigurationProvider: middleware.ConfigurationProviderFunc(func(_ *http.Request) (config.TenantConfiguration, error) {
				return *tenantConfig, nil
			}),
		}.Handle)
		srv.Use(middleware.ValidateHostMiddleware{ValidHosts: configuration.ValidHosts}.Handle)
		srv.Use(middleware.RequestIDMiddleware{}.Handle)
		srv.Use(middleware.CORSMiddleware{}.Handle)
	} else {
		srv = server.NewServer(configuration.Host, authDependency)
		srv.Use(middleware.ReadTenantConfigMiddleware{}.Handle)
	}

	srv.Use(middleware.DBMiddleware{Pool: dbPool}.Handle)
	srv.Use(middleware.RedisMiddleware{Pool: redisPool}.Handle)
	srv.Use(middleware.AuthMiddleware{}.Handle)

	srv.Use(middleware.Injecter{
		MiddlewareFactory: middleware.AuthnMiddlewareFactory{},
		Dependency:        authDependency,
	}.Handle)

	handler.AttachSignupHandler(&srv, authDependency)
	handler.AttachLoginHandler(&srv, authDependency)
	handler.AttachLogoutHandler(&srv, authDependency)
	handler.AttachRefreshHandler(&srv, authDependency)
	handler.AttachMeHandler(&srv, authDependency)
	handler.AttachSetDisableHandler(&srv, authDependency)
	handler.AttachChangePasswordHandler(&srv, authDependency)
	handler.AttachResetPasswordHandler(&srv, authDependency)
	handler.AttachWelcomeEmailHandler(&srv, authDependency)
	handler.AttachUpdateMetadataHandler(&srv, authDependency)
	forgotpwdhandler.AttachForgotPasswordHandler(&srv, authDependency)
	forgotpwdhandler.AttachForgotPasswordResetHandler(&srv, authDependency)
	userverifyhandler.AttachVerifyRequestHandler(&srv, authDependency)
	userverifyhandler.AttachVerifyCodeHandler(&srv, authDependency)
	ssohandler.AttachAuthURLHandler(&srv, authDependency)
	ssohandler.AttachConfigHandler(&srv, authDependency)
	ssohandler.AttachIFrameHandlerFactory(&srv, authDependency)
	ssohandler.AttachCustomTokenLoginHandler(&srv, authDependency)
	ssohandler.AttachAuthHandler(&srv, authDependency)
	ssohandler.AttachLoginHandler(&srv, authDependency)
	ssohandler.AttachLinkHandler(&srv, authDependency)
	ssohandler.AttachUnlinkHandler(&srv, authDependency)
	session.AttachListHandler(&srv, authDependency)
	session.AttachGetHandler(&srv, authDependency)
	session.AttachRevokeHandler(&srv, authDependency)
	session.AttachRevokeAllHandler(&srv, authDependency)
	mfaHandler.AttachListRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachRegenerateRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachListAuthenticatorHandler(&srv, authDependency)
	mfaHandler.AttachCreateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachActivateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachDeleteAuthenticatorHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachRevokeAllBearerTokenHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateBearerTokenHandler(&srv, authDependency)
	mfaHandler.AttachCreateOOBHandler(&srv, authDependency)
	mfaHandler.AttachTriggerOOBHandler(&srv, authDependency)
	mfaHandler.AttachActivateOOBHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateOOBHandler(&srv, authDependency)

	go func() {
		logger.Info("Starting auth gear")
		if err := srv.ListenAndServe(); err != nil {
			logger.WithError(err).Error("Cannot start HTTP server")
		}
	}()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	// wait interrupt signal
	select {
	case <-sig:
		logger.Info("Stopping HTTP server")
	}

	// create shutdown context with 10 second timeout
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// shutdown the server
	err = srv.Shutdown(ctx)
	if err != nil {
		logger.WithError(err).Fatal("Cannot shutdown server")
	}
}
<commit_msg>Reorganize imports in cmd/auth/main.go<commit_after>package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/joho/godotenv"
	"github.com/kelseyhightower/envconfig"

	"github.com/skygeario/skygear-server/pkg/auth"
	"github.com/skygeario/skygear-server/pkg/auth/handler"
	forgotpwdhandler "github.com/skygeario/skygear-server/pkg/auth/handler/forgotpwd"
	mfaHandler "github.com/skygeario/skygear-server/pkg/auth/handler/mfa"
	"github.com/skygeario/skygear-server/pkg/auth/handler/session"
	ssohandler "github.com/skygeario/skygear-server/pkg/auth/handler/sso"
	userverifyhandler "github.com/skygeario/skygear-server/pkg/auth/handler/userverify"
	"github.com/skygeario/skygear-server/pkg/auth/task"
	authTemplate "github.com/skygeario/skygear-server/pkg/auth/template"
	"github.com/skygeario/skygear-server/pkg/core/async"
	"github.com/skygeario/skygear-server/pkg/core/config"
	"github.com/skygeario/skygear-server/pkg/core/db"
	"github.com/skygeario/skygear-server/pkg/core/logging"
	"github.com/skygeario/skygear-server/pkg/core/middleware"
	"github.com/skygeario/skygear-server/pkg/core/redis"
	"github.com/skygeario/skygear-server/pkg/core/server"
	"github.com/skygeario/skygear-server/pkg/core/template"
)

type configuration struct {
	Standalone                        bool
	StandaloneTenantConfigurationFile string                      `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
	Host                              string                      `envconfig:"SERVER_HOST" default:"localhost:3000"`
	ValidHosts                        string                      `envconfig:"VALID_HOSTS"`
	Redis                             redis.Configuration         `envconfig:"REDIS"`
	UseInsecureCookie                 bool                        `envconfig:"INSECURE_COOKIE"`
	Default                           config.DefaultConfiguration `envconfig:"DEFAULT"`
}

/*
	@API Auth Gear
	@Version 1.0.0
	@Server {base_url}/_auth
		Auth Gear URL
		@Variable base_url https://my_app.skygearapis.com
			Skygear App URL

	@SecuritySchemeAPIKey access_key header X-Skygear-API-Key
		Access key used by client app
	@SecuritySchemeAPIKey master_key header X-Skygear-API-Key
		Master key used by admins, can perform administrative operations.
		Can be used as access key as well.
	@SecuritySchemeHTTP access_token Bearer JWT
		Access token of user
	@SecurityRequirement access_key

	@Tag User
		User information
	@Tag User Verification
		Login IDs verification
	@Tag Forgot Password
		Password recovery process
	@Tag Administration
		Administrative operation
	@Tag SSO
		Single sign-on
*/
func main() {
	// logging initialization
	logging.SetModule("auth")
	loggerFactory := logging.NewFactory(logging.NewDefaultLogHook(nil))
	logger := loggerFactory.NewLogger("auth")

	envErr := godotenv.Load()
	if envErr != nil {
		logger.WithError(envErr).Debug("Cannot load .env file")
	}

	configuration := configuration{}
	envconfig.Process("", &configuration)
	if configuration.ValidHosts == "" {
		configuration.ValidHosts = configuration.Host
	}

	// default template initialization
	templateEngine := template.NewEngine()
	authTemplate.RegisterDefaultTemplates(templateEngine)

	dbPool := db.NewPool()
	redisPool, err := redis.NewPool(configuration.Redis)
	if err != nil {
		logger.Fatalf("fail to create redis pool: %v", err.Error())
	}
	asyncTaskExecutor := async.NewExecutor(dbPool)
	authDependency := auth.DependencyMap{
		AsyncTaskExecutor:    asyncTaskExecutor,
		TemplateEngine:       templateEngine,
		UseInsecureCookie:    configuration.UseInsecureCookie,
		DefaultConfiguration: configuration.Default,
	}

	task.AttachVerifyCodeSendTask(asyncTaskExecutor, authDependency)
	task.AttachPwHousekeeperTask(asyncTaskExecutor, authDependency)
	task.AttachWelcomeEmailSendTask(asyncTaskExecutor, authDependency)

	var srv server.Server
	if configuration.Standalone {
		filename := configuration.StandaloneTenantConfigurationFile
		reader, err := os.Open(filename)
		if err != nil {
			logger.WithError(err).Error("Cannot open standalone config")
		}
		tenantConfig, err := config.NewTenantConfigurationFromYAML(reader)
		if err != nil {
			logger.WithError(err).Fatal("Cannot parse standalone config")
		}

		serverOption := server.DefaultOption()
		serverOption.GearPathPrefix = "/_auth"
		srv = server.NewServerWithOption(configuration.Host, authDependency, serverOption)
		srv.Use(middleware.WriteTenantConfigMiddleware{
			ConfigurationProvider: middleware.ConfigurationProviderFunc(func(_ *http.Request) (config.TenantConfiguration, error) {
				return *tenantConfig, nil
			}),
		}.Handle)
		srv.Use(middleware.ValidateHostMiddleware{ValidHosts: configuration.ValidHosts}.Handle)
		srv.Use(middleware.RequestIDMiddleware{}.Handle)
		srv.Use(middleware.CORSMiddleware{}.Handle)
	} else {
		srv = server.NewServer(configuration.Host, authDependency)
		srv.Use(middleware.ReadTenantConfigMiddleware{}.Handle)
	}

	srv.Use(middleware.DBMiddleware{Pool: dbPool}.Handle)
	srv.Use(middleware.RedisMiddleware{Pool: redisPool}.Handle)
	srv.Use(middleware.AuthMiddleware{}.Handle)

	srv.Use(middleware.Injecter{
		MiddlewareFactory: middleware.AuthnMiddlewareFactory{},
		Dependency:        authDependency,
	}.Handle)

	handler.AttachSignupHandler(&srv, authDependency)
	handler.AttachLoginHandler(&srv, authDependency)
	handler.AttachLogoutHandler(&srv, authDependency)
	handler.AttachRefreshHandler(&srv, authDependency)
	handler.AttachMeHandler(&srv, authDependency)
	handler.AttachSetDisableHandler(&srv, authDependency)
	handler.AttachChangePasswordHandler(&srv, authDependency)
	handler.AttachResetPasswordHandler(&srv, authDependency)
	handler.AttachWelcomeEmailHandler(&srv, authDependency)
	handler.AttachUpdateMetadataHandler(&srv, authDependency)
	forgotpwdhandler.AttachForgotPasswordHandler(&srv, authDependency)
	forgotpwdhandler.AttachForgotPasswordResetHandler(&srv, authDependency)
	userverifyhandler.AttachVerifyRequestHandler(&srv, authDependency)
	userverifyhandler.AttachVerifyCodeHandler(&srv, authDependency)
	ssohandler.AttachAuthURLHandler(&srv, authDependency)
	ssohandler.AttachConfigHandler(&srv, authDependency)
	ssohandler.AttachIFrameHandlerFactory(&srv, authDependency)
	ssohandler.AttachCustomTokenLoginHandler(&srv, authDependency)
	ssohandler.AttachAuthHandler(&srv, authDependency)
	ssohandler.AttachLoginHandler(&srv, authDependency)
	ssohandler.AttachLinkHandler(&srv, authDependency)
	ssohandler.AttachUnlinkHandler(&srv, authDependency)
	session.AttachListHandler(&srv, authDependency)
	session.AttachGetHandler(&srv, authDependency)
	session.AttachRevokeHandler(&srv, authDependency)
	session.AttachRevokeAllHandler(&srv, authDependency)
	mfaHandler.AttachListRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachRegenerateRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachListAuthenticatorHandler(&srv, authDependency)
	mfaHandler.AttachCreateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachActivateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachDeleteAuthenticatorHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateTOTPHandler(&srv, authDependency)
	mfaHandler.AttachRevokeAllBearerTokenHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateRecoveryCodeHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateBearerTokenHandler(&srv, authDependency)
	mfaHandler.AttachCreateOOBHandler(&srv, authDependency)
	mfaHandler.AttachTriggerOOBHandler(&srv, authDependency)
	mfaHandler.AttachActivateOOBHandler(&srv, authDependency)
	mfaHandler.AttachAuthenticateOOBHandler(&srv, authDependency)

	go func() {
		logger.Info("Starting auth gear")
		if err := srv.ListenAndServe(); err != nil {
			logger.WithError(err).Error("Cannot start HTTP server")
		}
	}()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	// wait interrupt signal
	select {
	case <-sig:
		logger.Info("Stopping HTTP server")
	}

	// create shutdown context with 10 second timeout
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// shutdown the server
	err = srv.Shutdown(ctx)
	if err != nil {
		logger.WithError(err).Fatal("Cannot shutdown server")
	}
}
<|endoftext|>


<commit_before>//  Copyright (c) 2014 Couchbase, 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 main

import (
	"expvar"
	"flag"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"runtime/pprof"
	"strings"
	"time"

	"github.com/gorilla/mux"

	"github.com/blevesearch/bleve"
	bleveHttp "github.com/blevesearch/bleve/http"
	bleveRegistry "github.com/blevesearch/bleve/registry"

	"github.com/couchbaselabs/cbft"
	log "github.com/couchbaselabs/clog"
	"github.com/couchbaselabs/go-couchbase"
)

var bindAddr = flag.String("addr", "localhost:8095",
	"http listen address:port")
var dataDir = flag.String("dataDir", "data",
	"directory for local configuration and index data")
var logFlags = flag.String("logFlags", "",
	"comma-separated clog flags, to control logging")
var staticDir = flag.String("staticDir", "static",
	"directory for static web UI content")
var staticETag = flag.String("staticETag", "",
	"static etag value")
var server = flag.String("server", "",
	"url to datasource server; couchbase example: http://localhost:8091")
var tags = flag.String("tags", "",
	"comma-separated list of tags (or roles) for this node")
var container = flag.String("container", "",
	"slash separated path of parent containers for this node,"+
		" for shelf/rack/row/zone awareness")
var weight = flag.Int("weight", 1,
	"weight of this node (a more capable node has higher weight)")
var register = flag.String("register", "wanted",
	"register this node as wanted, wantedForce, known, knownForce,"+
		" unwanted, unknown or unchanged")
var cfgConnect = flag.String("cfgConnect", "simple",
	"connection string/info to configuration provider")

var expvars = expvar.NewMap("stats")

func init() {
	expvars.Set("indexes", bleveHttp.IndexStats())
}

func main() {
	flag.Parse()

	rand.Seed(time.Now().UTC().UnixNano())

	go dumpOnSignalForPlatform()

	mr, err := cbft.NewMsgRing(os.Stderr, 1000)
	if err != nil {
		log.Fatalf("main: could not create MsgRing, err: %v", err)
	}
	log.SetOutput(mr)

	MainWelcome()

	// TODO: If cfg goes down, should we stop?  How do we reconnect?
	//
	cfg, err := MainCfg(*cfgConnect, *dataDir)
	if err != nil {
		log.Fatalf("main: could not start cfg, cfgConnect: %s, err: %v",
			*cfgConnect, err)
		return
	}

	uuid, err := MainUUID(*dataDir)
	if err != nil {
		log.Fatalf(fmt.Sprintf("%v", err))
		return
	}

	var tagsArr []string
	if *tags != "" {
		tagsArr = strings.Split(*tags, ",")
	}

	router, err := MainStart(cfg, uuid, tagsArr, *container, *weight,
		*bindAddr, *dataDir, *staticDir, *staticETag, *server, *register, mr)
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/", router)
	log.Printf("main: listening on: %v", *bindAddr)
	log.Fatal(http.ListenAndServe(*bindAddr, nil))
}

func MainWelcome() {
	log.Printf("main: %s started", os.Args[0])
	if *logFlags != "" {
		log.ParseLogFlag(*logFlags)
	}
	flag.VisitAll(func(f *flag.Flag) { log.Printf("  -%s=%s\n", f.Name, f.Value) })
	log.Printf("  GOMAXPROCS=%d", runtime.GOMAXPROCS(-1))

	log.Printf("main: registered bleve stores")
	types, instances := bleveRegistry.KVStoreTypesAndInstances()
	for _, s := range types {
		log.Printf("  %s", s)
	}
	for _, s := range instances {
		log.Printf("  %s", s)
	}
}

func MainUUID(dataDir string) (string, error) {
	uuid := cbft.NewUUID()
	uuidPath := dataDir + string(os.PathSeparator) + "cbft.uuid"
	uuidBuf, err := ioutil.ReadFile(uuidPath)
	if err == nil {
		uuid = strings.TrimSpace(string(uuidBuf))
		if uuid == "" {
			return "", fmt.Errorf("error: could not parse uuidPath: %s", uuidPath)
		}
		log.Printf("main: manager uuid: %s", uuid)
		log.Printf("main: manager uuid was reloaded")
	} else {
		log.Printf("main: manager uuid: %s", uuid)
		log.Printf("main: manager uuid was generated")
	}
	err = ioutil.WriteFile(uuidPath, []byte(uuid), 0600)
	if err != nil {
		return "", fmt.Errorf("error: could not write uuidPath: %s", uuidPath)
	}
	return uuid, nil
}

func MainStart(cfg cbft.Cfg, uuid string, tags []string, container string,
	weight int, bindAddr, dataDir, staticDir, staticETag, server string,
	register string, mr *cbft.MsgRing) (
	*mux.Router, error) {
	if server == "" {
		return nil, fmt.Errorf("error: server URL required (-server)")
	}

	_, err := couchbase.Connect(server)
	if err != nil {
		return nil, fmt.Errorf("error: could not connect to server URL: %s, err: %v",
			server, err)
	}

	mgr := cbft.NewManager(cbft.VERSION, cfg, uuid, tags, container, weight,
		bindAddr, dataDir, server, &MainHandlers{})
	err = mgr.Start(register)
	if err != nil {
		return nil, err
	}

	return cbft.NewManagerRESTRouter(mgr, staticDir, staticETag, mr)
}

type MainHandlers struct{}

func (meh *MainHandlers) OnRegisterPIndex(pindex *cbft.PIndex) {
	bindex, ok := pindex.Impl.(bleve.Index)
	if ok {
		bleveHttp.RegisterIndexName(pindex.Name, bindex)
	}
}

func (meh *MainHandlers) OnUnregisterPIndex(pindex *cbft.PIndex) {
	bleveHttp.UnregisterIndexByName(pindex.Name)
}

func dumpOnSignal(signals ...os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, signals...)
	for _ = range c {
		log.Printf("dump: goroutine...")
		pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
		log.Printf("dump: heap...")
		pprof.Lookup("heap").WriteTo(os.Stderr, 1)
	}
}
<commit_msg>emit version numbers on cbft startup<commit_after>//  Copyright (c) 2014 Couchbase, 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 main

import (
	"expvar"
	"flag"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"runtime/pprof"
	"strings"
	"time"

	"github.com/gorilla/mux"

	"github.com/blevesearch/bleve"
	bleveHttp "github.com/blevesearch/bleve/http"
	bleveRegistry "github.com/blevesearch/bleve/registry"

	"github.com/couchbaselabs/cbft"
	log "github.com/couchbaselabs/clog"
	"github.com/couchbaselabs/go-couchbase"
)

var VERSION = "0.0.0"

var bindAddr = flag.String("addr", "localhost:8095",
	"http listen address:port")
var dataDir = flag.String("dataDir", "data",
	"directory for local configuration and index data")
var logFlags = flag.String("logFlags", "",
	"comma-separated clog flags, to control logging")
var staticDir = flag.String("staticDir", "static",
	"directory for static web UI content")
var staticETag = flag.String("staticETag", "",
	"static etag value")
var server = flag.String("server", "",
	"url to datasource server; couchbase example: http://localhost:8091")
var tags = flag.String("tags", "",
	"comma-separated list of tags (or roles) for this node")
var container = flag.String("container", "",
	"slash separated path of parent containers for this node,"+
		" for shelf/rack/row/zone awareness")
var weight = flag.Int("weight", 1,
	"weight of this node (a more capable node has higher weight)")
var register = flag.String("register", "wanted",
	"register this node as wanted, wantedForce, known, knownForce,"+
		" unwanted, unknown or unchanged")
var cfgConnect = flag.String("cfgConnect", "simple",
	"connection string/info to configuration provider")

var expvars = expvar.NewMap("stats")

func init() {
	expvars.Set("indexes", bleveHttp.IndexStats())
}

func main() {
	log.Printf("main: %s started (%s/%s)", os.Args[0], VERSION, cbft.VERSION)

	flag.Parse()

	rand.Seed(time.Now().UTC().UnixNano())

	go dumpOnSignalForPlatform()

	mr, err := cbft.NewMsgRing(os.Stderr, 1000)
	if err != nil {
		log.Fatalf("main: could not create MsgRing, err: %v", err)
	}
	log.SetOutput(mr)

	MainWelcome()

	// TODO: If cfg goes down, should we stop?  How do we reconnect?
	//
	cfg, err := MainCfg(*cfgConnect, *dataDir)
	if err != nil {
		log.Fatalf("main: could not start cfg, cfgConnect: %s, err: %v",
			*cfgConnect, err)
		return
	}

	uuid, err := MainUUID(*dataDir)
	if err != nil {
		log.Fatalf(fmt.Sprintf("%v", err))
		return
	}

	var tagsArr []string
	if *tags != "" {
		tagsArr = strings.Split(*tags, ",")
	}

	router, err := MainStart(cfg, uuid, tagsArr, *container, *weight,
		*bindAddr, *dataDir, *staticDir, *staticETag, *server, *register, mr)
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/", router)
	log.Printf("main: listening on: %v", *bindAddr)
	log.Fatal(http.ListenAndServe(*bindAddr, nil))
}

func MainWelcome() {
	if *logFlags != "" {
		log.ParseLogFlag(*logFlags)
	}
	flag.VisitAll(func(f *flag.Flag) { log.Printf("  -%s=%s\n", f.Name, f.Value) })
	log.Printf("  GOMAXPROCS=%d", runtime.GOMAXPROCS(-1))

	log.Printf("main: registered bleve stores")
	types, instances := bleveRegistry.KVStoreTypesAndInstances()
	for _, s := range types {
		log.Printf("  %s", s)
	}
	for _, s := range instances {
		log.Printf("  %s", s)
	}
}

func MainUUID(dataDir string) (string, error) {
	uuid := cbft.NewUUID()
	uuidPath := dataDir + string(os.PathSeparator) + "cbft.uuid"
	uuidBuf, err := ioutil.ReadFile(uuidPath)
	if err == nil {
		uuid = strings.TrimSpace(string(uuidBuf))
		if uuid == "" {
			return "", fmt.Errorf("error: could not parse uuidPath: %s", uuidPath)
		}
		log.Printf("main: manager uuid: %s", uuid)
		log.Printf("main: manager uuid was reloaded")
	} else {
		log.Printf("main: manager uuid: %s", uuid)
		log.Printf("main: manager uuid was generated")
	}
	err = ioutil.WriteFile(uuidPath, []byte(uuid), 0600)
	if err != nil {
		return "", fmt.Errorf("error: could not write uuidPath: %s", uuidPath)
	}
	return uuid, nil
}

func MainStart(cfg cbft.Cfg, uuid string, tags []string, container string,
	weight int, bindAddr, dataDir, staticDir, staticETag, server string,
	register string, mr *cbft.MsgRing) (
	*mux.Router, error) {
	if server == "" {
		return nil, fmt.Errorf("error: server URL required (-server)")
	}

	_, err := couchbase.Connect(server)
	if err != nil {
		return nil, fmt.Errorf("error: could not connect to server URL: %s, err: %v",
			server, err)
	}

	mgr := cbft.NewManager(cbft.VERSION, cfg, uuid, tags, container, weight,
		bindAddr, dataDir, server, &MainHandlers{})
	err = mgr.Start(register)
	if err != nil {
		return nil, err
	}

	return cbft.NewManagerRESTRouter(mgr, staticDir, staticETag, mr)
}

type MainHandlers struct{}

func (meh *MainHandlers) OnRegisterPIndex(pindex *cbft.PIndex) {
	bindex, ok := pindex.Impl.(bleve.Index)
	if ok {
		bleveHttp.RegisterIndexName(pindex.Name, bindex)
	}
}

func (meh *MainHandlers) OnUnregisterPIndex(pindex *cbft.PIndex) {
	bleveHttp.UnregisterIndexByName(pindex.Name)
}

func dumpOnSignal(signals ...os.Signal) {
	c := make(chan os.Signal, 1)
	signal.Notify(c, signals...)
	for _ = range c {
		log.Printf("dump: goroutine...")
		pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
		log.Printf("dump: heap...")
		pprof.Lookup("heap").WriteTo(os.Stderr, 1)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/ghodss/yaml"
	grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"github.com/dexidp/dex/api"
	"github.com/dexidp/dex/pkg/log"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
)

func commandServe() *cobra.Command {
	return &cobra.Command{
		Use:     "serve [ config file ]",
		Short:   "Connect to the storage and begin serving requests.",
		Long:    ``,
		Example: "dex serve config.yaml",
		Run: func(cmd *cobra.Command, args []string) {
			if err := serve(cmd, args); err != nil {
				fmt.Fprintln(os.Stderr, err)
				os.Exit(2)
			}
		},
	}
}

func serve(cmd *cobra.Command, args []string) error {
	switch len(args) {
	default:
		return errors.New("surplus arguments")
	case 0:
		// TODO(ericchiang): Consider having a default config file location.
		return errors.New("no arguments provided")
	case 1:
	}

	configFile := args[0]
	configData, err := ioutil.ReadFile(configFile)
	if err != nil {
		return fmt.Errorf("failed to read config file %s: %v", configFile, err)
	}

	var c Config
	if err := yaml.Unmarshal(configData, &c); err != nil {
		return fmt.Errorf("error parse config file %s: %v", configFile, err)
	}

	logger, err := newLogger(c.Logger.Level, c.Logger.Format)
	if err != nil {
		return fmt.Errorf("invalid config: %v", err)
	}
	if c.Logger.Level != "" {
		logger.Infof("config using log level: %s", c.Logger.Level)
	}

	// Fast checks. Perform these first for a more responsive CLI.
	checks := []struct {
		bad    bool
		errMsg string
	}{
		{c.Issuer == "", "no issuer specified in config file"},
		{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
		{c.Storage.Config == nil, "no storage supplied in config file"},
		{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS  address to listen on"},
		{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
		{c.Web.HTTPS != "" && c.Web.TLSKey == "", "no private key specified for HTTPS"},
		{c.GRPC.TLSCert != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
		{c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
		{(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"},
		{c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"},
	}

	for _, check := range checks {
		if check.bad {
			return fmt.Errorf("invalid config: %s", check.errMsg)
		}
	}

	logger.Infof("config issuer: %s", c.Issuer)

	prometheusRegistry := prometheus.NewRegistry()
	err = prometheusRegistry.Register(prometheus.NewGoCollector())
	if err != nil {
		return fmt.Errorf("failed to register Go runtime metrics: %v", err)
	}

	err = prometheusRegistry.Register(prometheus.NewProcessCollector(os.Getpid(), ""))
	if err != nil {
		return fmt.Errorf("failed to register process metrics: %v", err)
	}

	grpcMetrics := grpcprometheus.NewServerMetrics()
	err = prometheusRegistry.Register(grpcMetrics)
	if err != nil {
		return fmt.Errorf("failed to register gRPC server metrics: %v", err)
	}

	var grpcOptions []grpc.ServerOption

	if c.GRPC.TLSCert != "" {
		// Parse certificates from certificate file and key file for server.
		cert, err := tls.LoadX509KeyPair(c.GRPC.TLSCert, c.GRPC.TLSKey)
		if err != nil {
			return fmt.Errorf("invalid config: error parsing gRPC certificate file: %v", err)
		}

		tlsConfig := tls.Config{
			Certificates:             []tls.Certificate{cert},
			MinVersion:               tls.VersionTLS12,
			PreferServerCipherSuites: true,
		}

		if c.GRPC.TLSClientCA != "" {
			// Parse certificates from client CA file to a new CertPool.
			cPool := x509.NewCertPool()
			clientCert, err := ioutil.ReadFile(c.GRPC.TLSClientCA)
			if err != nil {
				return fmt.Errorf("invalid config: reading from client CA file: %v", err)
			}
			if cPool.AppendCertsFromPEM(clientCert) != true {
				return errors.New("invalid config: failed to parse client CA")
			}

			tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
			tlsConfig.ClientCAs = cPool

			// Only add metrics if client auth is enabled
			grpcOptions = append(grpcOptions,
				grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()),
				grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()),
			)
		}

		grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(&tlsConfig)))
	}

	s, err := c.Storage.Config.Open(logger)
	if err != nil {
		return fmt.Errorf("failed to initialize storage: %v", err)
	}
	logger.Infof("config storage: %s", c.Storage.Type)

	if len(c.StaticClients) > 0 {
		for _, client := range c.StaticClients {
			logger.Infof("config static client: %s", client.ID)
		}
		s = storage.WithStaticClients(s, c.StaticClients)
	}
	if len(c.StaticPasswords) > 0 {
		passwords := make([]storage.Password, len(c.StaticPasswords))
		for i, p := range c.StaticPasswords {
			passwords[i] = storage.Password(p)
		}
		s = storage.WithStaticPasswords(s, passwords, logger)
	}

	storageConnectors := make([]storage.Connector, len(c.StaticConnectors))
	for i, c := range c.StaticConnectors {
		if c.ID == "" || c.Name == "" || c.Type == "" {
			return fmt.Errorf("invalid config: ID, Type and Name fields are required for a connector")
		}
		if c.Config == nil {
			return fmt.Errorf("invalid config: no config field for connector %q", c.ID)
		}
		logger.Infof("config connector: %s", c.ID)

		// convert to a storage connector object
		conn, err := ToStorageConnector(c)
		if err != nil {
			return fmt.Errorf("failed to initialize storage connectors: %v", err)
		}
		storageConnectors[i] = conn

	}

	if c.EnablePasswordDB {
		storageConnectors = append(storageConnectors, storage.Connector{
			ID:   server.LocalConnector,
			Name: "Email",
			Type: server.LocalConnector,
		})
		logger.Infof("config connector: local passwords enabled")
	}

	s = storage.WithStaticConnectors(s, storageConnectors)

	if len(c.OAuth2.ResponseTypes) > 0 {
		logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
	}
	if c.OAuth2.SkipApprovalScreen {
		logger.Infof("config skipping approval screen")
	}
	if len(c.Web.AllowedOrigins) > 0 {
		logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
	}

	// explicitly convert to UTC.
	now := func() time.Time { return time.Now().UTC() }

	serverConfig := server.Config{
		SupportedResponseTypes: c.OAuth2.ResponseTypes,
		SkipApprovalScreen:     c.OAuth2.SkipApprovalScreen,
		AllowedOrigins:         c.Web.AllowedOrigins,
		Issuer:                 c.Issuer,
		Storage:                s,
		Web:                    c.Frontend,
		Logger:                 logger,
		Now:                    now,
		PrometheusRegistry:     prometheusRegistry,
	}
	if c.Expiry.SigningKeys != "" {
		signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys)
		if err != nil {
			return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
		}
		logger.Infof("config signing keys expire after: %v", signingKeys)
		serverConfig.RotateKeysAfter = signingKeys
	}
	if c.Expiry.IDTokens != "" {
		idTokens, err := time.ParseDuration(c.Expiry.IDTokens)
		if err != nil {
			return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
		}
		logger.Infof("config id tokens valid for: %v", idTokens)
		serverConfig.IDTokensValidFor = idTokens
	}
	if c.Expiry.AuthRequests != "" {
		authRequests, err := time.ParseDuration(c.Expiry.AuthRequests)
		if err != nil {
			return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err)
		}
		logger.Infof("config auth requests valid for: %v", authRequests)
		serverConfig.AuthRequestsValidFor = authRequests
	}

	serv, err := server.NewServer(context.Background(), serverConfig)
	if err != nil {
		return fmt.Errorf("failed to initialize server: %v", err)
	}

	telemetryServ := http.NewServeMux()
	telemetryServ.Handle("/metrics", promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}))

	errc := make(chan error, 3)
	if c.Telemetry.HTTP != "" {
		logger.Infof("listening (http/telemetry) on %s", c.Telemetry.HTTP)
		go func() {
			err := http.ListenAndServe(c.Telemetry.HTTP, telemetryServ)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Telemetry.HTTP, err)
		}()
	}
	if c.Web.HTTP != "" {
		logger.Infof("listening (http) on %s", c.Web.HTTP)
		go func() {
			err := http.ListenAndServe(c.Web.HTTP, serv)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Web.HTTP, err)
		}()
	}
	if c.Web.HTTPS != "" {
		httpsSrv := &http.Server{
			Addr:    c.Web.HTTPS,
			Handler: serv,
			TLSConfig: &tls.Config{
				PreferServerCipherSuites: true,
				MinVersion:               tls.VersionTLS12,
			},
		}

		logger.Infof("listening (https) on %s", c.Web.HTTPS)
		go func() {
			err = httpsSrv.ListenAndServeTLS(c.Web.TLSCert, c.Web.TLSKey)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Web.HTTPS, err)
		}()
	}
	if c.GRPC.Addr != "" {
		logger.Infof("listening (grpc) on %s", c.GRPC.Addr)
		go func() {
			errc <- func() error {
				list, err := net.Listen("tcp", c.GRPC.Addr)
				if err != nil {
					return fmt.Errorf("listening on %s failed: %v", c.GRPC.Addr, err)
				}
				s := grpc.NewServer(grpcOptions...)
				api.RegisterDexServer(s, server.NewAPI(serverConfig.Storage, logger))
				grpcMetrics.InitializeMetrics(s)
				err = s.Serve(list)
				return fmt.Errorf("listening on %s failed: %v", c.GRPC.Addr, err)
			}()
		}()
	}

	return <-errc
}

var (
	logLevels  = []string{"debug", "info", "error"}
	logFormats = []string{"json", "text"}
)

type utcFormatter struct {
	f logrus.Formatter
}

func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) {
	e.Time = e.Time.UTC()
	return f.f.Format(e)
}

func newLogger(level string, format string) (log.Logger, error) {
	var logLevel logrus.Level
	switch strings.ToLower(level) {
	case "debug":
		logLevel = logrus.DebugLevel
	case "", "info":
		logLevel = logrus.InfoLevel
	case "error":
		logLevel = logrus.ErrorLevel
	default:
		return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level)
	}

	var formatter utcFormatter
	switch strings.ToLower(format) {
	case "", "text":
		formatter.f = &logrus.TextFormatter{DisableColors: true}
	case "json":
		formatter.f = &logrus.JSONFormatter{}
	default:
		return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format)
	}

	return &logrus.Logger{
		Out:       os.Stderr,
		Formatter: &formatter,
		Level:     logLevel,
	}, nil
}
<commit_msg>cmd/dex/serve.go: log static client name instead of ID<commit_after>package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/ghodss/yaml"
	grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"github.com/dexidp/dex/api"
	"github.com/dexidp/dex/pkg/log"
	"github.com/dexidp/dex/server"
	"github.com/dexidp/dex/storage"
)

func commandServe() *cobra.Command {
	return &cobra.Command{
		Use:     "serve [ config file ]",
		Short:   "Connect to the storage and begin serving requests.",
		Long:    ``,
		Example: "dex serve config.yaml",
		Run: func(cmd *cobra.Command, args []string) {
			if err := serve(cmd, args); err != nil {
				fmt.Fprintln(os.Stderr, err)
				os.Exit(2)
			}
		},
	}
}

func serve(cmd *cobra.Command, args []string) error {
	switch len(args) {
	default:
		return errors.New("surplus arguments")
	case 0:
		// TODO(ericchiang): Consider having a default config file location.
		return errors.New("no arguments provided")
	case 1:
	}

	configFile := args[0]
	configData, err := ioutil.ReadFile(configFile)
	if err != nil {
		return fmt.Errorf("failed to read config file %s: %v", configFile, err)
	}

	var c Config
	if err := yaml.Unmarshal(configData, &c); err != nil {
		return fmt.Errorf("error parse config file %s: %v", configFile, err)
	}

	logger, err := newLogger(c.Logger.Level, c.Logger.Format)
	if err != nil {
		return fmt.Errorf("invalid config: %v", err)
	}
	if c.Logger.Level != "" {
		logger.Infof("config using log level: %s", c.Logger.Level)
	}

	// Fast checks. Perform these first for a more responsive CLI.
	checks := []struct {
		bad    bool
		errMsg string
	}{
		{c.Issuer == "", "no issuer specified in config file"},
		{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
		{c.Storage.Config == nil, "no storage supplied in config file"},
		{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS  address to listen on"},
		{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
		{c.Web.HTTPS != "" && c.Web.TLSKey == "", "no private key specified for HTTPS"},
		{c.GRPC.TLSCert != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
		{c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
		{(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"},
		{c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"},
	}

	for _, check := range checks {
		if check.bad {
			return fmt.Errorf("invalid config: %s", check.errMsg)
		}
	}

	logger.Infof("config issuer: %s", c.Issuer)

	prometheusRegistry := prometheus.NewRegistry()
	err = prometheusRegistry.Register(prometheus.NewGoCollector())
	if err != nil {
		return fmt.Errorf("failed to register Go runtime metrics: %v", err)
	}

	err = prometheusRegistry.Register(prometheus.NewProcessCollector(os.Getpid(), ""))
	if err != nil {
		return fmt.Errorf("failed to register process metrics: %v", err)
	}

	grpcMetrics := grpcprometheus.NewServerMetrics()
	err = prometheusRegistry.Register(grpcMetrics)
	if err != nil {
		return fmt.Errorf("failed to register gRPC server metrics: %v", err)
	}

	var grpcOptions []grpc.ServerOption

	if c.GRPC.TLSCert != "" {
		// Parse certificates from certificate file and key file for server.
		cert, err := tls.LoadX509KeyPair(c.GRPC.TLSCert, c.GRPC.TLSKey)
		if err != nil {
			return fmt.Errorf("invalid config: error parsing gRPC certificate file: %v", err)
		}

		tlsConfig := tls.Config{
			Certificates:             []tls.Certificate{cert},
			MinVersion:               tls.VersionTLS12,
			PreferServerCipherSuites: true,
		}

		if c.GRPC.TLSClientCA != "" {
			// Parse certificates from client CA file to a new CertPool.
			cPool := x509.NewCertPool()
			clientCert, err := ioutil.ReadFile(c.GRPC.TLSClientCA)
			if err != nil {
				return fmt.Errorf("invalid config: reading from client CA file: %v", err)
			}
			if cPool.AppendCertsFromPEM(clientCert) != true {
				return errors.New("invalid config: failed to parse client CA")
			}

			tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
			tlsConfig.ClientCAs = cPool

			// Only add metrics if client auth is enabled
			grpcOptions = append(grpcOptions,
				grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()),
				grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()),
			)
		}

		grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(&tlsConfig)))
	}

	s, err := c.Storage.Config.Open(logger)
	if err != nil {
		return fmt.Errorf("failed to initialize storage: %v", err)
	}
	logger.Infof("config storage: %s", c.Storage.Type)

	if len(c.StaticClients) > 0 {
		for _, client := range c.StaticClients {
			logger.Infof("config static client: %s", client.Name)
		}
		s = storage.WithStaticClients(s, c.StaticClients)
	}
	if len(c.StaticPasswords) > 0 {
		passwords := make([]storage.Password, len(c.StaticPasswords))
		for i, p := range c.StaticPasswords {
			passwords[i] = storage.Password(p)
		}
		s = storage.WithStaticPasswords(s, passwords, logger)
	}

	storageConnectors := make([]storage.Connector, len(c.StaticConnectors))
	for i, c := range c.StaticConnectors {
		if c.ID == "" || c.Name == "" || c.Type == "" {
			return fmt.Errorf("invalid config: ID, Type and Name fields are required for a connector")
		}
		if c.Config == nil {
			return fmt.Errorf("invalid config: no config field for connector %q", c.ID)
		}
		logger.Infof("config connector: %s", c.ID)

		// convert to a storage connector object
		conn, err := ToStorageConnector(c)
		if err != nil {
			return fmt.Errorf("failed to initialize storage connectors: %v", err)
		}
		storageConnectors[i] = conn

	}

	if c.EnablePasswordDB {
		storageConnectors = append(storageConnectors, storage.Connector{
			ID:   server.LocalConnector,
			Name: "Email",
			Type: server.LocalConnector,
		})
		logger.Infof("config connector: local passwords enabled")
	}

	s = storage.WithStaticConnectors(s, storageConnectors)

	if len(c.OAuth2.ResponseTypes) > 0 {
		logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
	}
	if c.OAuth2.SkipApprovalScreen {
		logger.Infof("config skipping approval screen")
	}
	if len(c.Web.AllowedOrigins) > 0 {
		logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
	}

	// explicitly convert to UTC.
	now := func() time.Time { return time.Now().UTC() }

	serverConfig := server.Config{
		SupportedResponseTypes: c.OAuth2.ResponseTypes,
		SkipApprovalScreen:     c.OAuth2.SkipApprovalScreen,
		AllowedOrigins:         c.Web.AllowedOrigins,
		Issuer:                 c.Issuer,
		Storage:                s,
		Web:                    c.Frontend,
		Logger:                 logger,
		Now:                    now,
		PrometheusRegistry:     prometheusRegistry,
	}
	if c.Expiry.SigningKeys != "" {
		signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys)
		if err != nil {
			return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
		}
		logger.Infof("config signing keys expire after: %v", signingKeys)
		serverConfig.RotateKeysAfter = signingKeys
	}
	if c.Expiry.IDTokens != "" {
		idTokens, err := time.ParseDuration(c.Expiry.IDTokens)
		if err != nil {
			return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
		}
		logger.Infof("config id tokens valid for: %v", idTokens)
		serverConfig.IDTokensValidFor = idTokens
	}
	if c.Expiry.AuthRequests != "" {
		authRequests, err := time.ParseDuration(c.Expiry.AuthRequests)
		if err != nil {
			return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err)
		}
		logger.Infof("config auth requests valid for: %v", authRequests)
		serverConfig.AuthRequestsValidFor = authRequests
	}

	serv, err := server.NewServer(context.Background(), serverConfig)
	if err != nil {
		return fmt.Errorf("failed to initialize server: %v", err)
	}

	telemetryServ := http.NewServeMux()
	telemetryServ.Handle("/metrics", promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}))

	errc := make(chan error, 3)
	if c.Telemetry.HTTP != "" {
		logger.Infof("listening (http/telemetry) on %s", c.Telemetry.HTTP)
		go func() {
			err := http.ListenAndServe(c.Telemetry.HTTP, telemetryServ)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Telemetry.HTTP, err)
		}()
	}
	if c.Web.HTTP != "" {
		logger.Infof("listening (http) on %s", c.Web.HTTP)
		go func() {
			err := http.ListenAndServe(c.Web.HTTP, serv)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Web.HTTP, err)
		}()
	}
	if c.Web.HTTPS != "" {
		httpsSrv := &http.Server{
			Addr:    c.Web.HTTPS,
			Handler: serv,
			TLSConfig: &tls.Config{
				PreferServerCipherSuites: true,
				MinVersion:               tls.VersionTLS12,
			},
		}

		logger.Infof("listening (https) on %s", c.Web.HTTPS)
		go func() {
			err = httpsSrv.ListenAndServeTLS(c.Web.TLSCert, c.Web.TLSKey)
			errc <- fmt.Errorf("listening on %s failed: %v", c.Web.HTTPS, err)
		}()
	}
	if c.GRPC.Addr != "" {
		logger.Infof("listening (grpc) on %s", c.GRPC.Addr)
		go func() {
			errc <- func() error {
				list, err := net.Listen("tcp", c.GRPC.Addr)
				if err != nil {
					return fmt.Errorf("listening on %s failed: %v", c.GRPC.Addr, err)
				}
				s := grpc.NewServer(grpcOptions...)
				api.RegisterDexServer(s, server.NewAPI(serverConfig.Storage, logger))
				grpcMetrics.InitializeMetrics(s)
				err = s.Serve(list)
				return fmt.Errorf("listening on %s failed: %v", c.GRPC.Addr, err)
			}()
		}()
	}

	return <-errc
}

var (
	logLevels  = []string{"debug", "info", "error"}
	logFormats = []string{"json", "text"}
)

type utcFormatter struct {
	f logrus.Formatter
}

func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) {
	e.Time = e.Time.UTC()
	return f.f.Format(e)
}

func newLogger(level string, format string) (log.Logger, error) {
	var logLevel logrus.Level
	switch strings.ToLower(level) {
	case "debug":
		logLevel = logrus.DebugLevel
	case "", "info":
		logLevel = logrus.InfoLevel
	case "error":
		logLevel = logrus.ErrorLevel
	default:
		return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level)
	}

	var formatter utcFormatter
	switch strings.ToLower(format) {
	case "", "text":
		formatter.f = &logrus.TextFormatter{DisableColors: true}
	case "json":
		formatter.f = &logrus.JSONFormatter{}
	default:
		return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format)
	}

	return &logrus.Logger{
		Out:       os.Stderr,
		Formatter: &formatter,
		Level:     logLevel,
	}, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Kubernetes 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 main

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"

	"github.com/ghodss/yaml"
	"github.com/gosuri/uitable"
	"github.com/spf13/cobra"

	"k8s.io/helm/pkg/helm"
	"k8s.io/helm/pkg/proto/hapi/release"
	"k8s.io/helm/pkg/proto/hapi/services"
	"k8s.io/helm/pkg/timeconv"
)

var listHelp = `
This command lists all of the releases.

By default, it lists only releases that are deployed or failed. Flags like
'--deleted' and '--all' will alter this behavior. Such flags can be combined:
'--deleted --failed'.

By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date.

If an argument is provided, it will be treated as a filter. Filters are
regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned.

	$ helm list 'ara[a-z]+'
	NAME            	UPDATED                 	CHART
	maudlin-arachnid	Mon May  9 16:07:08 2016	alpine-0.1.0

If no results are found, 'helm list' will exit 0, but with no output (or in
the case of no '-q' flag, only headers).

By default, up to 256 items may be returned. To limit this, use the '--max' flag.
Setting '--max' to 0 will not return all results. Rather, it will return the
server's default, which may be much higher than 256. Pairing the '--max'
flag with the '--offset' flag allows you to page through results.
`

type listCmd struct {
	filter     string
	short      bool
	limit      int
	offset     string
	byDate     bool
	sortDesc   bool
	out        io.Writer
	all        bool
	deleted    bool
	deleting   bool
	deployed   bool
	failed     bool
	namespace  string
	superseded bool
	pending    bool
	client     helm.Interface
	colWidth   uint
	output     string
}

type listResult struct {
	Next     string
	Releases []listRelease
}

type listRelease struct {
	Name       string
	Revision   int32
	Updated    string
	Status     string
	Chart      string
	AppVersion string
	Namespace  string
}

func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
	list := &listCmd{
		out:    out,
		client: client,
	}

	cmd := &cobra.Command{
		Use:     "list [flags] [FILTER]",
		Short:   "list releases",
		Long:    listHelp,
		Aliases: []string{"ls"},
		PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() },
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) > 0 {
				list.filter = strings.Join(args, " ")
			}
			if list.client == nil {
				list.client = newClient()
			}
			return list.run()
		},
	}

	f := cmd.Flags()
	f.BoolVarP(&list.short, "short", "q", false, "output short (quiet) listing format")
	f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date")
	f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order")
	f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch")
	f.StringVarP(&list.offset, "offset", "o", "", "next release name in the list, used to offset from start value")
	f.BoolVarP(&list.all, "all", "a", false, "show all releases, not just the ones marked DEPLOYED")
	f.BoolVar(&list.deleted, "deleted", false, "show deleted releases")
	f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted")
	f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
	f.BoolVar(&list.failed, "failed", false, "show failed releases")
	f.BoolVar(&list.pending, "pending", false, "show pending releases")
	f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace")
	f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output")
	f.StringVar(&list.output, "output", "", "output the specified format (json or yaml)")

	// TODO: Do we want this as a feature of 'helm list'?
	//f.BoolVar(&list.superseded, "history", true, "show historical releases")

	return cmd
}

func (l *listCmd) run() error {
	sortBy := services.ListSort_NAME
	if l.byDate {
		sortBy = services.ListSort_LAST_RELEASED
	}

	sortOrder := services.ListSort_ASC
	if l.sortDesc {
		sortOrder = services.ListSort_DESC
	}

	stats := l.statusCodes()

	res, err := l.client.ListReleases(
		helm.ReleaseListLimit(l.limit),
		helm.ReleaseListOffset(l.offset),
		helm.ReleaseListFilter(l.filter),
		helm.ReleaseListSort(int32(sortBy)),
		helm.ReleaseListOrder(int32(sortOrder)),
		helm.ReleaseListStatuses(stats),
		helm.ReleaseListNamespace(l.namespace),
	)

	if err != nil {
		return prettyError(err)
	}

	rels := filterList(res.Releases)

	result := getListResult(rels, res.Next)

	output, err := formatResult(l.output, l.short, result, l.colWidth)

	if err != nil {
		return prettyError(err)
	}

	fmt.Fprintln(l.out, output)
	return nil
}

// filterList returns a list scrubbed of old releases.
func filterList(rels []*release.Release) []*release.Release {
	idx := map[string]int32{}

	for _, r := range rels {
		name, version := r.GetName(), r.GetVersion()
		if max, ok := idx[name]; ok {
			// check if we have a greater version already
			if max > version {
				continue
			}
		}
		idx[name] = version
	}

	uniq := make([]*release.Release, 0, len(idx))
	for _, r := range rels {
		if idx[r.GetName()] == r.GetVersion() {
			uniq = append(uniq, r)
		}
	}
	return uniq
}

// statusCodes gets the list of status codes that are to be included in the results.
func (l *listCmd) statusCodes() []release.Status_Code {
	if l.all {
		return []release.Status_Code{
			release.Status_UNKNOWN,
			release.Status_DEPLOYED,
			release.Status_DELETED,
			release.Status_DELETING,
			release.Status_FAILED,
			release.Status_PENDING_INSTALL,
			release.Status_PENDING_UPGRADE,
			release.Status_PENDING_ROLLBACK,
		}
	}
	status := []release.Status_Code{}
	if l.deployed {
		status = append(status, release.Status_DEPLOYED)
	}
	if l.deleted {
		status = append(status, release.Status_DELETED)
	}
	if l.deleting {
		status = append(status, release.Status_DELETING)
	}
	if l.failed {
		status = append(status, release.Status_FAILED)
	}
	if l.superseded {
		status = append(status, release.Status_SUPERSEDED)
	}
	if l.pending {
		status = append(status, release.Status_PENDING_INSTALL, release.Status_PENDING_UPGRADE, release.Status_PENDING_ROLLBACK)
	}

	// Default case.
	if len(status) == 0 {
		status = append(status, release.Status_DEPLOYED, release.Status_FAILED)
	}
	return status
}

func getListResult(rels []*release.Release, next string) listResult {
	listReleases := []listRelease{}
	for _, r := range rels {
		md := r.GetChart().GetMetadata()
		t := "-"
		if tspb := r.GetInfo().GetLastDeployed(); tspb != nil {
			t = timeconv.String(tspb)
		}

		lr := listRelease{
			Name:       r.GetName(),
			Revision:   r.GetVersion(),
			Updated:    t,
			Status:     r.GetInfo().GetStatus().GetCode().String(),
			Chart:      fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion()),
			AppVersion: md.GetAppVersion(),
			Namespace:  r.GetNamespace(),
		}
		listReleases = append(listReleases, lr)
	}

	return listResult{
		Releases: listReleases,
		Next:     next,
	}
}

func shortenListResult(result listResult) []string {
	names := []string{}
	for _, r := range result.Releases {
		names = append(names, r.Name)
	}

	return names
}

func formatResult(format string, short bool, result listResult, colWidth uint) (string, error) {
	var output string
	var err error

	var shortResult []string
	var finalResult interface{}
	if short {
		shortResult = shortenListResult(result)
		finalResult = shortResult
	} else {
		finalResult = result
	}

	switch format {
	case "":
		if short {
			output = formatTextShort(shortResult)
		} else {
			output = formatText(result, colWidth)
		}
	case "json":
		o, e := json.Marshal(finalResult)
		if e != nil {
			err = fmt.Errorf("Failed to Marshal JSON output: %s", e)
		} else {
			output = string(o)
		}
	case "yaml":
		o, e := yaml.Marshal(finalResult)
		if e != nil {
			err = fmt.Errorf("Failed to Marshal YAML output: %s", e)
		} else {
			output = string(o)
		}
	default:
		err = fmt.Errorf("Unknown output format \"%s\"", format)
	}
	return output, err
}

func formatText(result listResult, colWidth uint) string {
	nextOutput := ""
	if result.Next != "" {
		nextOutput = fmt.Sprintf("\tnext: %s\n", result.Next)
	}

	table := uitable.New()
	table.MaxColWidth = colWidth
	table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
	for _, lr := range result.Releases {
		table.AddRow(lr.Name, lr.Revision, lr.Updated, lr.Status, lr.Chart, lr.AppVersion, lr.Namespace)
	}

	return fmt.Sprintf("%s%s", nextOutput, table.String())
}

func formatTextShort(shortResult []string) string {
	return strings.Join(shortResult, "\n")
}
<commit_msg>fix(helm): return when listing with no releases or nil<commit_after>/*
Copyright 2016 The Kubernetes 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 main

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"

	"github.com/ghodss/yaml"
	"github.com/gosuri/uitable"
	"github.com/spf13/cobra"

	"k8s.io/helm/pkg/helm"
	"k8s.io/helm/pkg/proto/hapi/release"
	"k8s.io/helm/pkg/proto/hapi/services"
	"k8s.io/helm/pkg/timeconv"
)

var listHelp = `
This command lists all of the releases.

By default, it lists only releases that are deployed or failed. Flags like
'--deleted' and '--all' will alter this behavior. Such flags can be combined:
'--deleted --failed'.

By default, items are sorted alphabetically. Use the '-d' flag to sort by
release date.

If an argument is provided, it will be treated as a filter. Filters are
regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned.

	$ helm list 'ara[a-z]+'
	NAME            	UPDATED                 	CHART
	maudlin-arachnid	Mon May  9 16:07:08 2016	alpine-0.1.0

If no results are found, 'helm list' will exit 0, but with no output (or in
the case of no '-q' flag, only headers).

By default, up to 256 items may be returned. To limit this, use the '--max' flag.
Setting '--max' to 0 will not return all results. Rather, it will return the
server's default, which may be much higher than 256. Pairing the '--max'
flag with the '--offset' flag allows you to page through results.
`

type listCmd struct {
	filter     string
	short      bool
	limit      int
	offset     string
	byDate     bool
	sortDesc   bool
	out        io.Writer
	all        bool
	deleted    bool
	deleting   bool
	deployed   bool
	failed     bool
	namespace  string
	superseded bool
	pending    bool
	client     helm.Interface
	colWidth   uint
	output     string
}

type listResult struct {
	Next     string
	Releases []listRelease
}

type listRelease struct {
	Name       string
	Revision   int32
	Updated    string
	Status     string
	Chart      string
	AppVersion string
	Namespace  string
}

func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
	list := &listCmd{
		out:    out,
		client: client,
	}

	cmd := &cobra.Command{
		Use:     "list [flags] [FILTER]",
		Short:   "list releases",
		Long:    listHelp,
		Aliases: []string{"ls"},
		PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() },
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) > 0 {
				list.filter = strings.Join(args, " ")
			}
			if list.client == nil {
				list.client = newClient()
			}
			return list.run()
		},
	}

	f := cmd.Flags()
	f.BoolVarP(&list.short, "short", "q", false, "output short (quiet) listing format")
	f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date")
	f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order")
	f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch")
	f.StringVarP(&list.offset, "offset", "o", "", "next release name in the list, used to offset from start value")
	f.BoolVarP(&list.all, "all", "a", false, "show all releases, not just the ones marked DEPLOYED")
	f.BoolVar(&list.deleted, "deleted", false, "show deleted releases")
	f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted")
	f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled")
	f.BoolVar(&list.failed, "failed", false, "show failed releases")
	f.BoolVar(&list.pending, "pending", false, "show pending releases")
	f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace")
	f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output")
	f.StringVar(&list.output, "output", "", "output the specified format (json or yaml)")

	// TODO: Do we want this as a feature of 'helm list'?
	//f.BoolVar(&list.superseded, "history", true, "show historical releases")

	return cmd
}

func (l *listCmd) run() error {
	sortBy := services.ListSort_NAME
	if l.byDate {
		sortBy = services.ListSort_LAST_RELEASED
	}

	sortOrder := services.ListSort_ASC
	if l.sortDesc {
		sortOrder = services.ListSort_DESC
	}

	stats := l.statusCodes()

	res, err := l.client.ListReleases(
		helm.ReleaseListLimit(l.limit),
		helm.ReleaseListOffset(l.offset),
		helm.ReleaseListFilter(l.filter),
		helm.ReleaseListSort(int32(sortBy)),
		helm.ReleaseListOrder(int32(sortOrder)),
		helm.ReleaseListStatuses(stats),
		helm.ReleaseListNamespace(l.namespace),
	)

	if err != nil {
		return prettyError(err)
	}
	if res == nil {
		return nil
	}

	rels := filterList(res.GetReleases())

	result := getListResult(rels, res.Next)

	output, err := formatResult(l.output, l.short, result, l.colWidth)

	if err != nil {
		return prettyError(err)
	}

	fmt.Fprintln(l.out, output)
	return nil
}

// filterList returns a list scrubbed of old releases.
func filterList(rels []*release.Release) []*release.Release {
	idx := map[string]int32{}

	for _, r := range rels {
		name, version := r.GetName(), r.GetVersion()
		if max, ok := idx[name]; ok {
			// check if we have a greater version already
			if max > version {
				continue
			}
		}
		idx[name] = version
	}

	uniq := make([]*release.Release, 0, len(idx))
	for _, r := range rels {
		if idx[r.GetName()] == r.GetVersion() {
			uniq = append(uniq, r)
		}
	}
	return uniq
}

// statusCodes gets the list of status codes that are to be included in the results.
func (l *listCmd) statusCodes() []release.Status_Code {
	if l.all {
		return []release.Status_Code{
			release.Status_UNKNOWN,
			release.Status_DEPLOYED,
			release.Status_DELETED,
			release.Status_DELETING,
			release.Status_FAILED,
			release.Status_PENDING_INSTALL,
			release.Status_PENDING_UPGRADE,
			release.Status_PENDING_ROLLBACK,
		}
	}
	status := []release.Status_Code{}
	if l.deployed {
		status = append(status, release.Status_DEPLOYED)
	}
	if l.deleted {
		status = append(status, release.Status_DELETED)
	}
	if l.deleting {
		status = append(status, release.Status_DELETING)
	}
	if l.failed {
		status = append(status, release.Status_FAILED)
	}
	if l.superseded {
		status = append(status, release.Status_SUPERSEDED)
	}
	if l.pending {
		status = append(status, release.Status_PENDING_INSTALL, release.Status_PENDING_UPGRADE, release.Status_PENDING_ROLLBACK)
	}

	// Default case.
	if len(status) == 0 {
		status = append(status, release.Status_DEPLOYED, release.Status_FAILED)
	}
	return status
}

func getListResult(rels []*release.Release, next string) listResult {
	listReleases := []listRelease{}
	for _, r := range rels {
		md := r.GetChart().GetMetadata()
		t := "-"
		if tspb := r.GetInfo().GetLastDeployed(); tspb != nil {
			t = timeconv.String(tspb)
		}

		lr := listRelease{
			Name:       r.GetName(),
			Revision:   r.GetVersion(),
			Updated:    t,
			Status:     r.GetInfo().GetStatus().GetCode().String(),
			Chart:      fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion()),
			AppVersion: md.GetAppVersion(),
			Namespace:  r.GetNamespace(),
		}
		listReleases = append(listReleases, lr)
	}

	return listResult{
		Releases: listReleases,
		Next:     next,
	}
}

func shortenListResult(result listResult) []string {
	names := []string{}
	for _, r := range result.Releases {
		names = append(names, r.Name)
	}

	return names
}

func formatResult(format string, short bool, result listResult, colWidth uint) (string, error) {
	var output string
	var err error

	var shortResult []string
	var finalResult interface{}
	if short {
		shortResult = shortenListResult(result)
		finalResult = shortResult
	} else {
		finalResult = result
	}

	switch format {
	case "":
		if short {
			output = formatTextShort(shortResult)
		} else {
			output = formatText(result, colWidth)
		}
	case "json":
		o, e := json.Marshal(finalResult)
		if e != nil {
			err = fmt.Errorf("Failed to Marshal JSON output: %s", e)
		} else {
			output = string(o)
		}
	case "yaml":
		o, e := yaml.Marshal(finalResult)
		if e != nil {
			err = fmt.Errorf("Failed to Marshal YAML output: %s", e)
		} else {
			output = string(o)
		}
	default:
		err = fmt.Errorf("Unknown output format \"%s\"", format)
	}
	return output, err
}

func formatText(result listResult, colWidth uint) string {
	nextOutput := ""
	if result.Next != "" {
		nextOutput = fmt.Sprintf("\tnext: %s\n", result.Next)
	}

	table := uitable.New()
	table.MaxColWidth = colWidth
	table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE")
	for _, lr := range result.Releases {
		table.AddRow(lr.Name, lr.Revision, lr.Updated, lr.Status, lr.Chart, lr.AppVersion, lr.Namespace)
	}

	return fmt.Sprintf("%s%s", nextOutput, table.String())
}

func formatTextShort(shortResult []string) string {
	return strings.Join(shortResult, "\n")
}
<|endoftext|>