package etcd
import (
"errors"
"fmt"
"net"
"net/url"
"os"
"strings"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/server/v3/embed"
)
const (
MaxEtcdStartAttempts = 3
defaultTimeout = 5 * time.Second
tempDirPrefix = "etcdquota-test-"
)
func StartEtcd() (e *embed.Etcd, c *clientv3.Client, cleanup func(), err error) {
var dir string
dir, err = os.MkdirTemp("", tempDirPrefix)
if err != nil {
return
}
cleanup = func() {
if c != nil {
_ = c.Close()
}
if e != nil {
e.Close()
}
_ = os.RemoveAll(dir)
}
for i := 0; i < MaxEtcdStartAttempts; i++ {
e, err = tryStartEtcd(dir)
if err == nil {
break
}
if strings.Contains(err.Error(), "address already in use") {
continue
}
cleanup()
return
}
if e == nil {
cleanup()
err = errors.New("failed to start etcd: too many attempts")
return
}
select {
case <-e.Server.ReadyNotify():
case <-time.After(defaultTimeout):
cleanup()
err = errors.New("timed out waiting for etcd to start")
return
}
c, err = clientv3.New(clientv3.Config{
Endpoints: []string{e.Config().ListenClientUrls[0].String()},
DialTimeout: defaultTimeout,
})
if err != nil {
cleanup()
}
return
}
func tryStartEtcd(dir string) (*embed.Etcd, error) {
p1, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, err
}
if err := p1.Close(); err != nil {
return nil, err
}
p2, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, err
}
if err := p2.Close(); err != nil {
return nil, err
}
clientURL, _ := url.Parse("http://" + p1.Addr().String())
peerURL, _ := url.Parse("http://" + p2.Addr().String())
cfg := embed.NewConfig()
cfg.Dir = dir
cfg.ListenClientUrls = []url.URL{*clientURL}
cfg.AdvertiseClientUrls = []url.URL{*clientURL}
cfg.ListenPeerUrls = []url.URL{*peerURL}
cfg.AdvertisePeerUrls = []url.URL{*peerURL}
cfg.InitialCluster = fmt.Sprintf("default=%v", peerURL)
cfg.Logger = "zap"
return embed.StartEtcd(cfg)
}