Merge pull request #176 from totherme/162-apiserver-sane-default-constructor

162 apiserver sane default constructor
This commit is contained in:
k8s-ci-robot 2017-12-14 16:03:29 -08:00 committed by GitHub
commit 9afcb2bc07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 716 additions and 304 deletions

View file

@ -3,7 +3,6 @@ package test
import (
"fmt"
"io"
"net/url"
"os/exec"
"time"
@ -14,11 +13,10 @@ import (
// APIServer knows how to run a kubernetes apiserver. Set it up with the path to a precompiled binary.
type APIServer struct {
// The path to the apiserver binary
Path string
AddressManager AddressManager
PathFinder BinPathFinder
ProcessStarter simpleSessionStarter
CertDirManager certDirManager
Config *APIServerConfig
Etcd FixtureProcess
session SimpleSession
stdOut *gbytes.Buffer
@ -32,10 +30,8 @@ type certDirManager interface {
//go:generate counterfeiter . certDirManager
var apiServerBinPathFinder = DefaultBinPathFinder
// NewAPIServer creates a new APIServer Fixture Process
func NewAPIServer(config *APIServerConfig) (*APIServer, error) {
func NewAPIServer() (*APIServer, error) {
starter := func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return gexec.Start(command, out, err)
}
@ -46,8 +42,6 @@ func NewAPIServer(config *APIServerConfig) (*APIServer, error) {
}
return &APIServer{
Path: apiServerBinPathFinder("kube-apiserver"),
Config: config,
ProcessStarter: starter,
CertDirManager: NewTempDirManager(),
Etcd: etcd,
@ -55,34 +49,43 @@ func NewAPIServer(config *APIServerConfig) (*APIServer, error) {
}
// URL returns the URL APIServer is listening on. Clients can use this to connect to APIServer.
func (s *APIServer) URL() string {
return s.Config.APIServerURL
func (s *APIServer) URL() (string, error) {
port, err := s.AddressManager.Port()
if err != nil {
return "", err
}
host, err := s.AddressManager.Host()
if err != nil {
return "", err
}
return fmt.Sprintf("http://%s:%d", host, port), nil
}
// Start starts the apiserver, waits for it to come up, and returns an error, if occoured.
func (s *APIServer) Start() error {
if err := s.Config.Validate(); err != nil {
return err
}
s.ensureInitialized()
err := s.Etcd.Start()
port, addr, err := s.AddressManager.Initialize("localhost")
if err != nil {
return err
}
s.stdOut = gbytes.NewBuffer()
s.stdErr = gbytes.NewBuffer()
certDir, err := s.CertDirManager.Create()
if err != nil {
return err
}
clientURL, err := url.Parse(s.Config.APIServerURL)
err = s.Etcd.Start()
if err != nil {
return err
}
etcdURLString, err := s.Etcd.URL()
if err != nil {
s.Etcd.Stop()
return err
}
args := []string{
"--authorization-mode=Node,RBAC",
"--runtime-config=admissionregistration.k8s.io/v1alpha1",
@ -91,16 +94,16 @@ func (s *APIServer) Start() error {
"--admission-control-config-file=",
"--bind-address=0.0.0.0",
"--storage-backend=etcd3",
fmt.Sprintf("--etcd-servers=%s", s.Etcd.URL()),
fmt.Sprintf("--etcd-servers=%s", etcdURLString),
fmt.Sprintf("--cert-dir=%s", certDir),
fmt.Sprintf("--insecure-port=%s", clientURL.Port()),
fmt.Sprintf("--insecure-bind-address=%s", clientURL.Hostname()),
fmt.Sprintf("--insecure-port=%d", port),
fmt.Sprintf("--insecure-bind-address=%s", addr),
}
detectedStart := s.stdErr.Detect(fmt.Sprintf("Serving insecurely on %s", clientURL.Host))
detectedStart := s.stdErr.Detect(fmt.Sprintf("Serving insecurely on %s:%d", addr, port))
timedOut := time.After(20 * time.Second)
command := exec.Command(s.Path, args...)
command := exec.Command(s.PathFinder("kube-apiserver"), args...)
s.session, err = s.ProcessStarter(command, s.stdOut, s.stdErr)
if err != nil {
return err
@ -114,6 +117,18 @@ func (s *APIServer) Start() error {
}
}
func (s *APIServer) ensureInitialized() {
if s.PathFinder == nil {
s.PathFinder = DefaultBinPathFinder
}
if s.AddressManager == nil {
s.AddressManager = &DefaultAddressManager{}
}
s.stdOut = gbytes.NewBuffer()
s.stdErr = gbytes.NewBuffer()
}
// Stop stops this process gracefully, waits for its termination, and cleans up the cert directory.
func (s *APIServer) Stop() {
if s.session != nil {

View file

@ -1,14 +0,0 @@
package test
import "github.com/asaskevich/govalidator"
// APIServerConfig is a struct holding data to configure the API Server process
type APIServerConfig struct {
APIServerURL string `valid:"required,url"`
}
// Validate checks that the config contains only valid URLs
func (c *APIServerConfig) Validate() error {
_, err := govalidator.ValidateStruct(c)
return err
}

View file

@ -1,32 +0,0 @@
package test_test
import (
. "k8s.io/kubectl/pkg/framework/test"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("APIServerConfig", func() {
It("does not error on valid config", func() {
conf := &APIServerConfig{
APIServerURL: "http://this.is.some.url:1234",
}
err := conf.Validate()
Expect(err).NotTo(HaveOccurred())
})
It("errors on empty config", func() {
conf := &APIServerConfig{}
err := conf.Validate()
Expect(err).To(MatchError(ContainSubstring("APIServerURL: non zero value required")))
})
It("errors on malformed URLs", func() {
conf := &APIServerConfig{
APIServerURL: "something not URLish",
}
err := conf.Validate()
Expect(err).To(MatchError(ContainSubstring("APIServerURL: something not URLish does not validate as url")))
})
})

View file

@ -1,36 +0,0 @@
package test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("NewAPIServer", func() {
var oldAPIServerBinPathFinder BinPathFinder
BeforeEach(func() {
oldAPIServerBinPathFinder = apiServerBinPathFinder
})
AfterEach(func() {
apiServerBinPathFinder = oldAPIServerBinPathFinder
})
It("can construct a properly configured APIServer", func() {
config := &APIServerConfig{
APIServerURL: "some APIServer URL",
}
apiServerBinPathFinder = func(name string) string {
Expect(name).To(Equal("kube-apiserver"))
return "some api server path"
}
apiServer, err := NewAPIServer(config)
Expect(err).NotTo(HaveOccurred())
Expect(apiServer).NotTo(BeNil())
Expect(apiServer.ProcessStarter).NotTo(BeNil())
Expect(apiServer.CertDirManager).NotTo(BeNil())
Expect(apiServer.Path).To(Equal("some api server path"))
Expect(apiServer.Etcd).NotTo(BeNil())
Expect(apiServer.Config).To(Equal(config))
})
})

View file

@ -20,117 +20,198 @@ var _ = Describe("Apiserver", func() {
fakeSession *testfakes.FakeSimpleSession
fakeCertDirManager *testfakes.FakeCertDirManager
apiServer *APIServer
apiServerConfig *APIServerConfig
fakeEtcdProcess *testfakes.FakeFixtureProcess
fakePathFinder *testfakes.FakeBinPathFinder
fakeAddressManager *testfakes.FakeAddressManager
)
BeforeEach(func() {
fakeSession = &testfakes.FakeSimpleSession{}
fakeCertDirManager = &testfakes.FakeCertDirManager{}
fakeEtcdProcess = &testfakes.FakeFixtureProcess{}
fakePathFinder = &testfakes.FakeBinPathFinder{}
fakeAddressManager = &testfakes.FakeAddressManager{}
apiServerConfig = &APIServerConfig{
APIServerURL: "http://this.is.the.API.server:8080",
}
apiServer = &APIServer{
Path: "",
AddressManager: fakeAddressManager,
PathFinder: fakePathFinder.Spy,
CertDirManager: fakeCertDirManager,
Config: apiServerConfig,
Etcd: fakeEtcdProcess,
}
})
It("can be queried for the URL it listens on", func() {
Expect(apiServer.URL()).To(Equal("http://this.is.the.API.server:8080"))
Describe("starting and stopping the server", func() {
Context("when given a path to a binary that runs for a long time", func() {
It("can start and stop that binary", func() {
//TODO Break this long test up
sessionBuffer := gbytes.NewBuffer()
fmt.Fprint(sessionBuffer, "Everything is fine")
fakeSession.BufferReturns(sessionBuffer)
fakeSession.ExitCodeReturnsOnCall(0, -1)
fakeSession.ExitCodeReturnsOnCall(1, 143)
fakePathFinder.Returns("/some/path/to/apiserver")
fakeAddressManager.InitializeReturns(1234, "this.is.the.API.server", nil)
fakeEtcdProcess.URLReturns("the etcd url", nil)
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(command.Args).To(ContainElement("--insecure-port=1234"))
Expect(command.Args).To(ContainElement("--insecure-bind-address=this.is.the.API.server"))
Expect(command.Args).To(ContainElement("--etcd-servers=the etcd url"))
Expect(command.Path).To(Equal("/some/path/to/apiserver"))
fmt.Fprint(err, "Serving insecurely on this.is.the.API.server:1234")
return fakeSession, nil
}
By("Starting the API Server")
err := apiServer.Start()
Expect(err).NotTo(HaveOccurred())
By("...in turn starting Etcd")
Expect(fakeEtcdProcess.StartCallCount()).To(Equal(1),
"the Etcd process should be started exactly once")
By("...in turn calling the PathFinder")
Expect(fakePathFinder.CallCount()).To(Equal(1))
Expect(fakePathFinder.ArgsForCall(0)).To(Equal("kube-apiserver"))
By("...in turn calling the PortFinder")
Expect(fakeAddressManager.InitializeCallCount()).To(Equal(1))
Expect(fakeAddressManager.InitializeArgsForCall(0)).To(Equal("localhost"))
By("...getting the URL of Etcd")
Expect(fakeEtcdProcess.URLCallCount()).To(Equal(1))
Eventually(apiServer).Should(gbytes.Say("Everything is fine"))
Expect(fakeSession.ExitCodeCallCount()).To(Equal(0))
Expect(apiServer).NotTo(gexec.Exit())
Expect(fakeSession.ExitCodeCallCount()).To(Equal(1))
Expect(fakeCertDirManager.CreateCallCount()).To(Equal(1))
By("Stopping the API Server")
apiServer.Stop()
Expect(fakeEtcdProcess.StopCallCount()).To(Equal(1))
Expect(apiServer).To(gexec.Exit(143))
Expect(fakeSession.TerminateCallCount()).To(Equal(1))
Expect(fakeSession.WaitCallCount()).To(Equal(1))
Expect(fakeSession.ExitCodeCallCount()).To(Equal(2))
Expect(fakeCertDirManager.DestroyCallCount()).To(Equal(1))
})
})
Context("when starting etcd fails", func() {
It("propagates the error, and does not start the process", func() {
fakeEtcdProcess.StartReturnsOnCall(0, fmt.Errorf("starting etcd failed"))
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if starting etcd fails")
return nil, nil
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("starting etcd failed")))
})
})
Context("when getting the URL of Etcd fails", func() {
It("propagates the error, stop Etcd and keep APIServer down", func() {
fakeEtcdProcess.URLReturns("", fmt.Errorf("no etcd url"))
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if getting etcd's URL fails")
return nil, nil
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("no etcd url")))
Expect(fakeEtcdProcess.StopCallCount()).To(Equal(1))
})
})
Context("when the certificate directory cannot be created", func() {
It("propagates the error, and does not start any process", func() {
fakeCertDirManager.CreateReturnsOnCall(0, "", fmt.Errorf("Error on cert directory creation."))
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if creating the cert dir fails")
return nil, nil
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("Error on cert directory creation.")))
Expect(fakeEtcdProcess.StartCallCount()).To(Equal(0))
})
})
Context("when the address manager fails to get a new address", func() {
It("propagates the error and does not start any process", func() {
fakeAddressManager.InitializeReturns(0, "", fmt.Errorf("some error finding a free port"))
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if getting a free port fails")
return nil, nil
}
Expect(apiServer.Start()).To(MatchError(ContainSubstring("some error finding a free port")))
Expect(fakeEtcdProcess.StartCallCount()).To(Equal(0))
})
})
Context("when the starter returns an error", func() {
It("propagates the error", func() {
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return nil, fmt.Errorf("Some error in the apiserver starter.")
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("Some error in the apiserver starter.")))
})
})
Context("when we try to stop a server that hasn't been started", func() {
It("is a noop and does not call exit on the session", func() {
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return fakeSession, nil
}
apiServer.Stop()
Expect(fakeSession.ExitCodeCallCount()).To(Equal(0))
})
})
})
Context("when given a path to a binary that runs for a long time", func() {
It("can start and stop that binary", func() {
sessionBuffer := gbytes.NewBuffer()
fmt.Fprint(sessionBuffer, "Everything is fine")
fakeSession.BufferReturns(sessionBuffer)
fakeSession.ExitCodeReturnsOnCall(0, -1)
fakeSession.ExitCodeReturnsOnCall(1, 143)
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
fmt.Fprint(err, "Serving insecurely on this.is.the.API.server:8080")
return fakeSession, nil
}
By("Starting the API Server")
err := apiServer.Start()
Describe("querying the server for its URL", func() {
It("can be queried for the URL it listens on", func() {
fakeAddressManager.HostReturns("the.host.for.api.server", nil)
fakeAddressManager.PortReturns(5678, nil)
apiServerURL, err := apiServer.URL()
Expect(err).NotTo(HaveOccurred())
By("starting Etcd")
Expect(fakeEtcdProcess.StartCallCount()).To(Equal(1),
"the Etcd process should be started exactly once")
Eventually(apiServer).Should(gbytes.Say("Everything is fine"))
Expect(fakeSession.ExitCodeCallCount()).To(Equal(0))
Expect(apiServer).NotTo(gexec.Exit())
Expect(fakeSession.ExitCodeCallCount()).To(Equal(1))
Expect(fakeCertDirManager.CreateCallCount()).To(Equal(1))
By("Stopping the API Server")
apiServer.Stop()
Expect(fakeEtcdProcess.StopCallCount()).To(Equal(1))
Expect(apiServer).To(gexec.Exit(143))
Expect(fakeSession.TerminateCallCount()).To(Equal(1))
Expect(fakeSession.WaitCallCount()).To(Equal(1))
Expect(fakeSession.ExitCodeCallCount()).To(Equal(2))
Expect(fakeCertDirManager.DestroyCallCount()).To(Equal(1))
Expect(apiServerURL).To(Equal("http://the.host.for.api.server:5678"))
})
})
Context("when starting etcd fails", func() {
It("propagates the error, and does not start the process", func() {
fakeEtcdProcess.StartReturnsOnCall(0, fmt.Errorf("starting etcd failed"))
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if starting etcd fails")
return nil, nil
}
Context("when we query for the URL before starting the server", func() {
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("starting etcd failed")))
})
})
Context("and so the addressmanager fails to give us a port", func() {
It("propagates the failure", func() {
fakeAddressManager.PortReturns(0, fmt.Errorf("boom"))
_, err := apiServer.URL()
Expect(err).To(MatchError(ContainSubstring("boom")))
})
})
Context("when the certificate directory cannot be created", func() {
It("propagates the error, and does not start the process", func() {
fakeCertDirManager.CreateReturnsOnCall(0, "", fmt.Errorf("Error on cert directory creation."))
Context("and so the addressmanager fails to give us a host", func() {
It("propagates the failure", func() {
fakeAddressManager.HostReturns("", fmt.Errorf("biff!"))
_, err := apiServer.URL()
Expect(err).To(MatchError(ContainSubstring("biff!")))
})
})
apiServer.ProcessStarter = func(Command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(true).To(BeFalse(),
"the api server process starter shouldn't be called if creating the cert dir fails")
return nil, nil
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("Error on cert directory creation.")))
})
})
Context("when the starter returns an error", func() {
It("propagates the error", func() {
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return nil, fmt.Errorf("Some error in the apiserver starter.")
}
err := apiServer.Start()
Expect(err).To(MatchError(ContainSubstring("Some error in the apiserver starter.")))
})
})
Context("when we try to stop a server that hasn't been started", func() {
It("is a noop and does not call exit on the session", func() {
apiServer.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return fakeSession, nil
}
apiServer.Stop()
Expect(fakeSession.ExitCodeCallCount()).To(Equal(0))
})
})
})

View file

@ -22,6 +22,8 @@ func init() {
// a resolvable path to the binary in question
type BinPathFinder func(symbolicName string) (binPath string)
//go:generate counterfeiter . BinPathFinder
// DefaultBinPathFinder is an implementation of BinPathFinder which checks the an environment
// variable, derived from the symbolic name, and falls back to a default assets location when
// this variable is not set

View file

@ -21,7 +21,8 @@ var _ = Describe("DemoCLI Integration", func() {
})
It("can get a list of pods", func() {
apiURL := fixtures.APIServerURL()
apiURL, err := fixtures.APIServerURL()
Expect(err).NotTo(HaveOccurred())
command := exec.Command(pathToDemoCommand, "listPods", "--api-url", apiURL)
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)

View file

@ -15,7 +15,7 @@ import (
// Etcd knows how to run an etcd server. Set it up with the path to a precompiled binary.
type Etcd struct {
Path string
PathFinder BinPathFinder
ProcessStarter simpleSessionStarter
DataDirManager dataDirManager
Config *EtcdConfig
@ -43,8 +43,6 @@ type SimpleSession interface {
type simpleSessionStarter func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error)
var etcdBinPathFinder = DefaultBinPathFinder
// NewEtcd returns a Etcd process configured with sane defaults
func NewEtcd() (*Etcd, error) {
starter := func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
@ -57,36 +55,23 @@ func NewEtcd() (*Etcd, error) {
}
return &Etcd{
Path: etcdBinPathFinder("etcd"),
ProcessStarter: starter,
DataDirManager: NewTempDirManager(),
Config: config,
}, nil
}
// NewEtcdWithBinaryAndConfig returns a Etcd process, using the handed in binary and config.
func NewEtcdWithBinaryAndConfig(pathToEtcd string, config *EtcdConfig) *Etcd {
starter := func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
return gexec.Start(command, out, err)
}
etcd := &Etcd{
Path: pathToEtcd,
ProcessStarter: starter,
DataDirManager: NewTempDirManager(),
Config: config,
}
return etcd
}
// URL returns the URL Etcd is listening on. Clients can use this to connect to Etcd.
func (e *Etcd) URL() string {
return e.Config.ClientURL
func (e *Etcd) URL() (string, error) {
return e.Config.ClientURL, nil
}
// Start starts the etcd, waits for it to come up, and returns an error, if occoured.
func (e *Etcd) Start() error {
if e.PathFinder == nil {
e.PathFinder = DefaultBinPathFinder
}
if err := e.Config.Validate(); err != nil {
return err
}
@ -120,7 +105,7 @@ func (e *Etcd) Start() error {
"serving insecure client requests on %s", clientURL.Host))
timedOut := time.After(20 * time.Second)
command := exec.Command(e.Path, args...)
command := exec.Command(e.PathFinder("etcd"), args...)
e.session, err = e.ProcessStarter(command, e.stdOut, e.stdErr)
if err != nil {
return err

View file

@ -1,28 +0,0 @@
package test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Etcd", func() {
Context("when constructed with the zero-config constructor", func() {
var (
previousBinPathFinder BinPathFinder
)
BeforeEach(func() {
previousBinPathFinder = etcdBinPathFinder
etcdBinPathFinder = func(name string) (binPath string) {
return "/the/path/to/etcd"
}
})
AfterEach(func() {
etcdBinPathFinder = previousBinPathFinder
})
It("gets a sensible path", func() {
etcd, err := NewEtcd()
Expect(err).NotTo(HaveOccurred())
Expect(etcd.Path).To(Equal("/the/path/to/etcd"))
})
})
})

View file

@ -17,6 +17,7 @@ var _ = Describe("Etcd", func() {
var (
fakeSession *testfakes.FakeSimpleSession
fakeDataDirManager *testfakes.FakeDataDirManager
fakePathFinder *testfakes.FakeBinPathFinder
etcd *Etcd
etcdConfig *EtcdConfig
)
@ -24,6 +25,7 @@ var _ = Describe("Etcd", func() {
BeforeEach(func() {
fakeSession = &testfakes.FakeSimpleSession{}
fakeDataDirManager = &testfakes.FakeDataDirManager{}
fakePathFinder = &testfakes.FakeBinPathFinder{}
etcdConfig = &EtcdConfig{
ClientURL: "http://this.is.etcd.listening.for.clients:1234",
@ -31,14 +33,16 @@ var _ = Describe("Etcd", func() {
}
etcd = &Etcd{
Path: "",
PathFinder: fakePathFinder.Spy,
DataDirManager: fakeDataDirManager,
Config: etcdConfig,
}
})
It("can be queried for the port it listens on", func() {
Expect(etcd.URL()).To(Equal("http://this.is.etcd.listening.for.clients:1234"))
etcdURL, err := etcd.URL()
Expect(err).NotTo(HaveOccurred())
Expect(etcdURL).To(Equal("http://this.is.etcd.listening.for.clients:1234"))
})
Context("when given a path to a binary that runs for a long time", func() {
@ -49,8 +53,10 @@ var _ = Describe("Etcd", func() {
fakeSession.ExitCodeReturnsOnCall(0, -1)
fakeSession.ExitCodeReturnsOnCall(1, 143)
fakePathFinder.ReturnsOnCall(0, "/path/to/some/etcd")
etcd.ProcessStarter = func(command *exec.Cmd, out, err io.Writer) (SimpleSession, error) {
Expect(command.Path).To(Equal("/path/to/some/etcd"))
fmt.Fprint(err, "serving insecure client requests on this.is.etcd.listening.for.clients:1234")
return fakeSession, nil
}
@ -59,6 +65,10 @@ var _ = Describe("Etcd", func() {
err := etcd.Start()
Expect(err).NotTo(HaveOccurred())
By("...in turn calling the PathFinder")
Expect(fakePathFinder.CallCount()).To(Equal(1))
Expect(fakePathFinder.ArgsForCall(0)).To(Equal("etcd"))
Eventually(etcd).Should(gbytes.Say("Everything is dandy"))
Expect(fakeSession.ExitCodeCallCount()).To(Equal(0))
Expect(etcd).NotTo(gexec.Exit())

View file

@ -1,10 +1,5 @@
package test
import (
"fmt"
"net"
)
// Fixtures is a struct that knows how to start all your test fixtures.
//
// Right now, that means Etcd and your APIServer. This is likely to increase in future.
@ -17,23 +12,16 @@ type Fixtures struct {
// and other internals.
type FixtureProcess interface {
Start() error
//TODO Stop should return an error
Stop()
URL() string
URL() (string, error)
}
//go:generate counterfeiter . FixtureProcess
// NewFixtures will give you a Fixtures struct that's properly wired together.
func NewFixtures() (*Fixtures, error) {
apiServerConfig := &APIServerConfig{}
if url, urlErr := getHTTPListenURL(); urlErr == nil {
apiServerConfig.APIServerURL = url
} else {
return nil, urlErr
}
apiServer, err := NewAPIServer(apiServerConfig)
apiServer, err := NewAPIServer()
if err != nil {
return nil, err
}
@ -75,33 +63,6 @@ func (f *Fixtures) Stop() error {
}
// APIServerURL returns the URL to the APIServer. Clients can use this URL to connect to the APIServer.
func (f *Fixtures) APIServerURL() string {
func (f *Fixtures) APIServerURL() (string, error) {
return f.APIServer.URL()
}
func getHTTPListenURL() (url string, err error) {
host := "127.0.0.1"
port, err := getFreePort(host)
if err != nil {
return "", err
}
return fmt.Sprintf("http://%s:%d", host, port), nil
}
func getFreePort(host string) (port int, err error) {
var addr *net.TCPAddr
addr, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", host))
if err != nil {
return
}
var l *net.TCPListener
l, err = net.ListenTCP("tcp", addr)
if err != nil {
return
}
defer func() {
err = l.Close()
}()
return l.Addr().(*net.TCPAddr).Port, nil
}

View file

@ -51,9 +51,10 @@ var _ = Describe("Fixtures", func() {
})
It("can be queried for the APIServer URL", func() {
fakeAPIServerProcess.URLReturns("some url to the apiserver")
fakeAPIServerProcess.URLReturns("some url to the apiserver", nil)
url := fixtures.APIServerURL()
url, err := fixtures.APIServerURL()
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("some url to the apiserver"))
})

View file

@ -25,9 +25,13 @@ var _ = Describe("The Testing Framework", func() {
Expect(err).NotTo(HaveOccurred(), "Expected fixtures to start successfully")
var apiServerURL, etcdClientURL *url.URL
etcdClientURL, err = url.Parse(fixtures.APIServer.(*test.APIServer).Etcd.URL())
etcdUrlString, err := fixtures.APIServer.(*test.APIServer).Etcd.URL()
Expect(err).NotTo(HaveOccurred())
apiServerURL, err = url.Parse(fixtures.APIServerURL())
etcdClientURL, err = url.Parse(etcdUrlString)
Expect(err).NotTo(HaveOccurred())
urlString, err := fixtures.APIServerURL()
Expect(err).NotTo(HaveOccurred())
apiServerURL, err = url.Parse(urlString)
Expect(err).NotTo(HaveOccurred())
isEtcdListeningForClients := isSomethingListeningOnPort(etcdClientURL.Host)

View file

@ -5,11 +5,70 @@ import (
"net"
)
// AddressManager knows how to generate and remember a single address on some
// local interface for a service to listen on.
type AddressManager interface {
Initialize(host string) (port int, resolvedAddress string, err error)
Host() (string, error)
Port() (int, error)
}
//go:generate counterfeiter . AddressManager
// DefaultAddressManager implements an AddressManager. It allocates a new address
// (interface & port) a process can bind and keeps track of that.
type DefaultAddressManager struct {
port int
host string
}
// Initialize returns a address a process can listen on. Given a hostname it returns an address,
// a tuple consisting of a free port and the hostname resolved to its IP.
func (d *DefaultAddressManager) Initialize(host string) (port int, resolvedHost string, err error) {
if d.port != 0 {
return 0, "", fmt.Errorf("this DefaultAddressManager is already initialized")
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", host))
if err != nil {
return
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return
}
d.port = l.Addr().(*net.TCPAddr).Port
defer func() {
err = l.Close()
}()
d.host = addr.IP.String()
return d.port, d.host, nil
}
// Port returns the port that this DefaultAddressManager is managing. Port returns an
// error if this DefaultAddressManager has not yet been initialized.
func (d *DefaultAddressManager) Port() (int, error) {
if d.port == 0 {
return 0, fmt.Errorf("this DefaultAdressManager has is not initialized yet")
}
return d.port, nil
}
// Host returns the host that this DefaultAddressManager is managing. Host returns an
// error if this DefaultAddressManager has not yet been initialized.
func (d *DefaultAddressManager) Host() (string, error) {
if d.host == "" {
return "", fmt.Errorf("this DefaultAdressManager has is not initialized yet")
}
return d.host, nil
}
// PortFinder is the signature of a function returning a free port and a resolved
// address to listen/bind on, and an erro in case there were some problems finding
// a free pair of address & port
type PortFinder func(host string) (port int, resolvedAddress string, err error)
//go:generate counterfeiter . PortFinder
// DefaultPortFinder is the default implementation of PortFinder: It resolves that
// hostname or IP handed in and asks the kernel for a random port. To make that a bit
// safer, it also tries to bind to that port to make sure it is not in use. If all goes

View file

@ -3,10 +3,90 @@ package test_test
import (
. "k8s.io/kubectl/pkg/framework/test"
"fmt"
"net"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("DefaultAddressManager", func() {
var defaultAddressManager *DefaultAddressManager
BeforeEach(func() {
defaultAddressManager = &DefaultAddressManager{}
})
Describe("Initialize", func() {
It("returns a free port and an address to bind to", func() {
port, host, err := defaultAddressManager.Initialize("localhost")
Expect(err).NotTo(HaveOccurred())
Expect(host).To(Equal("127.0.0.1"))
Expect(port).NotTo(Equal(0))
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port))
Expect(err).NotTo(HaveOccurred())
l, err := net.ListenTCP("tcp", addr)
defer func() {
Expect(l.Close()).To(Succeed())
}()
Expect(err).NotTo(HaveOccurred())
})
Context("when given an invalid hostname", func() {
It("propagates the error", func() {
_, _, err := defaultAddressManager.Initialize("this is not a hostname")
Expect(err).To(MatchError(ContainSubstring("no such host")))
})
})
Context("when given a hostname that we don't have permission to listen on", func() {
It("propagates the error", func() {
_, _, err := defaultAddressManager.Initialize("example.com")
Expect(err).To(MatchError(ContainSubstring("bind: can't assign requested address")))
})
})
Context("initialized multiple times", func() {
It("fails", func() {
_, _, err := defaultAddressManager.Initialize("localhost")
Expect(err).NotTo(HaveOccurred())
_, _, err = defaultAddressManager.Initialize("localhost")
Expect(err).To(MatchError(ContainSubstring("already initialized")))
})
})
})
Describe("Port", func() {
It("returns an error if Initialize has not been called yet", func() {
_, err := defaultAddressManager.Port()
Expect(err).To(MatchError(ContainSubstring("not initialized yet")))
})
It("returns the same port as previously allocated by Initialize", func() {
expectedPort, _, err := defaultAddressManager.Initialize("localhost")
Expect(err).NotTo(HaveOccurred())
actualPort, err := defaultAddressManager.Port()
Expect(err).NotTo(HaveOccurred())
Expect(actualPort).To(Equal(expectedPort))
})
})
Describe("Host", func() {
It("returns an error if Initialize has not been called yet", func() {
_, err := defaultAddressManager.Host()
Expect(err).To(MatchError(ContainSubstring("not initialized yet")))
})
It("returns the same port as previously allocated by Initialize", func() {
_, expectedHost, err := defaultAddressManager.Initialize("localhost")
Expect(err).NotTo(HaveOccurred())
actualHost, err := defaultAddressManager.Host()
Expect(err).NotTo(HaveOccurred())
Expect(actualHost).To(Equal(expectedHost))
})
})
})
var _ = Describe("DefaultPortFinder", func() {
It("returns a free port and an address to bind to", func() {
port, addr, err := DefaultPortFinder("127.0.0.1")

View file

@ -0,0 +1,220 @@
// Code generated by counterfeiter. DO NOT EDIT.
package testfakes
import (
"sync"
"k8s.io/kubectl/pkg/framework/test"
)
type FakeAddressManager struct {
InitializeStub func(host string) (port int, resolvedAddress string, err error)
initializeMutex sync.RWMutex
initializeArgsForCall []struct {
host string
}
initializeReturns struct {
result1 int
result2 string
result3 error
}
initializeReturnsOnCall map[int]struct {
result1 int
result2 string
result3 error
}
HostStub func() (string, error)
hostMutex sync.RWMutex
hostArgsForCall []struct{}
hostReturns struct {
result1 string
result2 error
}
hostReturnsOnCall map[int]struct {
result1 string
result2 error
}
PortStub func() (int, error)
portMutex sync.RWMutex
portArgsForCall []struct{}
portReturns struct {
result1 int
result2 error
}
portReturnsOnCall map[int]struct {
result1 int
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeAddressManager) Initialize(host string) (port int, resolvedAddress string, err error) {
fake.initializeMutex.Lock()
ret, specificReturn := fake.initializeReturnsOnCall[len(fake.initializeArgsForCall)]
fake.initializeArgsForCall = append(fake.initializeArgsForCall, struct {
host string
}{host})
fake.recordInvocation("Initialize", []interface{}{host})
fake.initializeMutex.Unlock()
if fake.InitializeStub != nil {
return fake.InitializeStub(host)
}
if specificReturn {
return ret.result1, ret.result2, ret.result3
}
return fake.initializeReturns.result1, fake.initializeReturns.result2, fake.initializeReturns.result3
}
func (fake *FakeAddressManager) InitializeCallCount() int {
fake.initializeMutex.RLock()
defer fake.initializeMutex.RUnlock()
return len(fake.initializeArgsForCall)
}
func (fake *FakeAddressManager) InitializeArgsForCall(i int) string {
fake.initializeMutex.RLock()
defer fake.initializeMutex.RUnlock()
return fake.initializeArgsForCall[i].host
}
func (fake *FakeAddressManager) InitializeReturns(result1 int, result2 string, result3 error) {
fake.InitializeStub = nil
fake.initializeReturns = struct {
result1 int
result2 string
result3 error
}{result1, result2, result3}
}
func (fake *FakeAddressManager) InitializeReturnsOnCall(i int, result1 int, result2 string, result3 error) {
fake.InitializeStub = nil
if fake.initializeReturnsOnCall == nil {
fake.initializeReturnsOnCall = make(map[int]struct {
result1 int
result2 string
result3 error
})
}
fake.initializeReturnsOnCall[i] = struct {
result1 int
result2 string
result3 error
}{result1, result2, result3}
}
func (fake *FakeAddressManager) Host() (string, error) {
fake.hostMutex.Lock()
ret, specificReturn := fake.hostReturnsOnCall[len(fake.hostArgsForCall)]
fake.hostArgsForCall = append(fake.hostArgsForCall, struct{}{})
fake.recordInvocation("Host", []interface{}{})
fake.hostMutex.Unlock()
if fake.HostStub != nil {
return fake.HostStub()
}
if specificReturn {
return ret.result1, ret.result2
}
return fake.hostReturns.result1, fake.hostReturns.result2
}
func (fake *FakeAddressManager) HostCallCount() int {
fake.hostMutex.RLock()
defer fake.hostMutex.RUnlock()
return len(fake.hostArgsForCall)
}
func (fake *FakeAddressManager) HostReturns(result1 string, result2 error) {
fake.HostStub = nil
fake.hostReturns = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeAddressManager) HostReturnsOnCall(i int, result1 string, result2 error) {
fake.HostStub = nil
if fake.hostReturnsOnCall == nil {
fake.hostReturnsOnCall = make(map[int]struct {
result1 string
result2 error
})
}
fake.hostReturnsOnCall[i] = struct {
result1 string
result2 error
}{result1, result2}
}
func (fake *FakeAddressManager) Port() (int, error) {
fake.portMutex.Lock()
ret, specificReturn := fake.portReturnsOnCall[len(fake.portArgsForCall)]
fake.portArgsForCall = append(fake.portArgsForCall, struct{}{})
fake.recordInvocation("Port", []interface{}{})
fake.portMutex.Unlock()
if fake.PortStub != nil {
return fake.PortStub()
}
if specificReturn {
return ret.result1, ret.result2
}
return fake.portReturns.result1, fake.portReturns.result2
}
func (fake *FakeAddressManager) PortCallCount() int {
fake.portMutex.RLock()
defer fake.portMutex.RUnlock()
return len(fake.portArgsForCall)
}
func (fake *FakeAddressManager) PortReturns(result1 int, result2 error) {
fake.PortStub = nil
fake.portReturns = struct {
result1 int
result2 error
}{result1, result2}
}
func (fake *FakeAddressManager) PortReturnsOnCall(i int, result1 int, result2 error) {
fake.PortStub = nil
if fake.portReturnsOnCall == nil {
fake.portReturnsOnCall = make(map[int]struct {
result1 int
result2 error
})
}
fake.portReturnsOnCall[i] = struct {
result1 int
result2 error
}{result1, result2}
}
func (fake *FakeAddressManager) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.initializeMutex.RLock()
defer fake.initializeMutex.RUnlock()
fake.hostMutex.RLock()
defer fake.hostMutex.RUnlock()
fake.portMutex.RLock()
defer fake.portMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeAddressManager) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ test.AddressManager = new(FakeAddressManager)

View file

@ -0,0 +1,98 @@
// Code generated by counterfeiter. DO NOT EDIT.
package testfakes
import (
"sync"
"k8s.io/kubectl/pkg/framework/test"
)
type FakeBinPathFinder struct {
Stub func(symbolicName string) (binPath string)
mutex sync.RWMutex
argsForCall []struct {
symbolicName string
}
returns struct {
result1 string
}
returnsOnCall map[int]struct {
result1 string
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeBinPathFinder) Spy(symbolicName string) (binPath string) {
fake.mutex.Lock()
ret, specificReturn := fake.returnsOnCall[len(fake.argsForCall)]
fake.argsForCall = append(fake.argsForCall, struct {
symbolicName string
}{symbolicName})
fake.recordInvocation("BinPathFinder", []interface{}{symbolicName})
fake.mutex.Unlock()
if fake.Stub != nil {
return fake.Stub(symbolicName)
}
if specificReturn {
return ret.result1
}
return fake.returns.result1
}
func (fake *FakeBinPathFinder) CallCount() int {
fake.mutex.RLock()
defer fake.mutex.RUnlock()
return len(fake.argsForCall)
}
func (fake *FakeBinPathFinder) ArgsForCall(i int) string {
fake.mutex.RLock()
defer fake.mutex.RUnlock()
return fake.argsForCall[i].symbolicName
}
func (fake *FakeBinPathFinder) Returns(result1 string) {
fake.Stub = nil
fake.returns = struct {
result1 string
}{result1}
}
func (fake *FakeBinPathFinder) ReturnsOnCall(i int, result1 string) {
fake.Stub = nil
if fake.returnsOnCall == nil {
fake.returnsOnCall = make(map[int]struct {
result1 string
})
}
fake.returnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *FakeBinPathFinder) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.mutex.RLock()
defer fake.mutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeBinPathFinder) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ test.BinPathFinder = new(FakeBinPathFinder).Spy

View file

@ -20,14 +20,16 @@ type FakeFixtureProcess struct {
StopStub func()
stopMutex sync.RWMutex
stopArgsForCall []struct{}
URLStub func() string
URLStub func() (string, error)
uRLMutex sync.RWMutex
uRLArgsForCall []struct{}
uRLReturns struct {
result1 string
result2 error
}
uRLReturnsOnCall map[int]struct {
result1 string
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
@ -89,7 +91,7 @@ func (fake *FakeFixtureProcess) StopCallCount() int {
return len(fake.stopArgsForCall)
}
func (fake *FakeFixtureProcess) URL() string {
func (fake *FakeFixtureProcess) URL() (string, error) {
fake.uRLMutex.Lock()
ret, specificReturn := fake.uRLReturnsOnCall[len(fake.uRLArgsForCall)]
fake.uRLArgsForCall = append(fake.uRLArgsForCall, struct{}{})
@ -99,9 +101,9 @@ func (fake *FakeFixtureProcess) URL() string {
return fake.URLStub()
}
if specificReturn {
return ret.result1
return ret.result1, ret.result2
}
return fake.uRLReturns.result1
return fake.uRLReturns.result1, fake.uRLReturns.result2
}
func (fake *FakeFixtureProcess) URLCallCount() int {
@ -110,23 +112,26 @@ func (fake *FakeFixtureProcess) URLCallCount() int {
return len(fake.uRLArgsForCall)
}
func (fake *FakeFixtureProcess) URLReturns(result1 string) {
func (fake *FakeFixtureProcess) URLReturns(result1 string, result2 error) {
fake.URLStub = nil
fake.uRLReturns = struct {
result1 string
}{result1}
result2 error
}{result1, result2}
}
func (fake *FakeFixtureProcess) URLReturnsOnCall(i int, result1 string) {
func (fake *FakeFixtureProcess) URLReturnsOnCall(i int, result1 string, result2 error) {
fake.URLStub = nil
if fake.uRLReturnsOnCall == nil {
fake.uRLReturnsOnCall = make(map[int]struct {
result1 string
result2 error
})
}
fake.uRLReturnsOnCall[i] = struct {
result1 string
}{result1}
result2 error
}{result1, result2}
}
func (fake *FakeFixtureProcess) Invocations() map[string][][]interface{} {