<commit_before>package app

import (
	"bytes"
	"fmt"
	"math/big"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/state"
	ethTypes "github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/params"
	"github.com/ethereum/go-ethereum/rlp"

	"github.com/tendermint/abci/types"

	"github.com/tendermint/tmlibs/log"
)

const (
	maxTxSize = 32768
)

// TendereumApplication is an application that sits on top of Tendermint Core. It provides wraps
// an EVM and Ethereum state. It implements Query in order to service any RPC client.
// nolint: megacheck, structcheck
type TendereumApplication struct {
	types.BaseApplication

	db  ethdb.Database
	was *writeAheadState

	checkTxState *state.StateDB

	chainConfig params.ChainConfig // evm tighly coupled to chainConfig
	vmConfig    vm.Config
	signer      ethTypes.Signer

	logger log.Logger
}

// Interface assertions
var _ types.Application = (*TendereumApplication)(nil)

// see the following for checking out how to do with precompiles:
// https://github.com/cosmos/tendereum/blob/master/vendor/github.com/ethereum/go-ethereum/core/vm/evm.go#L40-L52
// https://github.com/cosmos/tendereum/blob/master/vendor/github.com/ethereum/go-ethereum/core/vm/contracts.go#L49-L60
//
// echo is a silly start contract
type echo struct{}

var _ vm.PrecompiledContract = echo{}

func (echo) RequiredGas(input []byte) uint64 { return 100 }
func (echo) Run(input []byte) ([]byte, error) {
	// input is a multiple of 32 bytes, each one an evm argument
	// result the same

	// example of return in the same order
	// return input, nil

	// example to return the arguments in reverse order
	if len(input)%32 != 0 {
		return nil, fmt.Errorf("Input must be a multiple of 32 bytes")
	}
	output := make([]byte, 0, len(input))
	for l := len(input) - 32; l >= 0; l -= 32 {
		output = append(output, input[l:l+32]...)
	}
	return output, nil
}

func init() {
	vm.PrecompiledContractsByzantium[common.BytesToAddress([]byte{255})] = echo{}
}

// NewTendereumApplication returns a new instance of a Tendereum application.
// NOTE: Pass in a config struct which allows the specification of a chain-id. The signer needs the
// chain-id in order to provide replay protection.
// TODO: Pass in a config struct to provide the home directory.
func NewTendereumApplication(logger log.Logger) *TendereumApplication {

	chainConfig := params.ChainConfig{
		ChainId:        big.NewInt(1),
		HomesteadBlock: new(big.Int),
		DAOForkBlock:   new(big.Int),
		DAOForkSupport: true,
		EIP150Block:    new(big.Int),
		EIP150Hash:     common.Hash{},
		EIP155Block:    new(big.Int),
		EIP158Block:    new(big.Int),
		ByzantiumBlock: new(big.Int),
		Ethash:         new(params.EthashConfig),
	}

	return &TendereumApplication{
		// should set almost all options to 1 except for chain-id
		// needs to ensure that chain-id is unique and doesn't conflict with other current
		// networks.
		chainConfig: chainConfig,
		// TODO: Should be settable in order to enable EVM jit.
		vmConfig: vm.Config{Tracer: vm.NewStructLogger(nil)},
		// EIP155 signer implements replay attack by including the chain-id in the signature
		signer: ethTypes.NewEIP155Signer(chainConfig.ChainId),
		logger: logger,
	}
}

// nolint: megacheck, structcheck
type writeAheadState struct {
	state        *state.StateDB
	txIndex      int
	transactions []*ethTypes.Transaction
	receipts     ethTypes.Receipts
	allLogs      []*ethTypes.Log
	totalUsedGas *big.Int
	gasPool      *core.GasPool
}

// ------------------------------------------------------------------------------------------------
// ABCI Implementation for Tendermint Core

// ------------------------------------------------------------------------------------------------
// Info/Query connection

// Info returns some basic information about Tendereum.
func (ta *TendereumApplication) Info(req types.RequestInfo) (res types.ResponseInfo) {
	res = types.ResponseInfo{Data: fmt.Sprintf("Tendereum"), Version: fmt.Sprintf("0.1.0")}
	return res
}

// SetOption will not be used most likely. This could be useful to implement Web3 api that requires
// setting options, such as minimum gas price.
// Potentially this can be used to implement the management api.
func (ta *TendereumApplication) SetOption(key, value string) (log string) {
	log = fmt.Sprintf("Not yet implemented.")
	return log
}

// Query handles all RPC queries that the RPC server sends to Tendermint Core.
// This is used to implement the RPC server which can be run by a light-node or full-node.
func (ta *TendereumApplication) Query(req types.RequestQuery) (res types.ResponseQuery) {
	res = types.ResponseQuery{Code: types.CodeType_OK, Log: "Not yet implemented."}
	return res
}

// ------------------------------------------------------------------------------------------------
// Mempool connection

// CheckTx delivers transaction from the Tendermint Core mempool.
// NOTE: Ethereum currently enforces a max transaction size limit per transaction and not just per
// block. Should we also enforce a maximum size per transaction. Maybe it is in relation to the
// number of transactions per block for the last couple of blocks.
func (ta *TendereumApplication) CheckTx(data []byte) types.Result {
	tx, err := decodeTx(data)
	if err != nil {
		ta.logger.Error("Decoding transaction", "error", err)
		return types.ErrEncodingError
	}

	if tx.Size() > maxTxSize {
		return types.ErrInternalError
	}

	if tx.Value().Sign() < 0 {
		return types.ErrInternalError
	}

	from, err := ethTypes.Sender(ta.signer, tx)
	if err != nil {
		return types.ErrUnauthorized
	}

	if ta.checkTxState.GetNonce(from)+1 != tx.Nonce() {
		return types.ErrBadNonce
	}

	if ta.checkTxState.GetBalance(from).Cmp(tx.Cost()) < 0 {
		return types.ErrInsufficientFunds
	}

	// the last parameter is whether we are on homestead. It is always true.
	intrGas := core.IntrinsicGas(tx.Data(), tx.To() == nil, true)
	if tx.Gas().Cmp(intrGas) < 0 {
		return types.ErrInternalError
	}

	// update current state to allow multiple transactions per block
	ta.checkTxState.SubBalance(from, tx.Cost())
	if to := tx.To(); to != nil {
		ta.checkTxState.AddBalance(*to, tx.Value())
	}
	ta.checkTxState.SetNonce(from, tx.Nonce()+1)

	return types.Result{Code: types.CodeType_OK}
}

// ------------------------------------------------------------------------------------------------
// Consensus connection

// InitChain is called when Tendermint Core is started.
func (ta *TendereumApplication) InitChain(req types.RequestInitChain) {
}

// BeginBlock is called before a new block is started. It is called before any transaction is
// submitted using DeliverTx.
func (ta *TendereumApplication) BeginBlock(req types.RequestBeginBlock) {
}

// DeliverTx delivers transactions that have been included in a finalised block.
// Updates the state
func (ta *TendereumApplication) DeliverTx(data []byte) (res types.Result) {
	var tx ethTypes.Transaction
	if err := rlp.Decode(bytes.NewReader(data), &tx); err != nil {
		return types.ErrBaseEncodingError
	}

	msg, err := tx.AsMessage(ta.signer)
	if err != nil {
		return types.ErrInternalError
	}

	context := vm.Context{
		CanTransfer: core.CanTransfer,
		Transfer:    core.Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		Origin:      msg.From(),
		GasPrice:    msg.GasPrice(),
	}

	evm := vm.NewEVM(context, ta.was.state, &ta.chainConfig, ta.vmConfig)
	// NOTE: This deducts gas and credits it to the defined coinbase.
	_, gas, failed, err := core.ApplyMessage(evm, msg, ta.was.gasPool)
	if err != nil {
		return types.ErrInternalError
	}

	ta.was.state.Finalise(true)

	ta.was.totalUsedGas.Add(ta.was.totalUsedGas, gas)

	// since Byzantium the root hash of a receipt is empty
	var root []byte
	receipt := ethTypes.NewReceipt(root, failed, ta.was.totalUsedGas)
	receipt.TxHash = tx.Hash()
	receipt.GasUsed = new(big.Int).Set(gas)
	if msg.To() == nil {
		receipt.ContractAddress = crypto.CreateAddress(evm.Context.Origin, tx.Nonce())
	}

	receipt.Logs = ta.was.state.GetLogs(tx.Hash())
	receipt.Bloom = ethTypes.CreateBloom(ethTypes.Receipts{receipt})

	return types.Result{Code: types.CodeType_OK}
}

// EndBlock is called to signal the end of the current block. It is called after all transactions
// have been delivered using DeliverTx.
func (ta *TendereumApplication) EndBlock(height uint64) (res types.ResponseEndBlock) {
	res = types.ResponseEndBlock{}
	return res
}

// Commit is called to obtain a unique state root for inclusion in the next Tendermint Core block.
// Writes all the changes to the state from DeliverTx to the underlying database.
func (ta *TendereumApplication) Commit() (res types.Result) {
	res = types.Result{Code: types.CodeType_OK, Log: "Not yet implemented."}
	return res
}

// ------------------------------------------------------------------------------------------------
// Helpers

func decodeTx(data []byte) (*ethTypes.Transaction, error) {
	var tx ethTypes.Transaction
	if err := rlp.Decode(bytes.NewReader(data), &tx); err != nil {
		return nil, err
	}
	return &tx, nil
}
<commit_msg>Commented references to ethereum delivertx,endblock,commit<commit_after>package app

import (
	"bytes"
	"fmt"
	"math/big"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/state"
	ethTypes "github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/params"
	"github.com/ethereum/go-ethereum/rlp"

	"github.com/tendermint/abci/types"

	"github.com/tendermint/tmlibs/log"
)

const (
	maxTxSize = 32768
)

// TendereumApplication is an application that sits on top of Tendermint Core. It provides wraps
// an EVM and Ethereum state. It implements Query in order to service any RPC client.
// nolint: megacheck, structcheck
type TendereumApplication struct {
	types.BaseApplication

	db  ethdb.Database
	was *writeAheadState

	checkTxState *state.StateDB

	chainConfig params.ChainConfig // evm tighly coupled to chainConfig
	vmConfig    vm.Config
	signer      ethTypes.Signer

	logger log.Logger
}

// Interface assertions
var _ types.Application = (*TendereumApplication)(nil)

// see the following for checking out how to do with precompiles:
// https://github.com/cosmos/tendereum/blob/master/vendor/github.com/ethereum/go-ethereum/core/vm/evm.go#L40-L52
// https://github.com/cosmos/tendereum/blob/master/vendor/github.com/ethereum/go-ethereum/core/vm/contracts.go#L49-L60
//
// echo is a silly start contract
type echo struct{}

var _ vm.PrecompiledContract = echo{}

func (echo) RequiredGas(input []byte) uint64 { return 100 }
func (echo) Run(input []byte) ([]byte, error) {
	// input is a multiple of 32 bytes, each one an evm argument
	// result the same

	// example of return in the same order
	// return input, nil

	// example to return the arguments in reverse order
	if len(input)%32 != 0 {
		return nil, fmt.Errorf("Input must be a multiple of 32 bytes")
	}
	output := make([]byte, 0, len(input))
	for l := len(input) - 32; l >= 0; l -= 32 {
		output = append(output, input[l:l+32]...)
	}
	return output, nil
}

