mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
mesos cloud provider implementation
This commit is contained in:
parent
db9c6373df
commit
eca9fd58c7
293
pkg/cloudprovider/mesos/client.go
Normal file
293
pkg/cloudprovider/mesos/client.go
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/mesos/mesos-go/detector"
|
||||||
|
mesos "github.com/mesos/mesos-go/mesosproto"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultClusterName = "mesos"
|
||||||
|
|
||||||
|
var noLeadingMasterError = fmt.Errorf("there is no current leading master available to query")
|
||||||
|
|
||||||
|
type mesosClient struct {
|
||||||
|
masterLock sync.RWMutex
|
||||||
|
master string // host:port formatted address
|
||||||
|
httpClient *http.Client
|
||||||
|
tr *http.Transport
|
||||||
|
initialMaster <-chan struct{} // signal chan, closes once an initial, non-nil master is found
|
||||||
|
state *stateCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type slaveNode struct {
|
||||||
|
hostname string
|
||||||
|
resources *api.NodeResources
|
||||||
|
}
|
||||||
|
|
||||||
|
type mesosState struct {
|
||||||
|
clusterName string
|
||||||
|
nodes []*slaveNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type stateCache struct {
|
||||||
|
sync.Mutex
|
||||||
|
expiresAt time.Time
|
||||||
|
cached *mesosState
|
||||||
|
err error
|
||||||
|
ttl time.Duration
|
||||||
|
refill func(context.Context) (*mesosState, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadCache reloads the state cache if it has expired.
|
||||||
|
func (c *stateCache) reloadCache(ctx context.Context) {
|
||||||
|
now := time.Now()
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
if c.expiresAt.Before(now) {
|
||||||
|
log.V(4).Infof("Reloading cached Mesos state")
|
||||||
|
c.cached, c.err = c.refill(ctx)
|
||||||
|
c.expiresAt = now.Add(c.ttl)
|
||||||
|
} else {
|
||||||
|
log.V(4).Infof("Using cached Mesos state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cachedState returns the cached Mesos state.
|
||||||
|
func (c *stateCache) cachedState(ctx context.Context) (*mesosState, error) {
|
||||||
|
c.reloadCache(ctx)
|
||||||
|
return c.cached, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// clusterName returns the cached Mesos cluster name.
|
||||||
|
func (c *stateCache) clusterName(ctx context.Context) (string, error) {
|
||||||
|
cached, err := c.cachedState(ctx)
|
||||||
|
return cached.clusterName, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodes returns the cached list of slave nodes.
|
||||||
|
func (c *stateCache) nodes(ctx context.Context) ([]*slaveNode, error) {
|
||||||
|
cached, err := c.cachedState(ctx)
|
||||||
|
return cached.nodes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMesosClient(
|
||||||
|
md detector.Master,
|
||||||
|
mesosHttpClientTimeout, stateCacheTTL time.Duration) (*mesosClient, error) {
|
||||||
|
|
||||||
|
tr := &http.Transport{}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: mesosHttpClientTimeout,
|
||||||
|
}
|
||||||
|
return createMesosClient(md, httpClient, tr, stateCacheTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMesosClient(
|
||||||
|
md detector.Master,
|
||||||
|
httpClient *http.Client,
|
||||||
|
tr *http.Transport,
|
||||||
|
stateCacheTTL time.Duration) (*mesosClient, error) {
|
||||||
|
|
||||||
|
initialMaster := make(chan struct{})
|
||||||
|
client := &mesosClient{
|
||||||
|
httpClient: httpClient,
|
||||||
|
tr: tr,
|
||||||
|
initialMaster: initialMaster,
|
||||||
|
state: &stateCache{
|
||||||
|
ttl: stateCacheTTL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client.state.refill = client.pollMasterForState
|
||||||
|
first := true
|
||||||
|
if err := md.Detect(detector.OnMasterChanged(func(info *mesos.MasterInfo) {
|
||||||
|
client.masterLock.Lock()
|
||||||
|
defer client.masterLock.Unlock()
|
||||||
|
if info == nil {
|
||||||
|
client.master = ""
|
||||||
|
} else if host := info.GetHostname(); host != "" {
|
||||||
|
client.master = host
|
||||||
|
} else {
|
||||||
|
client.master = unpackIPv4(info.GetIp())
|
||||||
|
}
|
||||||
|
if len(client.master) > 0 {
|
||||||
|
client.master = fmt.Sprintf("%s:%d", client.master, info.GetPort())
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
close(initialMaster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("cloud master changed to '%v'", client.master)
|
||||||
|
})); err != nil {
|
||||||
|
log.V(1).Infof("detector initialization failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackIPv4(ip uint32) string {
|
||||||
|
octets := make([]byte, 4, 4)
|
||||||
|
binary.BigEndian.PutUint32(octets, ip)
|
||||||
|
ipv4 := net.IP(octets)
|
||||||
|
return ipv4.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// listSlaves returns a (possibly cached) list of slave nodes.
|
||||||
|
// Callers must not mutate the contents of the returned slice.
|
||||||
|
func (c *mesosClient) listSlaves(ctx context.Context) ([]*slaveNode, error) {
|
||||||
|
return c.state.nodes(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clusterName returns a (possibly cached) cluster name.
|
||||||
|
func (c *mesosClient) clusterName(ctx context.Context) (string, error) {
|
||||||
|
return c.state.clusterName(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollMasterForState returns an array of slave nodes
|
||||||
|
func (c *mesosClient) pollMasterForState(ctx context.Context) (*mesosState, error) {
|
||||||
|
// wait for initial master detection
|
||||||
|
select {
|
||||||
|
case <-c.initialMaster: // noop
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
master := func() string {
|
||||||
|
c.masterLock.RLock()
|
||||||
|
defer c.masterLock.RUnlock()
|
||||||
|
return c.master
|
||||||
|
}()
|
||||||
|
if master == "" {
|
||||||
|
return nil, noLeadingMasterError
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO(jdef) should not assume master uses http (what about https?)
|
||||||
|
|
||||||
|
uri := fmt.Sprintf("http://%s/state.json", master)
|
||||||
|
req, err := http.NewRequest("GET", uri, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var state *mesosState
|
||||||
|
err = c.httpDo(ctx, req, func(res *http.Response, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("HTTP request failed with code %d: %v", res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
blob, err1 := ioutil.ReadAll(res.Body)
|
||||||
|
if err1 != nil {
|
||||||
|
return err1
|
||||||
|
}
|
||||||
|
log.V(3).Infof("Got mesos state, content length %v", len(blob))
|
||||||
|
state, err1 = parseMesosState(blob)
|
||||||
|
return err1
|
||||||
|
})
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMesosState(blob []byte) (*mesosState, error) {
|
||||||
|
type State struct {
|
||||||
|
ClusterName string `json:"cluster"`
|
||||||
|
Slaves []*struct {
|
||||||
|
Id string `json:"id"` // ex: 20150106-162714-3815890698-5050-2453-S2
|
||||||
|
Pid string `json:"pid"` // ex: slave(1)@10.22.211.18:5051
|
||||||
|
Hostname string `json:"hostname"` // ex: 10.22.211.18, or slave-123.nowhere.com
|
||||||
|
Resources map[string]interface{} `json:"resources"` // ex: {"mem": 123, "ports": "[31000-3200]"}
|
||||||
|
} `json:"slaves"`
|
||||||
|
}
|
||||||
|
state := &State{ClusterName: defaultClusterName}
|
||||||
|
if err := json.Unmarshal(blob, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nodes := []*slaveNode{}
|
||||||
|
for _, slave := range state.Slaves {
|
||||||
|
if slave.Hostname == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node := &slaveNode{hostname: slave.Hostname}
|
||||||
|
cap := api.ResourceList{}
|
||||||
|
if slave.Resources != nil && len(slave.Resources) > 0 {
|
||||||
|
// attempt to translate CPU (cores) and memory (MB) resources
|
||||||
|
if cpu, found := slave.Resources["cpus"]; found {
|
||||||
|
if cpuNum, ok := cpu.(float64); ok {
|
||||||
|
cap[api.ResourceCPU] = *resource.NewQuantity(int64(cpuNum), resource.DecimalSI)
|
||||||
|
} else {
|
||||||
|
log.Warningf("unexpected slave cpu resource type %T: %v", cpu, cpu)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warningf("slave failed to report cpu resource")
|
||||||
|
}
|
||||||
|
if mem, found := slave.Resources["mem"]; found {
|
||||||
|
if memNum, ok := mem.(float64); ok {
|
||||||
|
cap[api.ResourceMemory] = *resource.NewQuantity(int64(memNum), resource.BinarySI)
|
||||||
|
} else {
|
||||||
|
log.Warningf("unexpected slave mem resource type %T: %v", mem, mem)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warningf("slave failed to report mem resource")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cap) > 0 {
|
||||||
|
node.resources = &api.NodeResources{
|
||||||
|
Capacity: cap,
|
||||||
|
}
|
||||||
|
log.V(4).Infof("node %q reporting capacity %v", node.hostname, cap)
|
||||||
|
}
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &mesosState{
|
||||||
|
clusterName: state.ClusterName,
|
||||||
|
nodes: nodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseHandler func(*http.Response, error) error
|
||||||
|
|
||||||
|
// httpDo executes an HTTP request in the given context, canceling an ongoing request if the context
|
||||||
|
// is canceled prior to completion of the request. hacked from https://blog.golang.org/context
|
||||||
|
func (c *mesosClient) httpDo(ctx context.Context, req *http.Request, f responseHandler) error {
|
||||||
|
// Run the HTTP request in a goroutine and pass the response to f.
|
||||||
|
ch := make(chan error, 1)
|
||||||
|
go func() { ch <- f(c.httpClient.Do(req)) }()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
c.tr.CancelRequest(req)
|
||||||
|
<-ch // Wait for f to return.
|
||||||
|
return ctx.Err()
|
||||||
|
case err := <-ch:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
266
pkg/cloudprovider/mesos/client_test.go
Normal file
266
pkg/cloudprovider/mesos/client_test.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/mesos/mesos-go/detector"
|
||||||
|
"github.com/mesos/mesos-go/mesosutil"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
|
||||||
|
const (
|
||||||
|
TEST_MASTER_ID = "master-12345"
|
||||||
|
TEST_MASTER_IP = 177048842 // 10.141.141.10
|
||||||
|
TEST_MASTER_PORT = 5050
|
||||||
|
|
||||||
|
TEST_STATE_JSON = `
|
||||||
|
{
|
||||||
|
"version": "0.22.0",
|
||||||
|
"unregistered_frameworks": [],
|
||||||
|
"started_tasks": 0,
|
||||||
|
"start_time": 1429456501.61141,
|
||||||
|
"staged_tasks": 0,
|
||||||
|
"slaves": [
|
||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"ports": "[31000-32000]",
|
||||||
|
"mem": 15360,
|
||||||
|
"disk": 470842,
|
||||||
|
"cpus": 8
|
||||||
|
},
|
||||||
|
"registered_time": 1429456502.46999,
|
||||||
|
"pid": "slave(1)@mesos1.internal.company.com:5050",
|
||||||
|
"id": "20150419-081501-16777343-5050-16383-S2",
|
||||||
|
"hostname": "mesos1.internal.company.com",
|
||||||
|
"attributes": {},
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"ports": "[31000-32000]",
|
||||||
|
"mem": 15360,
|
||||||
|
"disk": 470842,
|
||||||
|
"cpus": 8
|
||||||
|
},
|
||||||
|
"registered_time": 1429456502.4144,
|
||||||
|
"pid": "slave(1)@mesos2.internal.company.com:5050",
|
||||||
|
"id": "20150419-081501-16777343-5050-16383-S1",
|
||||||
|
"hostname": "mesos2.internal.company.com",
|
||||||
|
"attributes": {},
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"ports": "[31000-32000]",
|
||||||
|
"mem": 15360,
|
||||||
|
"disk": 470842,
|
||||||
|
"cpus": 8
|
||||||
|
},
|
||||||
|
"registered_time": 1429456502.02879,
|
||||||
|
"pid": "slave(1)@mesos3.internal.company.com:5050",
|
||||||
|
"id": "20150419-081501-16777343-5050-16383-S0",
|
||||||
|
"hostname": "mesos3.internal.company.com",
|
||||||
|
"attributes": {},
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pid": "master@mesos-master0.internal.company.com:5050",
|
||||||
|
"orphan_tasks": [],
|
||||||
|
"lost_tasks": 0,
|
||||||
|
"leader": "master@mesos-master0.internal.company.com:5050",
|
||||||
|
"killed_tasks": 0,
|
||||||
|
"failed_tasks": 0,
|
||||||
|
"elected_time": 1429456501.61638,
|
||||||
|
"deactivated_slaves": 0,
|
||||||
|
"completed_frameworks": [],
|
||||||
|
"build_user": "buildbot",
|
||||||
|
"build_time": 1425085311,
|
||||||
|
"build_date": "2015-02-27 17:01:51",
|
||||||
|
"activated_slaves": 3,
|
||||||
|
"finished_tasks": 0,
|
||||||
|
"flags": {
|
||||||
|
"zk_session_timeout": "10secs",
|
||||||
|
"work_dir": "/tmp/mesos/local/Lc9arz",
|
||||||
|
"webui_dir": "/usr/local/share/mesos/webui",
|
||||||
|
"version": "false",
|
||||||
|
"user_sorter": "drf",
|
||||||
|
"slave_reregister_timeout": "10mins",
|
||||||
|
"logbufsecs": "0",
|
||||||
|
"log_auto_initialize": "true",
|
||||||
|
"initialize_driver_logging": "true",
|
||||||
|
"framework_sorter": "drf",
|
||||||
|
"authenticators": "crammd5",
|
||||||
|
"authenticate_slaves": "false",
|
||||||
|
"authenticate": "false",
|
||||||
|
"allocation_interval": "1secs",
|
||||||
|
"logging_level": "INFO",
|
||||||
|
"quiet": "false",
|
||||||
|
"recovery_slave_removal_limit": "100%",
|
||||||
|
"registry": "replicated_log",
|
||||||
|
"registry_fetch_timeout": "1mins",
|
||||||
|
"registry_store_timeout": "5secs",
|
||||||
|
"registry_strict": "false",
|
||||||
|
"root_submissions": "true"
|
||||||
|
},
|
||||||
|
"frameworks": [],
|
||||||
|
"git_branch": "refs/heads/0.22.0-rc1",
|
||||||
|
"git_sha": "46834faca67f877631e1beb7d61be5c080ec3dc2",
|
||||||
|
"git_tag": "0.22.0-rc1",
|
||||||
|
"hostname": "localhost",
|
||||||
|
"id": "20150419-081501-16777343-5050-16383"
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
|
||||||
|
type FakeMasterDetector struct {
|
||||||
|
callback detector.MasterChanged
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakeMasterDetector() *FakeMasterDetector {
|
||||||
|
return &FakeMasterDetector{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md FakeMasterDetector) Cancel() {
|
||||||
|
close(md.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md FakeMasterDetector) Detect(cb detector.MasterChanged) error {
|
||||||
|
md.callback = cb
|
||||||
|
leadingMaster := mesosutil.NewMasterInfo(TEST_MASTER_ID, TEST_MASTER_IP, TEST_MASTER_PORT)
|
||||||
|
cb.OnMasterChanged(leadingMaster)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md FakeMasterDetector) Done() <-chan struct{} {
|
||||||
|
return md.done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auxilliary functions
|
||||||
|
|
||||||
|
func makeHttpMocks() (*httptest.Server, *http.Client, *http.Transport) {
|
||||||
|
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.V(4).Infof("Mocking response for HTTP request: %#v", r)
|
||||||
|
if r.URL.Path == "/state.json" {
|
||||||
|
w.WriteHeader(200) // OK
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintln(w, TEST_STATE_JSON)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
fmt.Fprintln(w, "Bad Request")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Intercept all client requests and feed them to the test server
|
||||||
|
transport := &http.Transport{
|
||||||
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||||
|
return url.Parse(httpServer.URL)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := &http.Client{Transport: transport}
|
||||||
|
|
||||||
|
return httpServer, httpClient, transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
|
||||||
|
// test mesos.parseMesosState
|
||||||
|
func Test_parseMesosState(t *testing.T) {
|
||||||
|
state, err := parseMesosState([]byte(TEST_STATE_JSON))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseMesosState does not yield an error")
|
||||||
|
}
|
||||||
|
if state == nil {
|
||||||
|
t.Fatalf("parseMesosState yields a non-nil state")
|
||||||
|
}
|
||||||
|
if len(state.nodes) != 3 {
|
||||||
|
t.Fatalf("parseMesosState yields a state with 3 nodes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.listSlaves
|
||||||
|
func Test_listSlaves(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createMesosClient does not yield an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
slaveNodes, err := mesosClient.listSlaves(context.TODO())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listSlaves does not yield an error")
|
||||||
|
}
|
||||||
|
if len(slaveNodes) != 3 {
|
||||||
|
t.Fatalf("listSlaves yields a collection of size 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedHostnames := map[string]struct{}{
|
||||||
|
"mesos1.internal.company.com": {},
|
||||||
|
"mesos2.internal.company.com": {},
|
||||||
|
"mesos3.internal.company.com": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
actualHostnames := make(map[string]struct{})
|
||||||
|
for _, node := range slaveNodes {
|
||||||
|
actualHostnames[node.hostname] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expectedHostnames, actualHostnames) {
|
||||||
|
t.Fatalf("listSlaves yields a collection with the expected hostnames")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.clusterName
|
||||||
|
func Test_clusterName(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
|
||||||
|
name, err := mesosClient.clusterName(context.TODO())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("clusterName does not yield an error")
|
||||||
|
}
|
||||||
|
if name != defaultClusterName {
|
||||||
|
t.Fatalf("clusterName yields the expected (default) value")
|
||||||
|
}
|
||||||
|
}
|
79
pkg/cloudprovider/mesos/config.go
Normal file
79
pkg/cloudprovider/mesos/config.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.google.com/p/gcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMesosMaster = "localhost:5050"
|
||||||
|
DefaultHttpClientTimeout = time.Duration(10) * time.Second
|
||||||
|
DefaultStateCacheTTL = time.Duration(5) * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example Mesos cloud provider configuration file:
|
||||||
|
//
|
||||||
|
// [mesos-cloud]
|
||||||
|
// mesos-master = leader.mesos:5050
|
||||||
|
// http-client-timeout = 500ms
|
||||||
|
// state-cache-ttl = 1h
|
||||||
|
|
||||||
|
type ConfigWrapper struct {
|
||||||
|
Mesos_Cloud Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MesosMaster string `gcfg:"mesos-master"`
|
||||||
|
MesosHttpClientTimeout Duration `gcfg:"http-client-timeout"`
|
||||||
|
StateCacheTTL Duration `gcfg:"state-cache-ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Duration struct {
|
||||||
|
Duration time.Duration `gcfg:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalText(data []byte) error {
|
||||||
|
underlying, err := time.ParseDuration(string(data))
|
||||||
|
if err == nil {
|
||||||
|
d.Duration = underlying
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
MesosMaster: DefaultMesosMaster,
|
||||||
|
MesosHttpClientTimeout: Duration{Duration: DefaultHttpClientTimeout},
|
||||||
|
StateCacheTTL: Duration{Duration: DefaultStateCacheTTL},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig(configReader io.Reader) (*Config, error) {
|
||||||
|
config := createDefaultConfig()
|
||||||
|
wrapper := &ConfigWrapper{Mesos_Cloud: *config}
|
||||||
|
if configReader != nil {
|
||||||
|
if err := gcfg.ReadInto(wrapper, configReader); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config = &(wrapper.Mesos_Cloud)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
75
pkg/cloudprovider/mesos/config_test.go
Normal file
75
pkg/cloudprovider/mesos/config_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// test mesos.createDefaultConfig
|
||||||
|
func Test_createDefaultConfig(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
|
||||||
|
config := createDefaultConfig()
|
||||||
|
|
||||||
|
if config.MesosMaster != DefaultMesosMaster {
|
||||||
|
t.Fatalf("Default config has the expected MesosMaster value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MesosHttpClientTimeout.Duration != DefaultHttpClientTimeout {
|
||||||
|
t.Fatalf("Default config has the expected MesosHttpClientTimeout value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StateCacheTTL.Duration != DefaultStateCacheTTL {
|
||||||
|
t.Fatalf("Default config has the expected StateCacheTTL value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.readConfig
|
||||||
|
func Test_readConfig(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
|
||||||
|
configString := `
|
||||||
|
[mesos-cloud]
|
||||||
|
mesos-master = leader.mesos:5050
|
||||||
|
http-client-timeout = 500ms
|
||||||
|
state-cache-ttl = 1h`
|
||||||
|
|
||||||
|
reader := bytes.NewBufferString(configString)
|
||||||
|
|
||||||
|
config, err := readConfig(reader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Reading configuration does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MesosMaster != "leader.mesos:5050" {
|
||||||
|
t.Fatalf("Parsed config has the expected MesosMaster value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.MesosHttpClientTimeout.Duration != time.Duration(500)*time.Millisecond {
|
||||||
|
t.Fatalf("Parsed config has the expected MesosHttpClientTimeout value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StateCacheTTL.Duration != time.Duration(1)*time.Hour {
|
||||||
|
t.Fatalf("Parsed config has the expected StateCacheTTL value")
|
||||||
|
}
|
||||||
|
}
|
238
pkg/cloudprovider/mesos/mesos.go
Normal file
238
pkg/cloudprovider/mesos/mesos.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"github.com/mesos/mesos-go/detector"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
PluginName = "mesos"
|
||||||
|
CloudProvider *MesosCloud
|
||||||
|
|
||||||
|
noHostNameSpecified = errors.New("No hostname specified")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cloudprovider.RegisterCloudProvider(
|
||||||
|
PluginName,
|
||||||
|
func(configReader io.Reader) (cloudprovider.Interface, error) {
|
||||||
|
provider, err := newMesosCloud(configReader)
|
||||||
|
if err == nil {
|
||||||
|
CloudProvider = provider
|
||||||
|
}
|
||||||
|
return provider, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type MesosCloud struct {
|
||||||
|
client *mesosClient
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MesosCloud) MasterURI() string {
|
||||||
|
return c.config.MesosMaster
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMesosCloud(configReader io.Reader) (*MesosCloud, error) {
|
||||||
|
config, err := readConfig(configReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.V(1).Infof("new mesos cloud, master='%v'", config.MesosMaster)
|
||||||
|
if d, err := detector.New(config.MesosMaster); err != nil {
|
||||||
|
log.V(1).Infof("failed to create master detector: %v", err)
|
||||||
|
return nil, err
|
||||||
|
} else if cl, err := newMesosClient(d,
|
||||||
|
config.MesosHttpClientTimeout.Duration,
|
||||||
|
config.StateCacheTTL.Duration); err != nil {
|
||||||
|
log.V(1).Infof("failed to create mesos cloud client: %v", err)
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &MesosCloud{client: cl, config: config}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instances returns a copy of the Mesos cloud Instances implementation.
|
||||||
|
// Mesos natively provides minimal cloud-type resources. More robust cloud
|
||||||
|
// support requires a combination of Mesos and cloud-specific knowledge.
|
||||||
|
func (c *MesosCloud) Instances() (cloudprovider.Instances, bool) {
|
||||||
|
return c, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCPLoadBalancer always returns nil, false in this implementation.
|
||||||
|
// Mesos does not provide any type of native load balancing by default,
|
||||||
|
// so this implementation always returns (nil, false).
|
||||||
|
func (c *MesosCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zones always returns nil, false in this implementation.
|
||||||
|
// Mesos does not provide any type of native region or zone awareness,
|
||||||
|
// so this implementation always returns (nil, false).
|
||||||
|
func (c *MesosCloud) Zones() (cloudprovider.Zones, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clusters returns a copy of the Mesos cloud Clusters implementation.
|
||||||
|
// Mesos does not provide support for multiple clusters.
|
||||||
|
func (c *MesosCloud) Clusters() (cloudprovider.Clusters, bool) {
|
||||||
|
return c, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListClusters lists the names of the available Mesos clusters.
|
||||||
|
func (c *MesosCloud) ListClusters() ([]string, error) {
|
||||||
|
// Always returns a single cluster (this one!)
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
defer cancel()
|
||||||
|
name, err := c.client.clusterName(ctx)
|
||||||
|
return []string{name}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master gets back the address (either DNS name or IP address) of the master node for the cluster.
|
||||||
|
func (c *MesosCloud) Master(clusterName string) (string, error) {
|
||||||
|
clusters, err := c.ListClusters()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, name := range clusters {
|
||||||
|
if name == clusterName {
|
||||||
|
if c.client.master == "" {
|
||||||
|
return "", errors.New("The currently leading master is unknown.")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(c.client.master)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return host, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New(fmt.Sprintf("The supplied cluster '%v' does not exist", clusterName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ipAddress returns an IP address of the specified instance.
|
||||||
|
func (c *MesosCloud) ipAddress(name string) (net.IP, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, noHostNameSpecified
|
||||||
|
}
|
||||||
|
ipaddr := net.ParseIP(name)
|
||||||
|
if ipaddr != nil {
|
||||||
|
return ipaddr, nil
|
||||||
|
}
|
||||||
|
iplist, err := net.LookupIP(name)
|
||||||
|
if err != nil {
|
||||||
|
log.V(2).Infof("failed to resolve IP from host name '%v': %v", name, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ipaddr = iplist[0]
|
||||||
|
log.V(2).Infof("resolved host '%v' to '%v'", name, ipaddr)
|
||||||
|
return ipaddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalID returns the cloud provider ID of the specified instance.
|
||||||
|
func (c *MesosCloud) ExternalID(instance string) (string, error) {
|
||||||
|
ip, err := c.ipAddress(instance)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return ip.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists instances that match 'filter' which is a regular expression
|
||||||
|
// which must match the entire instance name (fqdn).
|
||||||
|
func (c *MesosCloud) List(filter string) ([]string, error) {
|
||||||
|
//TODO(jdef) use a timeout here? 15s?
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
nodes, err := c.client.listSlaves(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
log.V(2).Info("no slaves found, are any running?")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
filterRegex, err := regexp.Compile(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addr := []string{}
|
||||||
|
for _, node := range nodes {
|
||||||
|
if filterRegex.MatchString(node.hostname) {
|
||||||
|
addr = append(addr, node.hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeResources gets the resources for a particular node
|
||||||
|
func (c *MesosCloud) GetNodeResources(name string) (*api.NodeResources, error) {
|
||||||
|
//TODO(jdef) use a timeout here? 15s?
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
nodes, err := c.client.listSlaves(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
log.V(2).Info("no slaves found, are any running?")
|
||||||
|
} else {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if name == node.hostname {
|
||||||
|
return node.resources, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Warningf("failed to locate node spec for %q", name)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeAddresses returns the addresses of the specified instance.
|
||||||
|
func (c *MesosCloud) NodeAddresses(name string) ([]api.NodeAddress, error) {
|
||||||
|
ip, err := c.ipAddress(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the specified instance using the spec.
|
||||||
|
// Ths implementation is a noop.
|
||||||
|
func (c *MesosCloud) Configure(name string, spec *api.NodeSpec) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release deletes all the configuration related to the instance, including other cloud resources.
|
||||||
|
// Ths implementation is a noop.
|
||||||
|
func (c *MesosCloud) Release(name string) error {
|
||||||
|
return nil
|
||||||
|
}
|
288
pkg/cloudprovider/mesos/mesos_test.go
Normal file
288
pkg/cloudprovider/mesos/mesos_test.go
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/golang/glog"
|
||||||
|
"speter.net/go/exp/math/dec/inf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPAddress(t *testing.T) {
|
||||||
|
c := &MesosCloud{}
|
||||||
|
expected4 := net.IPv4(127, 0, 0, 1)
|
||||||
|
ip, err := c.ipAddress("127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ip, expected4) {
|
||||||
|
t.Fatalf("expected %#v instead of %#v", expected4, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected6 := net.ParseIP("::1")
|
||||||
|
if expected6 == nil {
|
||||||
|
t.Fatalf("failed to parse ipv6 ::1")
|
||||||
|
}
|
||||||
|
ip, err = c.ipAddress("::1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ip, expected6) {
|
||||||
|
t.Fatalf("expected %#v instead of %#v", expected6, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err = c.ipAddress("localhost")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ip, expected4) && !reflect.DeepEqual(ip, expected6) {
|
||||||
|
t.Fatalf("expected %#v or %#v instead of %#v", expected4, expected6, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.ipAddress("")
|
||||||
|
if err != noHostNameSpecified {
|
||||||
|
t.Fatalf("expected error noHostNameSpecified but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.newMesosCloud with no config
|
||||||
|
func Test_newMesosCloud_NoConfig(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, err := newMesosCloud(nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating a new Mesos cloud provider without config does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesosCloud.client.httpClient.Timeout != DefaultHttpClientTimeout {
|
||||||
|
t.Fatalf("Creating a new Mesos cloud provider without config does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesosCloud.client.state.ttl != DefaultStateCacheTTL {
|
||||||
|
t.Fatalf("Mesos client with default config has the expected state cache TTL value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.newMesosCloud with custom config
|
||||||
|
func Test_newMesosCloud_WithConfig(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
|
||||||
|
configString := `
|
||||||
|
[mesos-cloud]
|
||||||
|
http-client-timeout = 500ms
|
||||||
|
state-cache-ttl = 1h`
|
||||||
|
|
||||||
|
reader := bytes.NewBufferString(configString)
|
||||||
|
|
||||||
|
mesosCloud, err := newMesosCloud(reader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Creating a new Mesos cloud provider with a custom config does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesosCloud.client.httpClient.Timeout != time.Duration(500)*time.Millisecond {
|
||||||
|
t.Fatalf("Mesos client with a custom config has the expected HTTP client timeout value")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesosCloud.client.state.ttl != time.Duration(1)*time.Hour {
|
||||||
|
t.Fatalf("Mesos client with a custom config has the expected state cache TTL value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests for capability reporting functions
|
||||||
|
|
||||||
|
// test mesos.Instances
|
||||||
|
func Test_Instances(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, _ := newMesosCloud(nil)
|
||||||
|
|
||||||
|
instances, supports_instances := mesosCloud.Instances()
|
||||||
|
|
||||||
|
if !supports_instances || instances == nil {
|
||||||
|
t.Fatalf("MesosCloud provides an implementation of Instances")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.TCPLoadBalancer
|
||||||
|
func Test_TcpLoadBalancer(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, _ := newMesosCloud(nil)
|
||||||
|
|
||||||
|
lb, supports_lb := mesosCloud.TCPLoadBalancer()
|
||||||
|
|
||||||
|
if supports_lb || lb != nil {
|
||||||
|
t.Fatalf("MesosCloud does not provide an implementation of TCPLoadBalancer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.Zones
|
||||||
|
func Test_Zones(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, _ := newMesosCloud(nil)
|
||||||
|
|
||||||
|
zones, supports_zones := mesosCloud.Zones()
|
||||||
|
|
||||||
|
if supports_zones || zones != nil {
|
||||||
|
t.Fatalf("MesosCloud does not provide an implementation of Zones")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.Clusters
|
||||||
|
func Test_Clusters(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, _ := newMesosCloud(nil)
|
||||||
|
|
||||||
|
clusters, supports_clusters := mesosCloud.Clusters()
|
||||||
|
|
||||||
|
if !supports_clusters || clusters == nil {
|
||||||
|
t.Fatalf("MesosCloud does not provide an implementation of Clusters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.MasterURI
|
||||||
|
func Test_MasterURI(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
mesosCloud, _ := newMesosCloud(nil)
|
||||||
|
|
||||||
|
uri := mesosCloud.MasterURI()
|
||||||
|
|
||||||
|
if uri != DefaultMesosMaster {
|
||||||
|
t.Fatalf("MasterURI returns the expected master URI (expected \"localhost\", actual \"%s\"", uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.ListClusters
|
||||||
|
func Test_ListClusters(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
mesosCloud := &MesosCloud{client: mesosClient, config: createDefaultConfig()}
|
||||||
|
|
||||||
|
clusters, err := mesosCloud.ListClusters()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListClusters does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clusters) != 1 {
|
||||||
|
t.Fatalf("ListClusters should return a list of size 1: (actual: %#v)", clusters)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedClusterNames := []string{"mesos"}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(clusters, expectedClusterNames) {
|
||||||
|
t.Fatalf("ListClusters should return the expected list of names: (expected: %#v, actual: %#v)",
|
||||||
|
expectedClusterNames,
|
||||||
|
clusters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.Master
|
||||||
|
func Test_Master(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
mesosCloud := &MesosCloud{client: mesosClient, config: createDefaultConfig()}
|
||||||
|
|
||||||
|
clusters, err := mesosCloud.ListClusters()
|
||||||
|
clusterName := clusters[0]
|
||||||
|
master, err := mesosCloud.Master(clusterName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Master does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMaster := unpackIPv4(TEST_MASTER_IP)
|
||||||
|
|
||||||
|
if master != expectedMaster {
|
||||||
|
t.Fatalf("Master returns the expected value: (expected: %#v, actual: %#v", expectedMaster, master)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.List
|
||||||
|
func Test_List(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
mesosCloud := &MesosCloud{client: mesosClient, config: createDefaultConfig()}
|
||||||
|
|
||||||
|
clusters, err := mesosCloud.List(".*") // recognizes the language of all strings
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clusters) != 3 {
|
||||||
|
t.Fatalf("List with a catch-all filter should return a list of size 3: (actual: %#v)", clusters)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters, err = mesosCloud.List("$^") // end-of-string followed by start-of-string: recognizes the empty language
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clusters) != 0 {
|
||||||
|
t.Fatalf("List with a reject-all filter should return a list of size 0: (actual: %#v)", clusters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test mesos.GetNodeResources
|
||||||
|
func Test_GetNodeResources(t *testing.T) {
|
||||||
|
defer log.Flush()
|
||||||
|
md := FakeMasterDetector{}
|
||||||
|
httpServer, httpClient, httpTransport := makeHttpMocks()
|
||||||
|
defer httpServer.Close()
|
||||||
|
cacheTTL := 500 * time.Millisecond
|
||||||
|
mesosClient, err := createMesosClient(md, httpClient, httpTransport, cacheTTL)
|
||||||
|
mesosCloud := &MesosCloud{client: mesosClient, config: createDefaultConfig()}
|
||||||
|
|
||||||
|
resources, err := mesosCloud.GetNodeResources("mesos1.internal.company.com")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetNodeResources does not yield an error: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCpu := inf.NewDec(8, 0)
|
||||||
|
expectedMem := inf.NewDec(15360, 0)
|
||||||
|
|
||||||
|
actualCpu := resources.Capacity["cpu"].Amount
|
||||||
|
actualMem := resources.Capacity["memory"].Amount
|
||||||
|
|
||||||
|
if actualCpu.Cmp(expectedCpu) != 0 {
|
||||||
|
t.Fatalf("GetNodeResources should return the expected amount of cpu: (expected: %#v, vactual: %#v)", expectedCpu, actualCpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualMem.Cmp(expectedMem) != 0 {
|
||||||
|
t.Fatalf("GetNodeResources should return the expected amount of memory: (expected: %#v, vactual: %#v)", expectedMem, actualMem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
pkg/cloudprovider/mesos/plugins.go
Normal file
21
pkg/cloudprovider/mesos/plugins.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mesos
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/mesos/mesos-go/detector/zoo"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user