func init() {
	vm.PrecompiledContractsByzantium[common.BytesToAddress([]byte{255})] = echo{}
}

// NewTendereumApplication returns a new instance of a Tendereum application.
// NOTE: Pass in a config struct which allows the specification of a chain-id. The signer needs the
// chain-id in order to provide replay protection.
// TODO: Pass in a config struct to provide the home directory.
func NewTendereumApplication(logger log.Logger) *TendereumApplication {

	chainConfig := params.ChainConfig{
		ChainId:        big.NewInt(1),
		HomesteadBlock: new(big.Int),
		DAOForkBlock:   new(big.Int),
		DAOForkSupport: true,
		EIP150Block:    new(big.Int),
		EIP150Hash:     common.Hash{},
		EIP155Block:    new(big.Int),
		EIP158Block:    new(big.Int),
		ByzantiumBlock: new(big.Int),
		Ethash:         new(params.EthashConfig),
	}

	return &TendereumApplication{
		// should set almost all options to 1 except for chain-id
		// needs to ensure that chain-id is unique and doesn't conflict with other current
		// networks.
		chainConfig: chainConfig,
		// TODO: Should be settable in order to enable EVM jit.
		vmConfig: vm.Config{Tracer: vm.NewStructLogger(nil)},
		// EIP155 signer implements replay attack by including the chain-id in the signature
		signer: ethTypes.NewEIP155Signer(chainConfig.ChainId),
		logger: logger,
	}
}

// nolint: megacheck, structcheck
type writeAheadState struct {
	state        *state.StateDB
	txIndex      int
	transactions []*ethTypes.Transaction
	receipts     ethTypes.Receipts
	allLogs      []*ethTypes.Log
	totalUsedGas *big.Int
	gasPool      *core.GasPool
}

// ------------------------------------------------------------------------------------------------
// ABCI Implementation for Tendermint Core

// ------------------------------------------------------------------------------------------------
// Info/Query connection

// Info returns some basic information about Tendereum.
// TODO: return hash and height info to do handshaking and recover after a restart
// without re-running the entire chain
func (ta *TendereumApplication) Info(req types.RequestInfo) (res types.ResponseInfo) {
	res = types.ResponseInfo{Data: fmt.Sprintf("Tendereum"), Version: fmt.Sprintf("0.1.0")}
	return res
}

// SetOption will not be used most likely. This could be useful to implement Web3 api that requires
// setting options, such as minimum gas price.
// Potentially this can be used to implement the management api.
func (ta *TendereumApplication) SetOption(key, value string) (log string) {
	log = fmt.Sprintf("Not yet implemented.")
	return log
}

// Query handles all RPC queries that the RPC server sends to Tendermint Core.
// This is used to implement the RPC server which can be run by a light-node or full-node.
// Note: many of the web3rpc endpoints need to be mapped to tendermint rpc about
// blocks and headers and such...
func (ta *TendereumApplication) Query(req types.RequestQuery) (res types.ResponseQuery) {
	// must be compatible with http://godoc.org/pkg/github.com/ethereum/go-ethereum/ethclient/
	// possibly via adaptors. note all methods there...

	// see code at:
	// http://godoc.org/pkg/github.com/ethereum/go-ethereum/internal/ethapi/#PublicBlockChainAPI
	// https://godoc.org/github.com/ethereum/go-ethereum/internal/ethapi#PublicTransactionPoolAPI.SendTransaction

	res = types.ResponseQuery{Code: types.CodeType_OK, Log: "Not yet implemented."}
	return res
}

// ------------------------------------------------------------------------------------------------
// Mempool connection

// CheckTx delivers transaction from the Tendermint Core mempool.
// NOTE: Ethereum currently enforces a max transaction size limit per transaction and not just per
// block. Should we also enforce a maximum size per transaction. Maybe it is in relation to the
// number of transactions per block for the last couple of blocks.
func (ta *TendereumApplication) CheckTx(data []byte) types.Result {
	tx, err := decodeTx(data)
	if err != nil {
		ta.logger.Error("Decoding transaction", "error", err)
		return types.ErrEncodingError
	}

	if tx.Size() > maxTxSize {
		return types.ErrInternalError
	}

	if tx.Value().Sign() < 0 {
		return types.ErrInternalError
	}

	from, err := ethTypes.Sender(ta.signer, tx)
	if err != nil {
		return types.ErrUnauthorized
	}

	if ta.checkTxState.GetNonce(from)+1 != tx.Nonce() {
		return types.ErrBadNonce
	}

	if ta.checkTxState.GetBalance(from).Cmp(tx.Cost()) < 0 {
		return types.ErrInsufficientFunds
	}

	// the last parameter is whether we are on homestead. It is always true.
	intrGas := core.IntrinsicGas(tx.Data(), tx.To() == nil, true)
	if tx.Gas().Cmp(intrGas) < 0 {
		return types.ErrInternalError
	}

	// update current state to allow multiple transactions per block
	ta.checkTxState.SubBalance(from, tx.Cost())
	if to := tx.To(); to != nil {
		ta.checkTxState.AddBalance(*to, tx.Value())
	}
	ta.checkTxState.SetNonce(from, tx.Nonce()+1)

	return types.Result{Code: types.CodeType_OK}
}

// ------------------------------------------------------------------------------------------------
// Consensus connection

// InitChain is called when Tendermint Core is started.
func (ta *TendereumApplication) InitChain(req types.RequestInitChain) {
}

// BeginBlock is called before a new block is started. It is called before any transaction is
// submitted using DeliverTx.
func (ta *TendereumApplication) BeginBlock(req types.RequestBeginBlock) {
	// https://github.com/ethereum/go-ethereum/blob/master/core/blockchain.go#L955-L961
	// some setup like here?
}

// DeliverTx delivers transactions that have been included in a finalised block.
// Updates the state
//
// This function is a slight adaptation of core.ApplyTransaction
// https://github.com/ethereum/go-ethereum/blob/master/core/state_processor.go#L86-L131
func (ta *TendereumApplication) DeliverTx(data []byte) (res types.Result) {
	// What about this logic from Process?
	// https://github.com/ethereum/go-ethereum/blob/master/core/state_processor.go#L72-L82
	// at the least, we should do:
	//   statedb.Prepare(tx.Hash(), block.Hash(), i)

	var tx ethTypes.Transaction
	if err := rlp.Decode(bytes.NewReader(data), &tx); err != nil {
		return types.ErrBaseEncodingError
	}

	msg, err := tx.AsMessage(ta.signer)
	if err != nil {
		return types.ErrInternalError
	}

	// look at https://github.com/ethereum/go-ethereum/blob/master/core/evm.go#L38-L59
	context := vm.Context{
		CanTransfer: core.CanTransfer,
		Transfer:    core.Transfer,
		GetHash:     func(uint64) common.Hash { return common.Hash{} },
		Origin:      msg.From(),
		// -> set to proposer (in header?)
		// Coinbase:    beneficiary,
		// -> get blocknumber and time from header
		// BlockNumber: new(big.Int).Set(header.Number),
		// Time:        new(big.Int).Set(header.Time),
		// -> fake difficulty (or remove?)
		// Difficulty:  new(big.Int).Set(header.Difficulty),
		// -> GasLimit from a config?
		// GasLimit:    new(big.Int).Set(header.GasLimit),
		GasPrice: new(big.Int).Set(msg.GasPrice()),
	}

	evm := vm.NewEVM(context, ta.was.state, &ta.chainConfig, ta.vmConfig)
	// NOTE: This deducts gas and credits it to the defined coinbase.
	_, gas, failed, err := core.ApplyMessage(evm, msg, ta.was.gasPool)
	if err != nil {
		return types.ErrInternalError
	}

	ta.was.state.Finalise(true)

	ta.was.totalUsedGas.Add(ta.was.totalUsedGas, gas)

	// since Byzantium the root hash of a receipt is empty
	var root []byte
	receipt := ethTypes.NewReceipt(root, failed, ta.was.totalUsedGas)
	receipt.TxHash = tx.Hash()
	receipt.GasUsed = new(big.Int).Set(gas)
	if msg.To() == nil {
		receipt.ContractAddress = crypto.CreateAddress(evm.Context.Origin, tx.Nonce())
	}

	receipt.Logs = ta.was.state.GetLogs(tx.Hash())
	receipt.Bloom = ethTypes.CreateBloom(ethTypes.Receipts{receipt})

	// store all receipts and logs...
	ta.was.receipts = append(ta.was.receipts, receipt)
	ta.was.allLogs = append(ta.was.allLogs, receipt.Logs...)

	return types.Result{Code: types.CodeType_OK}
}

// EndBlock is called to signal the end of the current block. It is called after all transactions
// have been delivered using DeliverTx.
func (ta *TendereumApplication) EndBlock(height uint64) (res types.ResponseEndBlock) {
	// https://github.com/ethereum/go-ethereum/blob/master/consensus/ethash/consensus.go#L509-L519
	// 	 state.AddBalance(header.Coinbase, reward)

	// TODO: add staking rules from precompiled staking contract and modify validators
	res = types.ResponseEndBlock{}
	return res
}

// Commit is called to obtain a unique state root for inclusion in the next Tendermint Core block.
// Writes all the changes to the state from DeliverTx to the underlying database.
func (ta *TendereumApplication) Commit() (res types.Result) {
	// https://github.com/ethereum/go-ethereum/blob/master/consensus/ethash/consensus.go#L514-L515
	//   header.Root = ta.was.state.IntermediateRoot(true)

	// Also, WriteBlockAndState
	// https://github.com/ethereum/go-ethereum/blob/master/core/blockchain.go#L965-L982
	// https://github.com/ethereum/go-ethereum/blob/master/core/blockchain.go#L806-L817

	// make sure we store the receipts and logs with the bloom filter and all,
	// so we can serve them up for web3 rpc requests

	res = types.Result{Code: types.CodeType_OK, Log: "Not yet implemented."}
	return res
}

// ------------------------------------------------------------------------------------------------
// Helpers

func decodeTx(data []byte) (*ethTypes.Transaction, error) {
	var tx ethTypes.Transaction
	if err := rlp.Decode(bytes.NewReader(data), &tx); err != nil {
		return nil, err
	}
	return &tx, nil
}
<|endoftext|>


<commit_before>// Copyright Istio 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 xds

import (
	"context"
	"fmt"
	"reflect"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	"google.golang.org/grpc"

	"istio.io/istio/pkg/test/util/retry"
	"istio.io/pkg/monitoring"

	"istio.io/istio/pilot/pkg/model"
	v3 "istio.io/istio/pilot/pkg/xds/v3"
)

func createProxies(n int) []*Connection {
	proxies := make([]*Connection, 0, n)
	for p := 0; p < n; p++ {
		proxies = append(proxies, &Connection{
			ConID:       fmt.Sprintf("proxy-%v", p),
			pushChannel: make(chan *Event),
			stream:      &fakeStream{},
		})
	}
	return proxies
}

func wgDoneOrTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
	c := make(chan struct{})
	go func() {
		wg.Wait()
		c <- struct{}{}
	}()
	select {
	case <-c:
		return true
	case <-time.After(timeout):
		return false
	}
}

func TestSendPushesManyPushes(t *testing.T) {
	stopCh := make(chan struct{})
	semaphore := make(chan struct{}, 2)
	queue := NewPushQueue()

	proxies := createProxies(5)

	pushes := make(map[string]int)
	pushesMu := &sync.Mutex{}

	for _, proxy := range proxies {
		proxy := proxy
		// Start receive thread
		go func() {
			for {
				p := <-proxy.pushChannel
				p.done()
				pushesMu.Lock()
				pushes[proxy.ConID]++
				pushesMu.Unlock()
			}
		}()
	}
	go doSendPushes(stopCh, semaphore, queue)

	for push := 0; push < 100; push++ {
		for _, proxy := range proxies {
			queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
		}
		time.Sleep(time.Millisecond * 10)
	}
	for queue.Pending() > 0 {
		time.Sleep(time.Millisecond)
	}
	pushesMu.Lock()
	defer pushesMu.Unlock()
	for proxy, numPushes := range pushes {
		if numPushes == 0 {
			t.Fatalf("Proxy %v had 0 pushes", proxy)
		}
	}
}

func TestSendPushesSinglePush(t *testing.T) {
	stopCh := make(chan struct{})
	semaphore := make(chan struct{}, 2)
	queue := NewPushQueue()

	proxies := createProxies(5)

	wg := &sync.WaitGroup{}
	wg.Add(5)

	pushes := make(map[string]int)
	pushesMu := &sync.Mutex{}

	for _, proxy := range proxies {
		proxy := proxy
		// Start receive thread
		go func() {
			for {
				p := <-proxy.pushChannel
				p.done()
				pushesMu.Lock()
				pushes[proxy.ConID]++
				pushesMu.Unlock()
				wg.Done()
			}
		}()
	}
	go doSendPushes(stopCh, semaphore, queue)

	for _, proxy := range proxies {
		queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
	}

	if !wgDoneOrTimeout(wg, time.Second) {
		t.Fatalf("Expected 5 pushes but got %v", len(pushes))
	}
	expected := map[string]int{
		"proxy-0": 1,
		"proxy-1": 1,
		"proxy-2": 1,
		"proxy-3": 1,
		"proxy-4": 1,
	}
	if !reflect.DeepEqual(expected, pushes) {
		t.Fatalf("Expected pushes %+v, got %+v", expected, pushes)
	}
}

type fakeStream struct {
	grpc.ServerStream
}

func (h *fakeStream) Send(*discovery.DiscoveryResponse) error {
	return nil
}

func (h *fakeStream) Recv() (*discovery.DiscoveryRequest, error) {
	return nil, nil
}

func (h *fakeStream) Context() context.Context {
	return context.Background()
}

func TestDebounce(t *testing.T) {
	// This test tests the timeout and debouncing of config updates
	// If it is flaking, DebounceAfter may need to be increased, or the code refactored to mock time.
	// For now, this seems to work well
	opts := debounceOptions{
		debounceAfter:     time.Millisecond * 50,
		debounceMax:       time.Millisecond * 100,
		enableEDSDebounce: false,
	}

	tests := []struct {
		name string
		test func(updateCh chan *model.PushRequest, expect func(partial, full int32))
	}{
		{
			name: "Should not debounce partial pushes",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: false}
				expect(1, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(2, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(3, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(4, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(5, 0)
			},
		},
		{
			name: "Should debounce full pushes",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 0)
			},
		},
		{
			name: "Should send full updates in batches",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 1)
			},
		},
		{
			name: "Should send full updates in batches, partial updates immediately",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: false}
				updateCh <- &model.PushRequest{Full: false}
				expect(2, 1)
				updateCh <- &model.PushRequest{Full: false}
				expect(3, 1)
			},
		},
		{
			name: "Should force a push after DebounceMax",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				// Send many requests within debounce window
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				expect(0, 1)
			},
		},
		{
			name: "Should push synchronously after debounce",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter + 10*time.Millisecond)
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 2)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			stopCh := make(chan struct{})
			updateCh := make(chan *model.PushRequest)
			pushingCh := make(chan struct{}, 1)
			errCh := make(chan error, 1)

			var partialPushes int32
			var fullPushes int32

			wg := sync.WaitGroup{}

			fakePush := func(req *model.PushRequest) {
				if req.Full {
					select {
					case pushingCh <- struct{}{}:
					default:
						errCh <- fmt.Errorf("multiple pushes happen simultaneously")
						return
					}
					atomic.AddInt32(&fullPushes, 1)
					time.Sleep(opts.debounceMax * 2)
					<-pushingCh
				} else {
					atomic.AddInt32(&partialPushes, 1)
				}
			}

			wg.Add(1)
			go func() {
				debounce(updateCh, stopCh, opts, fakePush)
				wg.Done()
			}()

			expect := func(expectedPartial, expectedFull int32) {
				t.Helper()
				err := retry.UntilSuccess(func() error {
					select {
					case err := <-errCh:
						t.Error(err)
						return err
					default:
						partial := atomic.LoadInt32(&partialPushes)
						full := atomic.LoadInt32(&fullPushes)
						if partial != expectedPartial || full != expectedFull {
							return fmt.Errorf("got %v full and %v partial, expected %v full and %v partial", full, partial, expectedFull, expectedPartial)
						}
						return nil
					}
				}, retry.Timeout(opts.debounceAfter*8), retry.Delay(opts.debounceAfter/2))
				if err != nil {
					t.Error(err)
				}
			}

			// Send updates
			tt.test(updateCh, expect)

			close(stopCh)
			wg.Wait()
		})
	}
}

func TestShouldRespond(t *testing.T) {
	tests := []struct {
		name       string
		connection *Connection
		request    *discovery.DiscoveryRequest
		response   bool
	}{
		{
			name: "initial request",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl: v3.ClusterType,
			},
			response: true,
		},
		{
			name: "ack",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.ClusterType: {
							VersionSent: "v1",
							NonceSent:   "nonce",
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
			},
			response: false,
		},
		{
			name: "nack",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.ClusterType: {
							VersionSent: "v1",
							NonceSent:   "nonce",
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "stale nonce",
			},
			response: false,
		},
		{
			name: "reconnect",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "reconnect nonce",
			},
			response: true,
		},
		{
			name: "resources change",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.EndpointType: {
							VersionSent:   "v1",
							NonceSent:     "nonce",
							ResourceNames: []string{"cluster1"},
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.EndpointType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
				ResourceNames: []string{"cluster1", "cluster2"},
			},
			response: true,
		},
		{
			name: "ack with same resources",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.EndpointType: {
							VersionSent:   "v1",
							NonceSent:     "nonce",
							ResourceNames: []string{"cluster2", "cluster1"},
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.EndpointType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
				ResourceNames: []string{"cluster1", "cluster2"},
			},
			response: false,
		},
	}

	metric := monitoring.NewSum("test", "test reject metric")

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := NewFakeDiscoveryServer(t, FakeOptions{})
			if response := s.Discovery.shouldRespond(tt.connection, metric, tt.request); response != tt.response {
				t.Fatalf("Unexpected value for response, expected %v, got %v", tt.response, response)
			}
			if tt.name != "reconnect" && tt.response {
				if tt.connection.node.Active[tt.request.TypeUrl].VersionAcked != tt.request.VersionInfo &&
					tt.connection.node.Active[tt.request.TypeUrl].NonceAcked != tt.request.ResponseNonce {
					t.Fatalf("Version & Nonce not updated properly")
				}
			}
		})
	}
}
<commit_msg>TestSendPushes pushChannel routine leak (#26257)<commit_after>// Copyright Istio 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 xds

import (
	"context"
	"fmt"
	"reflect"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	"google.golang.org/grpc"

	"istio.io/istio/pkg/test/util/retry"
	"istio.io/pkg/monitoring"

	"istio.io/istio/pilot/pkg/model"
	v3 "istio.io/istio/pilot/pkg/xds/v3"
)

func createProxies(n int) []*Connection {
	proxies := make([]*Connection, 0, n)
	for p := 0; p < n; p++ {
		proxies = append(proxies, &Connection{
			ConID:       fmt.Sprintf("proxy-%v", p),
			pushChannel: make(chan *Event),
			stream:      &fakeStream{},
		})
	}
	return proxies
}

func wgDoneOrTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
	c := make(chan struct{})
	go func() {
		wg.Wait()
		c <- struct{}{}
	}()
	select {
	case <-c:
		return true
	case <-time.After(timeout):
		return false
	}
}

func TestSendPushesManyPushes(t *testing.T) {
	stopCh := make(chan struct{})
	defer close(stopCh)

	semaphore := make(chan struct{}, 2)
	queue := NewPushQueue()

	proxies := createProxies(5)

	pushes := make(map[string]int)
	pushesMu := &sync.Mutex{}

	for _, proxy := range proxies {
		proxy := proxy
		// Start receive thread
		go func() {
			for {
				select {
				case p := <-proxy.pushChannel:
					p.done()
					pushesMu.Lock()
					pushes[proxy.ConID]++
					pushesMu.Unlock()
				case <-stopCh:
					return
				}
			}
		}()
	}
	go doSendPushes(stopCh, semaphore, queue)

	for push := 0; push < 100; push++ {
		for _, proxy := range proxies {
			queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
		}
		time.Sleep(time.Millisecond * 10)
	}
	for queue.Pending() > 0 {
		time.Sleep(time.Millisecond)
	}
	pushesMu.Lock()
	defer pushesMu.Unlock()
	for proxy, numPushes := range pushes {
		if numPushes == 0 {
			t.Fatalf("Proxy %v had 0 pushes", proxy)
		}
	}
}

func TestSendPushesSinglePush(t *testing.T) {
	stopCh := make(chan struct{})
	defer close(stopCh)

	semaphore := make(chan struct{}, 2)
	queue := NewPushQueue()

	proxies := createProxies(5)

	wg := &sync.WaitGroup{}
	wg.Add(5)

	pushes := make(map[string]int)
	pushesMu := &sync.Mutex{}

	for _, proxy := range proxies {
		proxy := proxy
		// Start receive thread
		go func() {
			for {
				select {
				case p := <-proxy.pushChannel:
					p.done()
					pushesMu.Lock()
					pushes[proxy.ConID]++
					pushesMu.Unlock()
					wg.Done()
				case <-stopCh:
					return
				}
			}
		}()
	}
	go doSendPushes(stopCh, semaphore, queue)

	for _, proxy := range proxies {
		queue.Enqueue(proxy, &model.PushRequest{Push: &model.PushContext{}})
	}

	if !wgDoneOrTimeout(wg, time.Second) {
		t.Fatalf("Expected 5 pushes but got %v", len(pushes))
	}
	expected := map[string]int{
		"proxy-0": 1,
		"proxy-1": 1,
		"proxy-2": 1,
		"proxy-3": 1,
		"proxy-4": 1,
	}
	if !reflect.DeepEqual(expected, pushes) {
		t.Fatalf("Expected pushes %+v, got %+v", expected, pushes)
	}
}

type fakeStream struct {
	grpc.ServerStream
}

func (h *fakeStream) Send(*discovery.DiscoveryResponse) error {
	return nil
}

func (h *fakeStream) Recv() (*discovery.DiscoveryRequest, error) {
	return nil, nil
}

func (h *fakeStream) Context() context.Context {
	return context.Background()
}

func TestDebounce(t *testing.T) {
	// This test tests the timeout and debouncing of config updates
	// If it is flaking, DebounceAfter may need to be increased, or the code refactored to mock time.
	// For now, this seems to work well
	opts := debounceOptions{
		debounceAfter:     time.Millisecond * 50,
		debounceMax:       time.Millisecond * 100,
		enableEDSDebounce: false,
	}

	tests := []struct {
		name string
		test func(updateCh chan *model.PushRequest, expect func(partial, full int32))
	}{
		{
			name: "Should not debounce partial pushes",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: false}
				expect(1, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(2, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(3, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(4, 0)
				updateCh <- &model.PushRequest{Full: false}
				expect(5, 0)
			},
		},
		{
			name: "Should debounce full pushes",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 0)
			},
		},
		{
			name: "Should send full updates in batches",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 1)
			},
		},
		{
			name: "Should send full updates in batches, partial updates immediately",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: true}
				updateCh <- &model.PushRequest{Full: false}
				updateCh <- &model.PushRequest{Full: false}
				expect(2, 1)
				updateCh <- &model.PushRequest{Full: false}
				expect(3, 1)
			},
		},
		{
			name: "Should force a push after DebounceMax",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				// Send many requests within debounce window
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter / 2)
				expect(0, 1)
			},
		},
		{
			name: "Should push synchronously after debounce",
			test: func(updateCh chan *model.PushRequest, expect func(partial, full int32)) {
				updateCh <- &model.PushRequest{Full: true}
				time.Sleep(opts.debounceAfter + 10*time.Millisecond)
				updateCh <- &model.PushRequest{Full: true}
				expect(0, 2)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			stopCh := make(chan struct{})
			updateCh := make(chan *model.PushRequest)
			pushingCh := make(chan struct{}, 1)
			errCh := make(chan error, 1)

			var partialPushes int32
			var fullPushes int32

			wg := sync.WaitGroup{}

			fakePush := func(req *model.PushRequest) {
				if req.Full {
					select {
					case pushingCh <- struct{}{}:
					default:
						errCh <- fmt.Errorf("multiple pushes happen simultaneously")
						return
					}
					atomic.AddInt32(&fullPushes, 1)
					time.Sleep(opts.debounceMax * 2)
					<-pushingCh
				} else {
					atomic.AddInt32(&partialPushes, 1)
				}
			}

			wg.Add(1)
			go func() {
				debounce(updateCh, stopCh, opts, fakePush)
				wg.Done()
			}()

			expect := func(expectedPartial, expectedFull int32) {
				t.Helper()
				err := retry.UntilSuccess(func() error {
					select {
					case err := <-errCh:
						t.Error(err)
						return err
					default:
						partial := atomic.LoadInt32(&partialPushes)
						full := atomic.LoadInt32(&fullPushes)
						if partial != expectedPartial || full != expectedFull {
							return fmt.Errorf("got %v full and %v partial, expected %v full and %v partial", full, partial, expectedFull, expectedPartial)
						}
						return nil
					}
				}, retry.Timeout(opts.debounceAfter*8), retry.Delay(opts.debounceAfter/2))
				if err != nil {
					t.Error(err)
				}
			}

			// Send updates
			tt.test(updateCh, expect)

			close(stopCh)
			wg.Wait()
		})
	}
}

func TestShouldRespond(t *testing.T) {
	tests := []struct {
		name       string
		connection *Connection
		request    *discovery.DiscoveryRequest
		response   bool
	}{
		{
			name: "initial request",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl: v3.ClusterType,
			},
			response: true,
		},
		{
			name: "ack",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.ClusterType: {
							VersionSent: "v1",
							NonceSent:   "nonce",
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
			},
			response: false,
		},
		{
			name: "nack",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.ClusterType: {
							VersionSent: "v1",
							NonceSent:   "nonce",
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "stale nonce",
			},
			response: false,
		},
		{
			name: "reconnect",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.ClusterType,
				VersionInfo:   "v1",
				ResponseNonce: "reconnect nonce",
			},
			response: true,
		},
		{
			name: "resources change",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.EndpointType: {
							VersionSent:   "v1",
							NonceSent:     "nonce",
							ResourceNames: []string{"cluster1"},
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.EndpointType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
				ResourceNames: []string{"cluster1", "cluster2"},
			},
			response: true,
		},
		{
			name: "ack with same resources",
			connection: &Connection{
				node: &model.Proxy{
					Active: map[string]*model.WatchedResource{
						v3.EndpointType: {
							VersionSent:   "v1",
							NonceSent:     "nonce",
							ResourceNames: []string{"cluster2", "cluster1"},
						},
					},
				},
			},
			request: &discovery.DiscoveryRequest{
				TypeUrl:       v3.EndpointType,
				VersionInfo:   "v1",
				ResponseNonce: "nonce",
				ResourceNames: []string{"cluster1", "cluster2"},
			},
			response: false,
		},
	}

	metric := monitoring.NewSum("test", "test reject metric")

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := NewFakeDiscoveryServer(t, FakeOptions{})
			if response := s.Discovery.shouldRespond(tt.connection, metric, tt.request); response != tt.response {
				t.Fatalf("Unexpected value for response, expected %v, got %v", tt.response, response)
			}
			if tt.name != "reconnect" && tt.response {
				if tt.connection.node.Active[tt.request.TypeUrl].VersionAcked != tt.request.VersionInfo &&
					tt.connection.node.Active[tt.request.TypeUrl].NonceAcked != tt.request.ResponseNonce {
					t.Fatalf("Version & Nonce not updated properly")
				}
			}
		})
	}
}
<|endoftext|>


<commit_before>// Copyright 2016 The prometheus-operator 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 alertmanager

import (
	"fmt"
	"net/url"
	"path"

	apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/pkg/api/resource"
	"k8s.io/client-go/pkg/api/v1"
	"k8s.io/client-go/pkg/apis/apps/v1beta1"
	"k8s.io/client-go/pkg/util/intstr"

	"github.com/coreos/prometheus-operator/pkg/client/monitoring/v1alpha1"
)

const (
	governingServiceName = "alertmanager-operated"
	defaultBaseImage     = "quay.io/prometheus/alertmanager"
	defaultVersion       = "v0.5.1"
)

var (
	minReplicas int32 = 1
)

func makeStatefulSet(am *v1alpha1.Alertmanager, old *v1beta1.StatefulSet, config Config) *v1beta1.StatefulSet {
	// TODO(fabxc): is this the right point to inject defaults?
	// Ideally we would do it before storing but that's currently not possible.
	// Potentially an update handler on first insertion.

	if am.Spec.BaseImage == "" {
		am.Spec.BaseImage = defaultBaseImage
	}
	if am.Spec.Version == "" {
		am.Spec.Version = defaultVersion
	}
	if am.Spec.Replicas != nil && *am.Spec.Replicas < minReplicas {
		am.Spec.Replicas = &minReplicas
	}

	statefulset := &v1beta1.StatefulSet{
		ObjectMeta: apimetav1.ObjectMeta{
			Name:        prefixedName(am.Name),
			Labels:      am.ObjectMeta.Labels,
			Annotations: am.ObjectMeta.Annotations,
		},
		Spec: makeStatefulSetSpec(am, config),
	}
	if vc := am.Spec.Storage; vc == nil {
		statefulset.Spec.Template.Spec.Volumes = append(statefulset.Spec.Template.Spec.Volumes, v1.Volume{
			Name: volumeName(am.Name),
			VolumeSource: v1.VolumeSource{
				EmptyDir: &v1.EmptyDirVolumeSource{},
			},
		})
	} else {
		pvc := v1.PersistentVolumeClaim{
			ObjectMeta: apimetav1.ObjectMeta{
				Name: volumeName(am.Name),
			},
			Spec: v1.PersistentVolumeClaimSpec{
				AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
				Resources:   vc.Resources,
				Selector:    vc.Selector,
			},
		}
		if len(vc.Class) > 0 {
			pvc.ObjectMeta.Annotations = map[string]string{
				"volume.beta.kubernetes.io/storage-class": vc.Class,
			}
		}
		statefulset.Spec.VolumeClaimTemplates = append(statefulset.Spec.VolumeClaimTemplates, pvc)
	}

	if old != nil {
		statefulset.Annotations = old.Annotations
	}
	return statefulset
}

func makeStatefulSetService(p *v1alpha1.Alertmanager) *v1.Service {
	svc := &v1.Service{
		ObjectMeta: apimetav1.ObjectMeta{
			Name: governingServiceName,
			Labels: map[string]string{
				"operated-alertmanager": "true",
			},
		},
		Spec: v1.ServiceSpec{
			ClusterIP: "None",
			Ports: []v1.ServicePort{
				{
					Name:       "web",
					Port:       9093,
					TargetPort: intstr.FromInt(9093),
					Protocol:   v1.ProtocolTCP,
				},
				{
					Name:       "mesh",
					Port:       6783,
					TargetPort: intstr.FromInt(6783),
					Protocol:   v1.ProtocolTCP,
				},
			},
			Selector: map[string]string{
				"app": "alertmanager",
			},
		},
	}
	return svc
}

func makeStatefulSetSpec(a *v1alpha1.Alertmanager, config Config) v1beta1.StatefulSetSpec {
	image := fmt.Sprintf("%s:%s", a.Spec.BaseImage, a.Spec.Version)

	commands := []string{
		"/bin/alertmanager",
		fmt.Sprintf("-config.file=%s", "/etc/alertmanager/config/alertmanager.yaml"),
		fmt.Sprintf("-web.listen-address=:%d", 9093),
		fmt.Sprintf("-mesh.listen-address=:%d", 6783),
		fmt.Sprintf("-storage.path=%s", "/etc/alertmanager/data"),
	}

	webRoutePrefix := ""
	if a.Spec.ExternalURL != "" {
		commands = append(commands, "-web.external-url="+a.Spec.ExternalURL)
		extUrl, err := url.Parse(a.Spec.ExternalURL)
		if err == nil {
			webRoutePrefix = extUrl.Path
		}
	}

	localReloadURL := &url.URL{
		Scheme: "http",
		Host:   "localhost:9093",
		Path:   path.Clean(webRoutePrefix + "/-/reload"),
	}

	for i := int32(0); i < *a.Spec.Replicas; i++ {
		commands = append(commands, fmt.Sprintf("-mesh.peer=%s-%d.%s.%s.svc", prefixedName(a.Name), i, "alertmanager", a.Namespace))
	}

	terminationGracePeriod := int64(0)
	return v1beta1.StatefulSetSpec{
		ServiceName: governingServiceName,
		Replicas:    a.Spec.Replicas,
		Template: v1.PodTemplateSpec{
			ObjectMeta: apimetav1.ObjectMeta{
				Labels: map[string]string{
					"app":          "alertmanager",
					"alertmanager": a.Name,
				},
			},
			Spec: v1.PodSpec{
				TerminationGracePeriodSeconds: &terminationGracePeriod,
				Containers: []v1.Container{
					{
						Command: commands,
						Name:    "alertmanager",
						Image:   image,
						Ports: []v1.ContainerPort{
							{
								Name:          "web",
								ContainerPort: 9093,
								Protocol:      v1.ProtocolTCP,
							},
							{
								Name:          "mesh",
								ContainerPort: 6783,
								Protocol:      v1.ProtocolTCP,
							},
						},
						VolumeMounts: []v1.VolumeMount{
							{
								Name:      "config-volume",
								MountPath: "/etc/alertmanager/config",
							},
							{
								Name:      volumeName(a.Name),
								MountPath: "/var/alertmanager/data",
								SubPath:   subPathForStorage(a.Spec.Storage),
							},
						},
						ReadinessProbe: &v1.Probe{
							Handler: v1.Handler{
								HTTPGet: &v1.HTTPGetAction{
									Path: path.Clean(webRoutePrefix + "/api/v1/status"),
									Port: intstr.FromString("web"),
								},
							},
							InitialDelaySeconds: 3,
							TimeoutSeconds:      3,
							PeriodSeconds:       5,
							FailureThreshold:    10,
						},
					}, {
						Name:  "config-reloader",
						Image: config.ConfigReloaderImage,
						Args: []string{
							fmt.Sprintf("-webhook-url=%s", localReloadURL),
							"-volume-dir=/etc/alertmanager/config",
						},
						VolumeMounts: []v1.VolumeMount{
							{
								Name:      "config-volume",
								ReadOnly:  true,
								MountPath: "/etc/alertmanager/config",
							},
						},
						Resources: v1.ResourceRequirements{
							Limits: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("5m"),
								v1.ResourceMemory: resource.MustParse("10Mi"),
							},
						},
					},
				},
				Volumes: []v1.Volume{
					{
						Name: "config-volume",
						VolumeSource: v1.VolumeSource{
							ConfigMap: &v1.ConfigMapVolumeSource{
								LocalObjectReference: v1.LocalObjectReference{
									Name: configConfigMapName(a.Name),
								},
							},
						},
					},
				},
			},
		},
	}
}

func configConfigMapName(name string) string {
	return prefixedName(name)
}

func volumeName(name string) string {
	return fmt.Sprintf("%s-db", prefixedName(name))
}

func prefixedName(name string) string {
	return fmt.Sprintf("alertmanager-%s", name)
}

func subPathForStorage(s *v1alpha1.StorageSpec) string {
	if s == nil {
		return ""
	}

	return "alertmanager-db"
}
<commit_msg>alertmanager: fix mesh peer connection address<commit_after>// Copyright 2016 The prometheus-operator 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 alertmanager

import (
	"fmt"
	"net/url"
	"path"

	apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/pkg/api/resource"
	"k8s.io/client-go/pkg/api/v1"
	"k8s.io/client-go/pkg/apis/apps/v1beta1"
	"k8s.io/client-go/pkg/util/intstr"

	"github.com/coreos/prometheus-operator/pkg/client/monitoring/v1alpha1"
)

const (
	governingServiceName = "alertmanager-operated"
	defaultBaseImage     = "quay.io/prometheus/alertmanager"
	defaultVersion       = "v0.5.1"
)

var (
	minReplicas int32 = 1
)

func makeStatefulSet(am *v1alpha1.Alertmanager, old *v1beta1.StatefulSet, config Config) *v1beta1.StatefulSet {
	// TODO(fabxc): is this the right point to inject defaults?
	// Ideally we would do it before storing but that's currently not possible.
	// Potentially an update handler on first insertion.

	if am.Spec.BaseImage == "" {
		am.Spec.BaseImage = defaultBaseImage
	}
	if am.Spec.Version == "" {
		am.Spec.Version = defaultVersion
	}
	if am.Spec.Replicas != nil && *am.Spec.Replicas < minReplicas {
		am.Spec.Replicas = &minReplicas
	}

	statefulset := &v1beta1.StatefulSet{
		ObjectMeta: apimetav1.ObjectMeta{
			Name:        prefixedName(am.Name),
			Labels:      am.ObjectMeta.Labels,
			Annotations: am.ObjectMeta.Annotations,
		},
		Spec: makeStatefulSetSpec(am, config),
	}
	if vc := am.Spec.Storage; vc == nil {
		statefulset.Spec.Template.Spec.Volumes = append(statefulset.Spec.Template.Spec.Volumes, v1.Volume{
			Name: volumeName(am.Name),
			VolumeSource: v1.VolumeSource{
				EmptyDir: &v1.EmptyDirVolumeSource{},
			},
		})
	} else {
		pvc := v1.PersistentVolumeClaim{
			ObjectMeta: apimetav1.ObjectMeta{
				Name: volumeName(am.Name),
			},
			Spec: v1.PersistentVolumeClaimSpec{
				AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
				Resources:   vc.Resources,
				Selector:    vc.Selector,
			},
		}
		if len(vc.Class) > 0 {
			pvc.ObjectMeta.Annotations = map[string]string{
				"volume.beta.kubernetes.io/storage-class": vc.Class,
			}
		}
		statefulset.Spec.VolumeClaimTemplates = append(statefulset.Spec.VolumeClaimTemplates, pvc)
	}

	if old != nil {
		statefulset.Annotations = old.Annotations
	}
	return statefulset
}

func makeStatefulSetService(p *v1alpha1.Alertmanager) *v1.Service {
	svc := &v1.Service{
		ObjectMeta: apimetav1.ObjectMeta{
			Name: governingServiceName,
			Labels: map[string]string{
				"operated-alertmanager": "true",
			},
		},
		Spec: v1.ServiceSpec{
			ClusterIP: "None",
			Ports: []v1.ServicePort{
				{
					Name:       "web",
					Port:       9093,
					TargetPort: intstr.FromInt(9093),
					Protocol:   v1.ProtocolTCP,
				},
				{
					Name:       "mesh",
					Port:       6783,
					TargetPort: intstr.FromInt(6783),
					Protocol:   v1.ProtocolTCP,
				},
			},
			Selector: map[string]string{
				"app": "alertmanager",
			},
		},
	}
	return svc
}

func makeStatefulSetSpec(a *v1alpha1.Alertmanager, config Config) v1beta1.StatefulSetSpec {
	image := fmt.Sprintf("%s:%s", a.Spec.BaseImage, a.Spec.Version)

	commands := []string{
		"/bin/alertmanager",
		fmt.Sprintf("-config.file=%s", "/etc/alertmanager/config/alertmanager.yaml"),
		fmt.Sprintf("-web.listen-address=:%d", 9093),
		fmt.Sprintf("-mesh.listen-address=:%d", 6783),
		fmt.Sprintf("-storage.path=%s", "/etc/alertmanager/data"),
	}

	webRoutePrefix := ""
	if a.Spec.ExternalURL != "" {
		commands = append(commands, "-web.external-url="+a.Spec.ExternalURL)
		extUrl, err := url.Parse(a.Spec.ExternalURL)
		if err == nil {
			webRoutePrefix = extUrl.Path
		}
	}

	localReloadURL := &url.URL{
		Scheme: "http",
		Host:   "localhost:9093",
		Path:   path.Clean(webRoutePrefix + "/-/reload"),
	}

	for i := int32(0); i < *a.Spec.Replicas; i++ {
		commands = append(commands, fmt.Sprintf("-mesh.peer=%s-%d.%s.%s.svc", prefixedName(a.Name), i, governingServiceName, a.Namespace))
	}

	terminationGracePeriod := int64(0)
	return v1beta1.StatefulSetSpec{
		ServiceName: governingServiceName,
		Replicas:    a.Spec.Replicas,
		Template: v1.PodTemplateSpec{
			ObjectMeta: apimetav1.ObjectMeta{
				Labels: map[string]string{
					"app":          "alertmanager",
					"alertmanager": a.Name,
				},
			},
			Spec: v1.PodSpec{
				TerminationGracePeriodSeconds: &terminationGracePeriod,
				Containers: []v1.Container{
					{
						Command: commands,
						Name:    "alertmanager",
						Image:   image,
						Ports: []v1.ContainerPort{
							{
								Name:          "web",
								ContainerPort: 9093,
								Protocol:      v1.ProtocolTCP,
							},
							{
								Name:          "mesh",
								ContainerPort: 6783,
								Protocol:      v1.ProtocolTCP,
							},
						},
						VolumeMounts: []v1.VolumeMount{
							{
								Name:      "config-volume",
								MountPath: "/etc/alertmanager/config",
							},
							{
								Name:      volumeName(a.Name),
								MountPath: "/var/alertmanager/data",
								SubPath:   subPathForStorage(a.Spec.Storage),
							},
						},
						ReadinessProbe: &v1.Probe{
							Handler: v1.Handler{
								HTTPGet: &v1.HTTPGetAction{
									Path: path.Clean(webRoutePrefix + "/api/v1/status"),
									Port: intstr.FromString("web"),
								},
							},
							InitialDelaySeconds: 3,
							TimeoutSeconds:      3,
							PeriodSeconds:       5,
							FailureThreshold:    10,
						},
					}, {
						Name:  "config-reloader",
						Image: config.ConfigReloaderImage,
						Args: []string{
							fmt.Sprintf("-webhook-url=%s", localReloadURL),
							"-volume-dir=/etc/alertmanager/config",
						},
						VolumeMounts: []v1.VolumeMount{
							{
								Name:      "config-volume",
								ReadOnly:  true,
								MountPath: "/etc/alertmanager/config",
							},
						},
						Resources: v1.ResourceRequirements{
							Limits: v1.ResourceList{
								v1.ResourceCPU:    resource.MustParse("5m"),
								v1.ResourceMemory: resource.MustParse("10Mi"),
							},
						},
					},
				},
				Volumes: []v1.Volume{
					{
						Name: "config-volume",
						VolumeSource: v1.VolumeSource{
							ConfigMap: &v1.ConfigMapVolumeSource{
								LocalObjectReference: v1.LocalObjectReference{
									Name: configConfigMapName(a.Name),
								},
							},
						},
					},
				},
			},
		},
	}
}

func configConfigMapName(name string) string {
	return prefixedName(name)
}

func volumeName(name string) string {
	return fmt.Sprintf("%s-db", prefixedName(name))
}

func prefixedName(name string) string {
	return fmt.Sprintf("alertmanager-%s", name)
}

func subPathForStorage(s *v1alpha1.StorageSpec) string {
	if s == nil {
		return ""
	}

	return "alertmanager-db"
}
<|endoftext|>


<commit_before>/*
Copyright 2020 The Knative 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 v1beta1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"knative.dev/pkg/apis"
	"knative.dev/pkg/apis/duck"
)

// +genduck

var _ duck.Implementable = (*Rabbit)(nil)

// At the moment, a Rabbit is the subset of RabbitmqCluster from https://github.com/rabbitmq/cluster-operator/

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Rabbit is a skeleton type wrapping limited fields from RabbitmqCluster.
// This is not a real resource.
type Rabbit struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	// RabbitSpec
	Spec RabbitSpec `json:"spec"`

	// RabbitmqClusterStatus
	Status RabbitStatus `json:"status"`
}

// RabbiSpec
type RabbitSpec struct{}

// RabbitStatus.
type RabbitStatus struct {
	// RabbitAdmin identifies information on internal resources.
	Admin *RabbitAdmin `json:"admin,omitempty"`
}

type RabbitAdmin struct {
	SecretReference  *RabbitReference `json:"secretReference,omitempty"`
	ServiceReference *RabbitReference `json:"serviceReference,omitempty"`
}

type RabbitReference struct {
	Name      string            `json:"name,omitempty"`
	Namespace string            `json:"namespace,omitempty"`
	Keys      map[string]string `json:"keys,omitempty"`
}

var (
	// Verify RabbitmqClusterType resources meet duck contracts.
	_ duck.Populatable = (*Rabbit)(nil)
	_ apis.Listable    = (*Rabbit)(nil)
)

// GetFullType implements duck.Implementable
func (s *Rabbit) GetFullType() duck.Populatable {
	return &Rabbit{}
}

// Populate implements duck.Populatable
func (c *Rabbit) Populate() {
	// TOOD: fill this out.
}

// GetListType implements apis.Listable
func (c *Rabbit) GetListType() runtime.Object {
	return &RabbitList{}
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// RabbitList is a list of Rabbit resources
type RabbitList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Rabbit `json:"items"`
}
<commit_msg>[master] Fix spelling errors (#33)<commit_after>/*
Copyright 2020 The Knative 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 v1beta1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"knative.dev/pkg/apis"
	"knative.dev/pkg/apis/duck"
)

// +genduck

var _ duck.Implementable = (*Rabbit)(nil)

// At the moment, a Rabbit is the subset of RabbitmqCluster from https://github.com/rabbitmq/cluster-operator/

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Rabbit is a skeleton type wrapping limited fields from RabbitmqCluster.
// This is not a real resource.
type Rabbit struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	// RabbitSpec
	Spec RabbitSpec `json:"spec"`

	// RabbitmqClusterStatus
	Status RabbitStatus `json:"status"`
}

// RabbiSpec
type RabbitSpec struct{}

// RabbitStatus.
type RabbitStatus struct {
	// RabbitAdmin identifies information on internal resources.
	Admin *RabbitAdmin `json:"admin,omitempty"`
}

type RabbitAdmin struct {
	SecretReference  *RabbitReference `json:"secretReference,omitempty"`
	ServiceReference *RabbitReference `json:"serviceReference,omitempty"`
}

type RabbitReference struct {
	Name      string            `json:"name,omitempty"`
	Namespace string            `json:"namespace,omitempty"`
	Keys      map[string]string `json:"keys,omitempty"`
}

var (
	// Verify RabbitmqClusterType resources meet duck contracts.
	_ duck.Populatable = (*Rabbit)(nil)
	_ apis.Listable    = (*Rabbit)(nil)
)

// GetFullType implements duck.Implementable
func (s *Rabbit) GetFullType() duck.Populatable {
	return &Rabbit{}
}

// Populate implements duck.Populatable
func (c *Rabbit) Populate() {
	// TODO: fill this out.
}

// GetListType implements apis.Listable
func (c *Rabbit) GetListType() runtime.Object {
	return &RabbitList{}
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// RabbitList is a list of Rabbit resources
type RabbitList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Rabbit `json:"items"`
}
<|endoftext|>


<commit_before>package automation

import (
	"context"
	"fmt"
	"log"

	"google.golang.org/api/compute/v1"
)

// RecommendationApplyService is used to apply recommendations for VMs
type RecommendationApplyService struct {
	instancesService *compute.InstancesService
}

// NewRecommendationApplyService creates new RecommendationApplyService
func NewRecommendationApplyService(ctx context.Context) (*RecommendationApplyService, error) {
	computeService, err := compute.NewService(ctx)
	if err != nil {
		return nil, err
	}
	return &RecommendationApplyService{
		instancesService: compute.NewInstancesService(computeService),
	}, nil
}

func (s *RecommendationApplyService) stopInstance(project string, zone string, instance string) {
	_, err := s.instancesService.Stop(project, zone, instance).Do()
	if err != nil {
		log.Fatal(err)
	}
}

func (s *RecommendationApplyService) changeMachineType(project string, zone string, instance string, machineType string) {
	machineType = fmt.Sprintf("zones/%s/machineTypes/%s", zone, machineType)
	request := &compute.InstancesSetMachineTypeRequest{MachineType: machineType}
	_, err := s.instancesService.SetMachineType(project, zone, instance, request).Do()
	if err != nil {
		log.Fatal(err)
	}
}
<commit_msg>forgot that file<commit_after>package automation

import (
	"fmt"

	"google.golang.org/api/compute/v1"
)

func (s *googleService) StopInstance(project string, zone string, instance string) error {
	_, err := s.instancesService.Stop(project, zone, instance).Do()
	return err
}

func (s *googleService) ChangeMachineType(project string, zone string, instance string, machineType string) error {
	machineType = fmt.Sprintf("zones/%s/machineTypes/%s", zone, machineType)
	request := &compute.InstancesSetMachineTypeRequest{MachineType: machineType}
	_, err := s.instancesService.SetMachineType(project, zone, instance, request).Do()
	return err
}
<|endoftext|>


<commit_before>// Copyright 2020 Google LLC
//
// 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 discovery

import (
	"fmt"
	"strings"
	"time"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
)

// Ref: https://pkg.go.dev/k8s.io/client-go/discovery

const (
	defaultTimeout               = 60 * time.Second
	gatekeeperConstraintCategory = "constraint"
)

// Client is a wrapper for discovery.DiscoveryClient
type Client struct {
	discovery discovery.DiscoveryInterface
	log       logr.Logger
	timeout   time.Duration
}

// NewClient creates a new discovery client
func NewClient(log logr.Logger, config *rest.Config) (*Client, error) {
	discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
	if err != nil {
		return nil, err
	}
	return &Client{
		discovery: discoveryClient,
		log:       log,
		timeout:   defaultTimeout,
	}, nil
}

// GetConstraintGroupResources returns constraint types by category
func (c *Client) GetConstraintGroupResources() ([]schema.GroupResource, error) {
	c.log.V(2).Info("discovering resources types for category", "category", gatekeeperConstraintCategory)
	categoryExpander := restmapper.NewDiscoveryCategoryExpander(c.discovery)
	groupResources, exists := categoryExpander.Expand(gatekeeperConstraintCategory)
	if !exists {
		return nil, fmt.Errorf("could not find resource types with category <%v>", gatekeeperConstraintCategory)
	}
	return groupResources, nil
}

// CreateKindToGVRMap builds a mapping from kind to a slice of possible
// GroupVersionResources based on API resources from the discovery.DiscoveryClient.
// It does not map subresources (e.g., pods/status).
// This is a hack to work around the limited data available in constraint
// violations (kind only, no API group or version).
// Remove this as soon as a release containing this PR is available:
// https://github.com/open-policy-agent/gatekeeper/pull/855
func (c *Client) CreateKindToGVRMap() (map[string][]schema.GroupVersionResource, error) {
	c.log.V(1).Info("creating Kind to GroupVersionResource mappings")
	kindToGVR := map[string][]schema.GroupVersionResource{}
	apiGroupResources, err := restmapper.GetAPIGroupResources(c.discovery)
	if err != nil {
		return nil, err
	}
	for _, apiGroupResource := range apiGroupResources {
		group := apiGroupResource.Group.Name
		for version, versionedResource := range apiGroupResource.VersionedResources {
			for _, apiResource := range versionedResource {
				if !strings.Contains(apiResource.Name, "/") {
					// conditional to skip subresources, such as `pods/status`
					gvr := schema.GroupVersionResource{
						Group:    group,
						Version:  version,
						Resource: apiResource.Name,
					}
					kindToGVR[apiResource.Kind] = append(kindToGVR[apiResource.Kind], gvr)
				}
			}
		}
	}
	if c.log.V(2).Enabled() {
		for kind, gvrs := range kindToGVR {
			c.log.V(2).Info("kindToGVRMapping", "kind", kind, "GVRs", gvrs)
		}
	}
	return kindToGVR, nil
}

// SetTimeout for calls to the API server
func (c *Client) SetTimeout(timeout time.Duration) error {
	if timeout.Seconds() < 0 {
		return fmt.Errorf("Invalid timeout: %v", timeout)
	}
	c.timeout = timeout
	return nil
}
<commit_msg>Sync findings even when no constraints exist<commit_after>// Copyright 2020 Google LLC
//
// 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 discovery

import (
	"fmt"
	"strings"
	"time"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
)

// Ref: https://pkg.go.dev/k8s.io/client-go/discovery

const (
	defaultTimeout               = 60 * time.Second
	gatekeeperConstraintCategory = "constraint"
)

// Client is a wrapper for discovery.DiscoveryClient
type Client struct {
	discovery discovery.DiscoveryInterface
	log       logr.Logger
	timeout   time.Duration
}

// NewClient creates a new discovery client
func NewClient(log logr.Logger, config *rest.Config) (*Client, error) {
	discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
	if err != nil {
		return nil, err
	}
	return &Client{
		discovery: discoveryClient,
		log:       log,
		timeout:   defaultTimeout,
	}, nil
}

// GetConstraintGroupResources returns constraint types by category
func (c *Client) GetConstraintGroupResources() ([]schema.GroupResource, error) {
	c.log.V(2).Info("discovering resources types for category", "category", gatekeeperConstraintCategory)
	categoryExpander := restmapper.NewDiscoveryCategoryExpander(c.discovery)
	groupResources, exists := categoryExpander.Expand(gatekeeperConstraintCategory)
	if !exists {
		c.log.Info("could not find constraint resource types", "category", gatekeeperConstraintCategory)
	}
	return groupResources, nil
}

// CreateKindToGVRMap builds a mapping from kind to a slice of possible
// GroupVersionResources based on API resources from the discovery.DiscoveryClient.
// It does not map subresources (e.g., pods/status).
// This is a hack to work around the limited data available in constraint
// violations (kind only, no API group or version).
// Remove this as soon as a release containing this PR is available:
// https://github.com/open-policy-agent/gatekeeper/pull/855
func (c *Client) CreateKindToGVRMap() (map[string][]schema.GroupVersionResource, error) {
	c.log.V(1).Info("creating Kind to GroupVersionResource mappings")
	kindToGVR := map[string][]schema.GroupVersionResource{}
	apiGroupResources, err := restmapper.GetAPIGroupResources(c.discovery)
	if err != nil {
		return nil, err
	}
	for _, apiGroupResource := range apiGroupResources {
		group := apiGroupResource.Group.Name
		for version, versionedResource := range apiGroupResource.VersionedResources {
			for _, apiResource := range versionedResource {
				if !strings.Contains(apiResource.Name, "/") {
					// conditional to skip subresources, such as `pods/status`
					gvr := schema.GroupVersionResource{
						Group:    group,
						Version:  version,
						Resource: apiResource.Name,
					}
					kindToGVR[apiResource.Kind] = append(kindToGVR[apiResource.Kind], gvr)
				}
			}
		}
	}
	if c.log.V(2).Enabled() {
		for kind, gvrs := range kindToGVR {
			c.log.V(2).Info("kindToGVRMapping", "kind", kind, "GVRs", gvrs)
		}
	}
	return kindToGVR, nil
}

// SetTimeout for calls to the API server
func (c *Client) SetTimeout(timeout time.Duration) error {
	if timeout.Seconds() < 0 {
		return fmt.Errorf("Invalid timeout: %v", timeout)
	}
	c.timeout = timeout
	return nil
}
<|endoftext|>


<commit_before>package discovery

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"sort"
	"strings"
	"time"

	"net/http"

	log "github.com/Sirupsen/logrus"
	etcd "github.com/coreos/etcd/client"
	"github.com/matt-deboer/etcdcd/pkg/platform"
)

// Discovery provides correct startup details for etcd with respect to
// known vs. expected cluster membership
type Discovery struct {
	ConfigFile           string
	Platform             string
	ClientPort           int
	ServerPort           int
	ClientScheme         string
	ServerScheme         string
	MaxTries             int
	ProxyMode            bool
	MasterFilter         string
	DryRun               bool
	IgnoreNamingMismatch bool
	MinimumUptimeToJoin  time.Duration
}

func findMemberByName(members []etcd.Member, name string) *etcd.Member {
	for _, member := range members {
		if name == member.Name {
			return &member
		}
	}
	return nil
}

func containsMember(members []etcd.Member, member etcd.Member) bool {
	for _, m := range members {
		for _, peerURL := range m.PeerURLs {
			for _, memberPeerURL := range member.PeerURLs {
				if peerURL == memberPeerURL {
					return true
				}
			}
		}
	}
	return false
}

// DiscoverEnvironment produces an environment hash
func (d *Discovery) DiscoverEnvironment() (map[string]string, error) {

	p, err := platform.Get(d.Platform, d.ConfigFile)
	if p == nil {
		if err != nil {
			return nil, err
		}
		return nil, errors.New("No such platform: " + d.Platform)
	}

	var expectedMembers []etcd.Member
	for tries := 0; tries < d.MaxTries && len(expectedMembers) == 0; tries++ {
		if members, err := p.ExpectedMembers(d.MasterFilter, d.ClientScheme,
			d.ClientPort, d.ServerScheme, d.ServerPort); err == nil {
			for _, m := range members {
				// have to cast here because of golang type-system--ugh!
				expectedMembers = append(expectedMembers, etcd.Member(m))
				if len(m.PeerURLs) == 0 {
					log.Fatalf("Platform %s returned an invalid member which will be ignored: %#v", d.Platform, m)
				}
			}
		} else {
			return nil, err
		}
		if len(expectedMembers) == 0 {
			sleepTime := (2 * time.Second)
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Failed to resolve expected members; sleeping for %s", sleepTime)
			}
			time.Sleep(sleepTime)
		}
	}

	if len(expectedMembers) == 0 {
		log.Fatal("Failed to determine expected members")
	} else if log.GetLevel() >= log.DebugLevel {
		log.Debugf("Expected cluster members: %v#", expectedMembers)
	}
	sort.Slice(expectedMembers, func(i, j int) bool { return expectedMembers[i].Name < expectedMembers[j].Name })

	localMaster := findMemberByName(expectedMembers, p.LocalInstanceName())
	membersAPI, currentMembers, uptime, err := d.resolveMembersAndAPI(expectedMembers, localMaster)

	environment := map[string]string{}
	environment["ETCD_NAME"] = p.LocalInstanceName()
	environment["ETCD_INITIAL_CLUSTER"] = initialClusterString(expectedMembers)

	if localMaster != nil {
		if log.GetLevel() >= log.DebugLevel {
			log.Debugf("Local master: %#v", *localMaster)
		}
		// this instance is an expected master
		if len(currentMembers) > 0 && !containsMember(currentMembers, *localMaster) && uptime >= d.MinimumUptimeToJoin {
			// there is an existing cluster
			if err = d.assertSaneClusterState(expectedMembers, currentMembers); err != nil {
				log.Fatal(err)
			}

			d.evictBadPeers(membersAPI, expectedMembers, currentMembers)
			log.Infof("Joining existing cluster as a master")
			// TODO: what if we encounter a state where not of the expected masters are
			// members of the current cluster?
			if err := d.joinExistingCluster(membersAPI, expectedMembers, localMaster); err != nil {
				log.Fatal(err)
			}
			environment["ETCD_INITIAL_CLUSTER_STATE"] = "existing"
		} else {
			log.Infof("Creating a new cluster")
			environment["ETCD_INITIAL_CLUSTER_STATE"] = "new"
		}
	} else if d.ProxyMode {
		log.Infof("Proxying existing cluster")
		environment["ETCD_INITIAL_CLUSTER_STATE"] = "existing"
		environment["ETCD_PROXY"] = "on"
	} else {
		return nil, fmt.Errorf(
			"Invalid cluster configuration: localhost (%s) is not an expected master, and not in proxy mode",
			p.LocalInstanceName())
	}
	return environment, nil
}

func initialClusterString(members []etcd.Member) string {
	if len(members) > 0 {
		initialCluster := make([]string, 0, len(members))
		for _, m := range members {
			member := fmt.Sprintf("%s=%s", m.Name, m.PeerURLs[0])
			initialCluster = append(initialCluster, member)
		}
		return strings.Join(initialCluster, ",")
	}
	return ""
}

// Check for mismatched names between expected and current members with
// matching peer URLs; also check for lack of intersection between
// expected and current members--indicating an invalid current (or expected)
// cluster state
func (d *Discovery) assertSaneClusterState(expectedMembers []etcd.Member, currentMembers []etcd.Member) error {
	partialMatchCount := 0
	for _, current := range currentMembers {
		for _, expected := range expectedMembers {
			matchingPeerURL := ""
			for _, expectedPeerURL := range expected.PeerURLs {
				for _, currentPeerURL := range current.PeerURLs {
					if expectedPeerURL == currentPeerURL {
						matchingPeerURL = expectedPeerURL
						partialMatchCount++
						break
					}
				}
				if len(matchingPeerURL) > 0 {
					break
				}
			}
			if len(matchingPeerURL) > 0 && len(current.Name) > 0 && current.Name != expected.Name {
				if !d.IgnoreNamingMismatch {
					return fmt.Errorf("Expected peer %s with peer URL %s already exists with a different name: %s",
						expected.Name, matchingPeerURL, current.Name)
				} else if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Ignoring expected peer %s with peer URL %s already exists with a different name: %s",
						expected.Name, matchingPeerURL, current.Name)
				}
			}
		}
	}
	if partialMatchCount == 0 && len(expectedMembers) > 0 && len(currentMembers) > 0 {
		expectedJSON, _ := json.Marshal(expectedMembers)
		currentJSON, _ := json.Marshal(currentMembers)
		return fmt.Errorf("Invalid cluster state: found no intersection between peer URLs of expected members %s and current members %s",
			expectedJSON, currentJSON)
	}

	return nil
}

func (d *Discovery) evictBadPeers(membersAPI etcd.MembersAPI, expectedMembers []etcd.Member, currentMembers []etcd.Member) {

	for _, peer := range currentMembers {
		if !containsMember(expectedMembers, peer) {
			msg := fmt.Sprintf("Ejecting bad peer %s %v from the cluster:", peer.Name, peer.PeerURLs)
			if d.DryRun {
				log.Infof("DRY_RUN: would have ejected peer %s %v from the cluster", peer.Name, peer.PeerURLs)
			} else {
				for tries := 0; tries < d.MaxTries; tries++ {
					err := membersAPI.Remove(context.Background(), peer.ID)
					if err == nil {
						log.Infof("%s DONE", msg)
						break
					} else if (tries + 1) == d.MaxTries {
						log.Errorf("%s ERROR: %v", msg, err)
					}
				}
			}
		}
	}
}

func (d *Discovery) joinExistingCluster(membersAPI etcd.MembersAPI,
	expectedMembers []etcd.Member, localMember *etcd.Member) error {

	msg := "Joining existing cluster: "
	for tries := 0; tries < d.MaxTries; tries++ {
		if d.DryRun {
			log.Infof("DRY_RUN: would have added %s %v to the cluster", localMember.Name, localMember.PeerURLs)
		} else {
			_, err := membersAPI.Add(context.Background(), localMember.PeerURLs[0])
			if err == nil {
				log.Infof("%s DONE", msg)
				break
			} else if (tries + 1) == d.MaxTries {
				log.Errorf("%s ERROR: %v", msg, err)
				return err
			}
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Retryable error attempting to add local master %#v: %v", localMember, err)
			}
			membersAPI, _, _, err = d.resolveMembersAndAPI(expectedMembers, localMember)
			if err != nil {
				log.Errorf("%s ERROR: %v", msg, err)
				return err
			}
		}
	}
	return nil
}

func (d *Discovery) resolveMembersAndAPI(expectedMembers []etcd.Member,
	localMember *etcd.Member) (membersAPI etcd.MembersAPI, currentMembers []etcd.Member, uptime time.Duration, err error) {

	ctx := context.Background()
	var lastErr error
	for tries := 0; tries <= d.MaxTries; tries++ {
		for _, member := range expectedMembers {
			// don't attempt self connection; afterall, this is intended as a pre-cursor
			// to the actual etcd service on the local host
			if localMember != nil && member.PeerURLs[0] != localMember.PeerURLs[0] {
				cfg := etcd.Config{
					Endpoints: member.ClientURLs,
					Transport: etcd.DefaultTransport,
					// set timeout per request to fail fast when the target endpoint is unavailable
					HeaderTimeoutPerRequest: time.Second,
				}
				etcdClient, err := etcd.New(cfg)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error connecting to %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				}

				membersAPI = etcd.NewMembersAPI(etcdClient)
				leader, err := membersAPI.Leader(ctx)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error getting leader %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				} else if leader == nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error getting leader %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = errors.New("Failed to resolve cluster leader")
				}

				currentMembers, err = membersAPI.List(ctx)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error listing members %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				}

				uptime, err = getUptime(member.ClientURLs[0])
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error listing leader uptime %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				} else if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Member %s reported current leader uptime is %s", member.Name, uptime)
				}

				if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Actual cluster members: %#v", currentMembers)
				}
				return membersAPI, currentMembers, uptime, nil
			}
		}
		if len(currentMembers) == 0 {
			// TODO: what's our timeout here?
			sleepTime := (1 * time.Second)
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Failed to resolve members; sleeping for %s", sleepTime)
			}
			time.Sleep(sleepTime)
		} else {
			break
		}
	}
	return nil, nil, time.Millisecond, lastErr
}

func getUptime(endpoint string) (time.Duration, error) {
	resp, err := http.DefaultClient.Get(endpoint + "/v2/stats/self")
	if err != nil {
		return time.Millisecond, err
	}

	defer resp.Body.Close()
	contents, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return time.Millisecond, err
	}
	stats := make(map[string]interface{})
	err = json.Unmarshal(contents, &stats)
	if err != nil {
		return time.Millisecond, err
	}
	if leaderInfo, ok := stats["leaderInfo"]; ok {
		if uptimeString, ok := leaderInfo.(map[string]interface{})["uptime"]; ok {
			return time.ParseDuration(uptimeString.(string))
		}
	}
	return time.Millisecond, fmt.Errorf("Missing leader uptime info for endpiont %s", endpoint)
}
<commit_msg>still join existing, even if we are already a member<commit_after>package discovery

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"sort"
	"strings"
	"time"

	"net/http"

	log "github.com/Sirupsen/logrus"
	etcd "github.com/coreos/etcd/client"
	"github.com/matt-deboer/etcdcd/pkg/platform"
)

// Discovery provides correct startup details for etcd with respect to
// known vs. expected cluster membership
type Discovery struct {
	ConfigFile           string
	Platform             string
	ClientPort           int
	ServerPort           int
	ClientScheme         string
	ServerScheme         string
	MaxTries             int
	ProxyMode            bool
	MasterFilter         string
	DryRun               bool
	IgnoreNamingMismatch bool
	MinimumUptimeToJoin  time.Duration
}

func findMemberByName(members []etcd.Member, name string) *etcd.Member {
	for _, member := range members {
		if name == member.Name {
			return &member
		}
	}
	return nil
}

func containsMember(members []etcd.Member, member etcd.Member) bool {
	for _, m := range members {
		for _, peerURL := range m.PeerURLs {
			for _, memberPeerURL := range member.PeerURLs {
				if peerURL == memberPeerURL {
					return true
				}
			}
		}
	}
	return false
}

// DiscoverEnvironment produces an environment hash
func (d *Discovery) DiscoverEnvironment() (map[string]string, error) {

	p, err := platform.Get(d.Platform, d.ConfigFile)
	if p == nil {
		if err != nil {
			return nil, err
		}
		return nil, errors.New("No such platform: " + d.Platform)
	}

	var expectedMembers []etcd.Member
	for tries := 0; tries < d.MaxTries && len(expectedMembers) == 0; tries++ {
		if members, err := p.ExpectedMembers(d.MasterFilter, d.ClientScheme,
			d.ClientPort, d.ServerScheme, d.ServerPort); err == nil {
			for _, m := range members {
				// have to cast here because of golang type-system--ugh!
				expectedMembers = append(expectedMembers, etcd.Member(m))
				if len(m.PeerURLs) == 0 {
					log.Fatalf("Platform %s returned an invalid member which will be ignored: %#v", d.Platform, m)
				}
			}
		} else {
			return nil, err
		}
		if len(expectedMembers) == 0 {
			sleepTime := (2 * time.Second)
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Failed to resolve expected members; sleeping for %s", sleepTime)
			}
			time.Sleep(sleepTime)
		}
	}

	if len(expectedMembers) == 0 {
		log.Fatal("Failed to determine expected members")
	} else if log.GetLevel() >= log.DebugLevel {
		log.Debugf("Expected cluster members: %v#", expectedMembers)
	}
	sort.Slice(expectedMembers, func(i, j int) bool { return expectedMembers[i].Name < expectedMembers[j].Name })

	localMaster := findMemberByName(expectedMembers, p.LocalInstanceName())
	membersAPI, currentMembers, uptime, err := d.resolveMembersAndAPI(expectedMembers, localMaster)

	environment := map[string]string{}
	environment["ETCD_NAME"] = p.LocalInstanceName()
	environment["ETCD_INITIAL_CLUSTER"] = initialClusterString(expectedMembers)

	if localMaster != nil {
		if log.GetLevel() >= log.DebugLevel {
			log.Debugf("Local master: %#v", *localMaster)
		}
		// this instance is an expected master
		if len(currentMembers) > 0 && uptime >= d.MinimumUptimeToJoin {
			// there is an existing cluster
			if err = d.assertSaneClusterState(expectedMembers, currentMembers); err != nil {
				log.Fatal(err)
			}

			d.evictBadPeers(membersAPI, expectedMembers, currentMembers)
			log.Infof("Joining existing cluster as a master")
			// TODO: what if we encounter a state where not of the expected masters are
			// members of the current cluster?
			if !containsMember(currentMembers, *localMaster) {
				if err := d.joinExistingCluster(membersAPI, expectedMembers, localMaster); err != nil {
					log.Fatal(err)
				}
			}
			environment["ETCD_INITIAL_CLUSTER_STATE"] = "existing"
		} else {
			log.Infof("Creating a new cluster")
			environment["ETCD_INITIAL_CLUSTER_STATE"] = "new"
		}
	} else if d.ProxyMode {
		log.Infof("Proxying existing cluster")
		environment["ETCD_INITIAL_CLUSTER_STATE"] = "existing"
		environment["ETCD_PROXY"] = "on"
	} else {
		return nil, fmt.Errorf(
			"Invalid cluster configuration: localhost (%s) is not an expected master, and not in proxy mode",
			p.LocalInstanceName())
	}
	return environment, nil
}

func initialClusterString(members []etcd.Member) string {
	if len(members) > 0 {
		initialCluster := make([]string, 0, len(members))
		for _, m := range members {
			member := fmt.Sprintf("%s=%s", m.Name, m.PeerURLs[0])
			initialCluster = append(initialCluster, member)
		}
		return strings.Join(initialCluster, ",")
	}
	return ""
}

// Check for mismatched names between expected and current members with
// matching peer URLs; also check for lack of intersection between
// expected and current members--indicating an invalid current (or expected)
// cluster state
func (d *Discovery) assertSaneClusterState(expectedMembers []etcd.Member, currentMembers []etcd.Member) error {
	partialMatchCount := 0
	for _, current := range currentMembers {
		for _, expected := range expectedMembers {
			matchingPeerURL := ""
			for _, expectedPeerURL := range expected.PeerURLs {
				for _, currentPeerURL := range current.PeerURLs {
					if expectedPeerURL == currentPeerURL {
						matchingPeerURL = expectedPeerURL
						partialMatchCount++
						break
					}
				}
				if len(matchingPeerURL) > 0 {
					break
				}
			}
			if len(matchingPeerURL) > 0 && len(current.Name) > 0 && current.Name != expected.Name {
				if !d.IgnoreNamingMismatch {
					return fmt.Errorf("Expected peer %s with peer URL %s already exists with a different name: %s",
						expected.Name, matchingPeerURL, current.Name)
				} else if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Ignoring expected peer %s with peer URL %s already exists with a different name: %s",
						expected.Name, matchingPeerURL, current.Name)
				}
			}
		}
	}
	if partialMatchCount == 0 && len(expectedMembers) > 0 && len(currentMembers) > 0 {
		expectedJSON, _ := json.Marshal(expectedMembers)
		currentJSON, _ := json.Marshal(currentMembers)
		return fmt.Errorf("Invalid cluster state: found no intersection between peer URLs of expected members %s and current members %s",
			expectedJSON, currentJSON)
	}

	return nil
}

func (d *Discovery) evictBadPeers(membersAPI etcd.MembersAPI, expectedMembers []etcd.Member, currentMembers []etcd.Member) {

	for _, peer := range currentMembers {
		if !containsMember(expectedMembers, peer) {
			msg := fmt.Sprintf("Ejecting bad peer %s %v from the cluster:", peer.Name, peer.PeerURLs)
			if d.DryRun {
				log.Infof("DRY_RUN: would have ejected peer %s %v from the cluster", peer.Name, peer.PeerURLs)
			} else {
				for tries := 0; tries < d.MaxTries; tries++ {
					err := membersAPI.Remove(context.Background(), peer.ID)
					if err == nil {
						log.Infof("%s DONE", msg)
						break
					} else if (tries + 1) == d.MaxTries {
						log.Errorf("%s ERROR: %v", msg, err)
					}
				}
			}
		}
	}
}

func (d *Discovery) joinExistingCluster(membersAPI etcd.MembersAPI,
	expectedMembers []etcd.Member, localMember *etcd.Member) error {

	msg := "Joining existing cluster: "
	for tries := 0; tries < d.MaxTries; tries++ {
		if d.DryRun {
			log.Infof("DRY_RUN: would have added %s %v to the cluster", localMember.Name, localMember.PeerURLs)
		} else {
			_, err := membersAPI.Add(context.Background(), localMember.PeerURLs[0])
			if err == nil {
				log.Infof("%s DONE", msg)
				break
			} else if (tries + 1) == d.MaxTries {
				log.Errorf("%s ERROR: %v", msg, err)
				return err
			}
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Retryable error attempting to add local master %#v: %v", localMember, err)
			}
			membersAPI, _, _, err = d.resolveMembersAndAPI(expectedMembers, localMember)
			if err != nil {
				log.Errorf("%s ERROR: %v", msg, err)
				return err
			}
		}
	}
	return nil
}

func (d *Discovery) resolveMembersAndAPI(expectedMembers []etcd.Member,
	localMember *etcd.Member) (membersAPI etcd.MembersAPI, currentMembers []etcd.Member, uptime time.Duration, err error) {

	ctx := context.Background()
	var lastErr error
	for tries := 0; tries <= d.MaxTries; tries++ {
		for _, member := range expectedMembers {
			// don't attempt self connection; afterall, this is intended as a pre-cursor
			// to the actual etcd service on the local host
			if localMember != nil && member.PeerURLs[0] != localMember.PeerURLs[0] {
				cfg := etcd.Config{
					Endpoints: member.ClientURLs,
					Transport: etcd.DefaultTransport,
					// set timeout per request to fail fast when the target endpoint is unavailable
					HeaderTimeoutPerRequest: time.Second,
				}
				etcdClient, err := etcd.New(cfg)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error connecting to %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				}

				membersAPI = etcd.NewMembersAPI(etcdClient)
				leader, err := membersAPI.Leader(ctx)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error getting leader %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				} else if leader == nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error getting leader %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = errors.New("Failed to resolve cluster leader")
				}

				currentMembers, err = membersAPI.List(ctx)
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error listing members %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				}

				uptime, err = getUptime(member.ClientURLs[0])
				if err != nil {
					if log.GetLevel() >= log.DebugLevel {
						log.Debugf("Error listing leader uptime %s %v, %v", member.Name, member.ClientURLs, err)
					}
					lastErr = err
					continue
				} else if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Member %s reported current leader uptime is %s", member.Name, uptime)
				}

				if log.GetLevel() >= log.DebugLevel {
					log.Debugf("Actual cluster members: %#v", currentMembers)
				}
				return membersAPI, currentMembers, uptime, nil
			}
		}
		if len(currentMembers) == 0 {
			// TODO: what's our timeout here?
			sleepTime := (1 * time.Second)
			if log.GetLevel() >= log.DebugLevel {
				log.Debugf("Failed to resolve members; sleeping for %s", sleepTime)
			}
			time.Sleep(sleepTime)
		} else {
			break
		}
	}
	return nil, nil, time.Millisecond, lastErr
}

func getUptime(endpoint string) (time.Duration, error) {
	resp, err := http.DefaultClient.Get(endpoint + "/v2/stats/self")
	if err != nil {
		return time.Millisecond, err
	}

	defer resp.Body.Close()
	contents, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return time.Millisecond, err
	}
	stats := make(map[string]interface{})
	err = json.Unmarshal(contents, &stats)
	if err != nil {
		return time.Millisecond, err
	}
	if leaderInfo, ok := stats["leaderInfo"]; ok {
		if uptimeString, ok := leaderInfo.(map[string]interface{})["uptime"]; ok {
			return time.ParseDuration(uptimeString.(string))
		}
	}
	return time.Millisecond, fmt.Errorf("Missing leader uptime info for endpiont %s", endpoint)
}
<|endoftext|>