Merge branch 'master' into fix/golint2
Conflicts: pkg/api/types.go pkg/health/health_check.go pkg/kubelet/kubelet.go pkg/kubelet/kubelet_server.go pkg/kubelet/kubelet_server_test.go pkg/kubelet/kubelet_test.go
This commit is contained in:
@@ -127,7 +127,7 @@ echo " This might loop forever if there was some uncaught error during start"
|
||||
echo " up."
|
||||
echo
|
||||
|
||||
until $(curl --insecure --user ${user}:${passwd} --max-time 1 \
|
||||
until $(curl --insecure --user ${user}:${passwd} --max-time 5 \
|
||||
--fail --output /dev/null --silent https://${KUBE_MASTER_IP}/api/v1beta1/pods); do
|
||||
printf "."
|
||||
sleep 2
|
||||
|
@@ -2,7 +2,7 @@ version: v1beta2
|
||||
id: cadvisor-agent
|
||||
containers:
|
||||
- name: cadvisor
|
||||
image: google/cadvisor
|
||||
image: google/cadvisor:latest
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
|
@@ -147,20 +147,6 @@ type Container struct {
|
||||
LivenessProbe *LivenessProbe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"`
|
||||
}
|
||||
|
||||
// Percentile represents a pair which contains a percentage from 0 to 100 and
|
||||
// its corresponding value.
|
||||
type Percentile struct {
|
||||
Percentage int `json:"percentage,omitempty"`
|
||||
Value uint64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStats represents statistical information of a container
|
||||
type ContainerStats struct {
|
||||
CPUUsagePercentiles []Percentile `json:"cpu_usage_percentiles,omitempty"`
|
||||
MemoryUsagePercentiles []Percentile `json:"memory_usage_percentiles,omitempty"`
|
||||
MaxMemoryUsage uint64 `json:"max_memory_usage,omitempty"`
|
||||
}
|
||||
|
||||
// Event is the representation of an event logged to etcd backends
|
||||
type Event struct {
|
||||
Event string `json:"event,omitempty"`
|
||||
|
@@ -20,12 +20,14 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
@@ -83,25 +85,37 @@ func MakeAsync(fn WorkFunc) <-chan interface{} {
|
||||
//
|
||||
// TODO: consider migrating this to go-restful which is a more full-featured version of the same thing.
|
||||
type APIServer struct {
|
||||
prefix string
|
||||
storage map[string]RESTStorage
|
||||
ops *Operations
|
||||
logserver http.Handler
|
||||
prefix string
|
||||
storage map[string]RESTStorage
|
||||
ops *Operations
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// New creates a new APIServer object.
|
||||
// 'storage' contains a map of handlers.
|
||||
// 'prefix' is the hosting path prefix.
|
||||
func New(storage map[string]RESTStorage, prefix string) *APIServer {
|
||||
return &APIServer{
|
||||
storage: storage,
|
||||
prefix: prefix,
|
||||
ops: NewOperations(),
|
||||
logserver: http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))),
|
||||
s := &APIServer{
|
||||
storage: storage,
|
||||
prefix: strings.TrimRight(prefix, "/"),
|
||||
ops: NewOperations(),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
s.mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
|
||||
s.mux.HandleFunc(s.prefix+"/", s.ServeREST)
|
||||
healthz.InstallHandler(s.mux)
|
||||
s.mux.HandleFunc("/index.html", s.handleIndex)
|
||||
|
||||
// Handle both operations and operations/* with the same handler
|
||||
opPrefix := path.Join(s.prefix, "operations")
|
||||
s.mux.HandleFunc(opPrefix, s.handleOperationRequest)
|
||||
s.mux.HandleFunc(opPrefix+"/", s.handleOperationRequest)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (server *APIServer) handleIndex(w http.ResponseWriter) {
|
||||
func (server *APIServer) handleIndex(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// TODO: serve this out of a file?
|
||||
data := "<html><body>Welcome to Kubernetes</body></html>"
|
||||
@@ -117,47 +131,37 @@ func (server *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
glog.Infof("APIServer panic'd on %v %v: %#v\n%s\n", req.Method, req.RequestURI, x, debug.Stack())
|
||||
}
|
||||
}()
|
||||
defer MakeLogged(req, &w).StacktraceWhen(
|
||||
StatusIsNot(
|
||||
defer httplog.MakeLogged(req, &w).StacktraceWhen(
|
||||
httplog.StatusIsNot(
|
||||
http.StatusOK,
|
||||
http.StatusAccepted,
|
||||
http.StatusConflict,
|
||||
),
|
||||
).Log()
|
||||
url, err := url.ParseRequestURI(req.RequestURI)
|
||||
if err != nil {
|
||||
server.error(err, w)
|
||||
return
|
||||
}
|
||||
if url.Path == "/index.html" || url.Path == "/" || url.Path == "" {
|
||||
server.handleIndex(w)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(url.Path, "/logs/") {
|
||||
server.logserver.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(url.Path, server.prefix) {
|
||||
|
||||
// Dispatch via our mux.
|
||||
server.mux.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// ServeREST handles requests to all our RESTStorage objects.
|
||||
func (server *APIServer) ServeREST(w http.ResponseWriter, req *http.Request) {
|
||||
if !strings.HasPrefix(req.URL.Path, server.prefix) {
|
||||
server.notFound(req, w)
|
||||
return
|
||||
}
|
||||
requestParts := strings.Split(url.Path[len(server.prefix):], "/")[1:]
|
||||
requestParts := strings.Split(req.URL.Path[len(server.prefix):], "/")[1:]
|
||||
if len(requestParts) < 1 {
|
||||
server.notFound(req, w)
|
||||
return
|
||||
}
|
||||
if requestParts[0] == "operations" {
|
||||
server.handleOperationRequest(requestParts[1:], w, req)
|
||||
return
|
||||
}
|
||||
storage := server.storage[requestParts[0]]
|
||||
if storage == nil {
|
||||
LogOf(w).Addf("'%v' has no storage object", requestParts[0])
|
||||
httplog.LogOf(w).Addf("'%v' has no storage object", requestParts[0])
|
||||
server.notFound(req, w)
|
||||
return
|
||||
}
|
||||
|
||||
server.handleREST(requestParts, url, req, w, storage)
|
||||
server.handleREST(requestParts, req, w, storage)
|
||||
}
|
||||
|
||||
func (server *APIServer) notFound(req *http.Request, w http.ResponseWriter) {
|
||||
@@ -197,7 +201,7 @@ func (server *APIServer) finishReq(out <-chan interface{}, sync bool, timeout ti
|
||||
status := http.StatusOK
|
||||
switch stat := obj.(type) {
|
||||
case api.Status:
|
||||
LogOf(w).Addf("programmer error: use *api.Status as a result, not api.Status.")
|
||||
httplog.LogOf(w).Addf("programmer error: use *api.Status as a result, not api.Status.")
|
||||
if stat.Code != 0 {
|
||||
status = stat.Code
|
||||
}
|
||||
@@ -236,14 +240,14 @@ func parseTimeout(str string) time.Duration {
|
||||
// sync=[false|true] Synchronous request (only applies to create, update, delete operations)
|
||||
// timeout=<duration> Timeout for synchronous requests, only applies if sync=true
|
||||
// labels=<label-selector> Used for filtering list operations
|
||||
func (server *APIServer) handleREST(parts []string, requestURL *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) {
|
||||
sync := requestURL.Query().Get("sync") == "true"
|
||||
timeout := parseTimeout(requestURL.Query().Get("timeout"))
|
||||
func (server *APIServer) handleREST(parts []string, req *http.Request, w http.ResponseWriter, storage RESTStorage) {
|
||||
sync := req.URL.Query().Get("sync") == "true"
|
||||
timeout := parseTimeout(req.URL.Query().Get("timeout"))
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
selector, err := labels.ParseSelector(requestURL.Query().Get("labels"))
|
||||
selector, err := labels.ParseSelector(req.URL.Query().Get("labels"))
|
||||
if err != nil {
|
||||
server.error(err, w)
|
||||
return
|
||||
@@ -325,7 +329,18 @@ func (server *APIServer) handleREST(parts []string, requestURL *url.URL, req *ht
|
||||
}
|
||||
}
|
||||
|
||||
func (server *APIServer) handleOperationRequest(parts []string, w http.ResponseWriter, req *http.Request) {
|
||||
func (server *APIServer) handleOperationRequest(w http.ResponseWriter, req *http.Request) {
|
||||
opPrefix := path.Join(server.prefix, "operations")
|
||||
if !strings.HasPrefix(req.URL.Path, opPrefix) {
|
||||
server.notFound(req, w)
|
||||
return
|
||||
}
|
||||
trimmed := strings.TrimLeft(req.URL.Path[len(opPrefix):], "/")
|
||||
parts := strings.Split(trimmed, "/")
|
||||
if len(parts) > 1 {
|
||||
server.notFound(req, w)
|
||||
return
|
||||
}
|
||||
if req.Method != "GET" {
|
||||
server.notFound(req, w)
|
||||
}
|
||||
|
@@ -409,3 +409,37 @@ func TestSyncCreateTimeout(t *testing.T) {
|
||||
t.Errorf("Unexpected status: %d, Expected: %d, %#v", response.StatusCode, 202, response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpGet(t *testing.T) {
|
||||
simpleStorage := &SimpleRESTStorage{}
|
||||
handler := New(map[string]RESTStorage{
|
||||
"foo": simpleStorage,
|
||||
}, "/prefix/version")
|
||||
server := httptest.NewServer(handler)
|
||||
client := http.Client{}
|
||||
|
||||
simple := Simple{Name: "foo"}
|
||||
data, _ := api.Encode(simple)
|
||||
request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data))
|
||||
expectNoError(t, err)
|
||||
response, err := client.Do(request)
|
||||
expectNoError(t, err)
|
||||
if response.StatusCode != http.StatusAccepted {
|
||||
t.Errorf("Unexpected response %#v", response)
|
||||
}
|
||||
|
||||
var itemOut api.Status
|
||||
body, err := extractBody(response, &itemOut)
|
||||
expectNoError(t, err)
|
||||
if itemOut.Status != api.StatusWorking || itemOut.Details == "" {
|
||||
t.Errorf("Unexpected status: %#v (%s)", itemOut, string(body))
|
||||
}
|
||||
|
||||
req2, err := http.NewRequest("GET", server.URL+"/prefix/version/operations/"+itemOut.Details, nil)
|
||||
expectNoError(t, err)
|
||||
_, err = client.Do(req2)
|
||||
expectNoError(t, err)
|
||||
if response.StatusCode != http.StatusAccepted {
|
||||
t.Errorf("Unexpected response %#v", response)
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ type Operation struct {
|
||||
awaiting <-chan interface{}
|
||||
finished *time.Time
|
||||
lock sync.Mutex
|
||||
notify chan bool
|
||||
notify chan struct{}
|
||||
}
|
||||
|
||||
// Operations tracks all the ongoing operations.
|
||||
@@ -62,7 +62,7 @@ func (ops *Operations) NewOperation(from <-chan interface{}) *Operation {
|
||||
op := &Operation{
|
||||
ID: strconv.FormatInt(id, 10),
|
||||
awaiting: from,
|
||||
notify: make(chan bool, 1),
|
||||
notify: make(chan struct{}),
|
||||
}
|
||||
go op.wait()
|
||||
go ops.insert(op)
|
||||
@@ -116,7 +116,7 @@ func (ops *Operations) expire(maxAge time.Duration) {
|
||||
|
||||
// Waits forever for the operation to complete; call via go when
|
||||
// the operation is created. Sets op.finished when the operation
|
||||
// does complete, and sends on the notify channel, in case there
|
||||
// does complete, and closes the notify channel, in case there
|
||||
// are any WaitFor() calls in progress.
|
||||
// Does not keep op locked while waiting.
|
||||
func (op *Operation) wait() {
|
||||
@@ -128,7 +128,7 @@ func (op *Operation) wait() {
|
||||
op.result = result
|
||||
finished := time.Now()
|
||||
op.finished = &finished
|
||||
op.notify <- true
|
||||
close(op.notify)
|
||||
}
|
||||
|
||||
// WaitFor waits for the specified duration, or until the operation finishes,
|
||||
@@ -137,9 +137,6 @@ func (op *Operation) WaitFor(timeout time.Duration) {
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
case <-op.notify:
|
||||
// Re-send on this channel in case there are others
|
||||
// waiting for notification.
|
||||
op.notify <- true
|
||||
}
|
||||
}
|
||||
|
||||
|
18
pkg/health/doc.go
Normal file
18
pkg/health/doc.go
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 health contains utilities for health checking, as well as health status information.
|
||||
package health
|
51
pkg/health/health.go
Normal file
51
pkg/health/health.go
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
Healthy Status = iota
|
||||
Unhealthy
|
||||
Unknown
|
||||
)
|
||||
|
||||
type HTTPGetInterface interface {
|
||||
Get(url string) (*http.Response, error)
|
||||
}
|
||||
|
||||
func Check(url string, client HTTPGetInterface) (Status, error) {
|
||||
res, err := client.Get(url)
|
||||
if res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
}
|
||||
if res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusBadRequest {
|
||||
return Healthy, nil
|
||||
} else {
|
||||
glog.V(1).Infof("Health check failed for %s, Response: %v", url, *res)
|
||||
return Unhealthy, nil
|
||||
}
|
||||
}
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kubelet
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,23 +25,8 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// HealthCheckStatus is an enum type which describes a status of health check.
|
||||
type HealthCheckStatus int
|
||||
|
||||
// These are the valid values of HealthCheckStatus.
|
||||
const (
|
||||
CheckHealthy HealthCheckStatus = 0
|
||||
CheckUnhealthy HealthCheckStatus = 1
|
||||
CheckUnknown HealthCheckStatus = 2
|
||||
)
|
||||
|
||||
// HealthChecker hides implementation details of checking health of containers.
|
||||
type HealthChecker interface {
|
||||
HealthCheck(container api.Container) (HealthCheckStatus, error)
|
||||
}
|
||||
|
||||
type httpDoInterface interface {
|
||||
Get(string) (*http.Response, error)
|
||||
HealthCheck(container api.Container) (Status, error)
|
||||
}
|
||||
|
||||
// MakeHealthChecker creates a new HealthChecker.
|
||||
@@ -60,20 +45,18 @@ type MuxHealthChecker struct {
|
||||
checkers map[string]HealthChecker
|
||||
}
|
||||
|
||||
// HealthCheck delegates the health-checking of the container to one of the bundled implementations.
|
||||
// It chooses an implementation according to container.LivenessProbe.Type.
|
||||
func (m *MuxHealthChecker) HealthCheck(container api.Container) (HealthCheckStatus, error) {
|
||||
func (m *MuxHealthChecker) HealthCheck(container api.Container) (Status, error) {
|
||||
checker, ok := m.checkers[container.LivenessProbe.Type]
|
||||
if !ok || checker == nil {
|
||||
glog.Warningf("Failed to find health checker for %s %s", container.Name, container.LivenessProbe.Type)
|
||||
return CheckUnknown, nil
|
||||
return Unknown, nil
|
||||
}
|
||||
return checker.HealthCheck(container)
|
||||
}
|
||||
|
||||
// HTTPHealthChecker is an implementation of HealthChecker which checks container health by sending HTTP Get requests.
|
||||
type HTTPHealthChecker struct {
|
||||
client httpDoInterface
|
||||
client HTTPGetInterface
|
||||
}
|
||||
|
||||
func (h *HTTPHealthChecker) findPort(container api.Container, portName string) int64 {
|
||||
@@ -86,18 +69,17 @@ func (h *HTTPHealthChecker) findPort(container api.Container, portName string) i
|
||||
return -1
|
||||
}
|
||||
|
||||
// HealthCheck checks if the container is healthy by trying sending HTTP Get requests to the container.
|
||||
func (h *HTTPHealthChecker) HealthCheck(container api.Container) (HealthCheckStatus, error) {
|
||||
func (h *HTTPHealthChecker) HealthCheck(container api.Container) (Status, error) {
|
||||
params := container.LivenessProbe.HTTPGet
|
||||
if params == nil {
|
||||
return CheckUnknown, fmt.Errorf("Error, no HTTP parameters specified: %v", container)
|
||||
return Unknown, fmt.Errorf("Error, no HTTP parameters specified: %v", container)
|
||||
}
|
||||
port := h.findPort(container, params.Port)
|
||||
if port == -1 {
|
||||
var err error
|
||||
port, err = strconv.ParseInt(params.Port, 10, 0)
|
||||
if err != nil {
|
||||
return CheckUnknown, err
|
||||
return Unknown, err
|
||||
}
|
||||
}
|
||||
var host string
|
||||
@@ -107,17 +89,5 @@ func (h *HTTPHealthChecker) HealthCheck(container api.Container) (HealthCheckSta
|
||||
host = "localhost"
|
||||
}
|
||||
url := fmt.Sprintf("http://%s:%d%s", host, port, params.Path)
|
||||
res, err := h.client.Get(url)
|
||||
if res != nil && res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
// At this point, if it fails, its either a policy (unlikely) or HTTP protocol (likely) error.
|
||||
return CheckUnhealthy, nil
|
||||
}
|
||||
if res.StatusCode == http.StatusOK {
|
||||
return CheckHealthy, nil
|
||||
}
|
||||
glog.V(1).Infof("Health check failed for %v, Response: %v", container, *res)
|
||||
return CheckUnhealthy, nil
|
||||
return Check(url, h.client)
|
||||
}
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kubelet
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -56,7 +56,7 @@ func TestHttpHealth(t *testing.T) {
|
||||
}
|
||||
|
||||
ok, err := check.HealthCheck(container)
|
||||
if ok != CheckHealthy {
|
||||
if ok != Healthy {
|
||||
t.Error("Unexpected unhealthy")
|
||||
}
|
||||
if err != nil {
|
@@ -29,3 +29,7 @@ func handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func InstallHandler(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/healthz", handleHealthz)
|
||||
}
|
||||
|
19
pkg/httplog/doc.go
Normal file
19
pkg/httplog/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 httplog contains a helper object and functions to maintain a log
|
||||
// along with an http response.
|
||||
package httplog
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package apiserver
|
||||
package httplog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,6 +25,18 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Handler wraps all HTTP calls to delegate with nice logging.
|
||||
// delegate may use LogOf(w).Addf(...) to write additional info to
|
||||
// the per-request log message.
|
||||
//
|
||||
// Intended to wrap calls to your ServeMux.
|
||||
func Handler(delegate http.Handler, pred StacktracePred) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
defer MakeLogged(req, &w).StacktraceWhen(pred).Log()
|
||||
delegate.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// StacktracePred returns true if a stacktrace should be logged for this status
|
||||
type StacktracePred func(httpStatus int) (logStacktrace bool)
|
||||
|
||||
@@ -44,7 +56,7 @@ type respLogger struct {
|
||||
|
||||
// DefaultStacktracePred is the default implementation of StacktracePred.
|
||||
func DefaultStacktracePred(status int) bool {
|
||||
return status != http.StatusOK && status != http.StatusAccepted
|
||||
return status < http.StatusOK || status >= http.StatusBadRequest
|
||||
}
|
||||
|
||||
// MakeLogged turns a normal response writer into a logged response writer.
|
||||
@@ -60,6 +72,10 @@ func DefaultStacktracePred(status int) bool {
|
||||
//
|
||||
// Use LogOf(w).Addf(...) to log something along with the response result.
|
||||
func MakeLogged(req *http.Request, w *http.ResponseWriter) *respLogger {
|
||||
if _, ok := (*w).(*respLogger); ok {
|
||||
// Don't double-wrap!
|
||||
panic("multiple MakeLogged calls!")
|
||||
}
|
||||
rl := &respLogger{
|
||||
startTime: time.Now(),
|
||||
req: req,
|
138
pkg/kubelet/docker.go
Normal file
138
pkg/kubelet/docker.go
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 kubelet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// DockerContainerData is the structured representation of the JSON object returned by Docker inspect
|
||||
type DockerContainerData struct {
|
||||
state struct {
|
||||
Running bool
|
||||
}
|
||||
}
|
||||
|
||||
// DockerInterface is an abstract interface for testability. It abstracts the interface of docker.Client.
|
||||
type DockerInterface interface {
|
||||
ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error)
|
||||
InspectContainer(id string) (*docker.Container, error)
|
||||
CreateContainer(docker.CreateContainerOptions) (*docker.Container, error)
|
||||
StartContainer(id string, hostConfig *docker.HostConfig) error
|
||||
StopContainer(id string, timeout uint) error
|
||||
PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error
|
||||
}
|
||||
|
||||
// DockerID is an ID of docker container. It is a type to make it clear when we're working with docker container Ids
|
||||
type DockerID string
|
||||
|
||||
// DockerPuller is an abstract interface for testability. It abstracts image pull operations.
|
||||
type DockerPuller interface {
|
||||
Pull(image string) error
|
||||
}
|
||||
|
||||
// dockerPuller is the default implementation of DockerPuller.
|
||||
type dockerPuller struct {
|
||||
client DockerInterface
|
||||
}
|
||||
|
||||
// NewDockerPuller creates a new instance of the default implementation of DockerPuller.
|
||||
func NewDockerPuller(client DockerInterface) DockerPuller {
|
||||
return dockerPuller{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (p dockerPuller) Pull(image string) error {
|
||||
image, tag := parseImageName(image)
|
||||
opts := docker.PullImageOptions{
|
||||
Repository: image,
|
||||
Tag: tag,
|
||||
}
|
||||
return p.client.PullImage(opts, docker.AuthConfiguration{})
|
||||
}
|
||||
|
||||
// Converts "-" to "_-_" and "_" to "___" so that we can use "--" to meaningfully separate parts of a docker name.
|
||||
func escapeDash(in string) (out string) {
|
||||
out = strings.Replace(in, "_", "___", -1)
|
||||
out = strings.Replace(out, "-", "_-_", -1)
|
||||
return
|
||||
}
|
||||
|
||||
// Reverses the transformation of escapeDash.
|
||||
func unescapeDash(in string) (out string) {
|
||||
out = strings.Replace(in, "_-_", "-", -1)
|
||||
out = strings.Replace(out, "___", "_", -1)
|
||||
return
|
||||
}
|
||||
|
||||
const containerNamePrefix = "k8s"
|
||||
|
||||
// Creates a name which can be reversed to identify both manifest id and container name.
|
||||
func buildDockerName(manifest *api.ContainerManifest, container *api.Container) string {
|
||||
// Note, manifest.ID could be blank.
|
||||
return fmt.Sprintf("%s--%s--%s--%08x", containerNamePrefix, escapeDash(container.Name), escapeDash(manifest.ID), rand.Uint32())
|
||||
}
|
||||
|
||||
// Upacks a container name, returning the manifest id and container name we would have used to
|
||||
// construct the docker name. If the docker name isn't one we created, we may return empty strings.
|
||||
func parseDockerName(name string) (manifestID, containerName string) {
|
||||
// For some reason docker appears to be appending '/' to names.
|
||||
// If its there, strip it.
|
||||
if name[0] == '/' {
|
||||
name = name[1:]
|
||||
}
|
||||
parts := strings.Split(name, "--")
|
||||
if len(parts) == 0 || parts[0] != containerNamePrefix {
|
||||
return
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
containerName = unescapeDash(parts[1])
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
manifestID = unescapeDash(parts[2])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Parses image name including an tag and returns image name and tag
|
||||
// TODO: Future Docker versions can parse the tag on daemon side, see:
|
||||
// https://github.com/dotcloud/docker/issues/6876
|
||||
// So this can be deprecated at some point.
|
||||
func parseImageName(image string) (string, string) {
|
||||
tag := ""
|
||||
parts := strings.SplitN(image, "/", 2)
|
||||
repo := ""
|
||||
if len(parts) == 2 {
|
||||
repo = parts[0]
|
||||
image = parts[1]
|
||||
}
|
||||
parts = strings.SplitN(image, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
image = parts[0]
|
||||
tag = parts[1]
|
||||
}
|
||||
if repo != "" {
|
||||
image = fmt.Sprintf("%s/%s", repo, image)
|
||||
}
|
||||
return image, tag
|
||||
}
|
@@ -21,7 +21,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -34,6 +33,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/health"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/coreos/go-etcd/etcd"
|
||||
@@ -46,34 +46,14 @@ import (
|
||||
|
||||
const defaultChanSize = 1024
|
||||
|
||||
// DockerContainerData is the structured representation of the JSON object returned by Docker inspect
|
||||
type DockerContainerData struct {
|
||||
state struct {
|
||||
Running bool
|
||||
}
|
||||
}
|
||||
|
||||
// DockerInterface is an abstract interface for testability. It abstracts the interface of docker.Client.
|
||||
type DockerInterface interface {
|
||||
ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error)
|
||||
InspectContainer(id string) (*docker.Container, error)
|
||||
CreateContainer(docker.CreateContainerOptions) (*docker.Container, error)
|
||||
StartContainer(id string, hostConfig *docker.HostConfig) error
|
||||
StopContainer(id string, timeout uint) error
|
||||
PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error
|
||||
}
|
||||
|
||||
// DockerID is an ID of docker container. It is a type to make it clear when we're working with docker container Ids
|
||||
type DockerID string
|
||||
|
||||
// DockerPuller is an abstract interface for testability. It abstracts image pull operations.
|
||||
type DockerPuller interface {
|
||||
Pull(image string) error
|
||||
}
|
||||
// taken from lmctfy https://github.com/google/lmctfy/blob/master/lmctfy/controllers/cpu_controller.cc
|
||||
const minShares = 2
|
||||
const sharesPerCpu = 1024
|
||||
const milliCpuToCpu = 1000
|
||||
|
||||
// CadvisorInterface is an abstract interface for testability. It abstracts the interface of "github.com/google/cadvisor/client".Client.
|
||||
type CadvisorInterface interface {
|
||||
ContainerInfo(name string) (*info.ContainerInfo, error)
|
||||
ContainerInfo(name string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
MachineInfo() (*info.MachineInfo, error)
|
||||
}
|
||||
|
||||
@@ -93,7 +73,7 @@ type Kubelet struct {
|
||||
SyncFrequency time.Duration
|
||||
HTTPCheckFrequency time.Duration
|
||||
pullLock sync.Mutex
|
||||
HealthChecker HealthChecker
|
||||
HealthChecker health.HealthChecker
|
||||
}
|
||||
|
||||
type manifestUpdate struct {
|
||||
@@ -119,7 +99,7 @@ func (kl *Kubelet) RunKubelet(dockerEndpoint, configPath, manifestURL, etcdServe
|
||||
}
|
||||
}
|
||||
if kl.DockerPuller == nil {
|
||||
kl.DockerPuller = kl.MakeDockerPuller()
|
||||
kl.DockerPuller = NewDockerPuller(kl.DockerClient)
|
||||
}
|
||||
updateChannel := make(chan manifestUpdate)
|
||||
if configPath != "" {
|
||||
@@ -158,7 +138,7 @@ func (kl *Kubelet) RunKubelet(dockerEndpoint, configPath, manifestURL, etcdServe
|
||||
}
|
||||
go util.Forever(func() { s.ListenAndServe() }, 0)
|
||||
}
|
||||
kl.HealthChecker = MakeHealthChecker()
|
||||
kl.HealthChecker = health.MakeHealthChecker()
|
||||
kl.syncLoop(updateChannel, kl)
|
||||
}
|
||||
|
||||
@@ -236,70 +216,6 @@ func (kl *Kubelet) getContainer(ID DockerID) (*docker.APIContainers, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MakeDockerPuller creates a new instance of the default implementation of DockerPuller.
|
||||
func (kl *Kubelet) MakeDockerPuller() DockerPuller {
|
||||
return dockerPuller{
|
||||
client: kl.DockerClient,
|
||||
}
|
||||
}
|
||||
|
||||
// dockerPuller is the default implementation of DockerPuller.
|
||||
type dockerPuller struct {
|
||||
client DockerInterface
|
||||
}
|
||||
|
||||
func (p dockerPuller) Pull(image string) error {
|
||||
image, tag := parseImageName(image)
|
||||
opts := docker.PullImageOptions{
|
||||
Repository: image,
|
||||
Tag: tag,
|
||||
}
|
||||
return p.client.PullImage(opts, docker.AuthConfiguration{})
|
||||
}
|
||||
|
||||
// Converts "-" to "_-_" and "_" to "___" so that we can use "--" to meaningfully separate parts of a docker name.
|
||||
func escapeDash(in string) (out string) {
|
||||
out = strings.Replace(in, "_", "___", -1)
|
||||
out = strings.Replace(out, "-", "_-_", -1)
|
||||
return
|
||||
}
|
||||
|
||||
// Reverses the transformation of escapeDash.
|
||||
func unescapeDash(in string) (out string) {
|
||||
out = strings.Replace(in, "_-_", "-", -1)
|
||||
out = strings.Replace(out, "___", "_", -1)
|
||||
return
|
||||
}
|
||||
|
||||
const containerNamePrefix = "k8s"
|
||||
|
||||
// Creates a name which can be reversed to identify both manifest id and container name.
|
||||
func buildDockerName(manifest *api.ContainerManifest, container *api.Container) string {
|
||||
// Note, manifest.ID could be blank.
|
||||
return fmt.Sprintf("%s--%s--%s--%08x", containerNamePrefix, escapeDash(container.Name), escapeDash(manifest.ID), rand.Uint32())
|
||||
}
|
||||
|
||||
// Upacks a container name, returning the manifest id and container name we would have used to
|
||||
// construct the docker name. If the docker name isn't one we created, we may return empty strings.
|
||||
func parseDockerName(name string) (manifestID, containerName string) {
|
||||
// For some reason docker appears to be appending '/' to names.
|
||||
// If its there, strip it.
|
||||
if name[0] == '/' {
|
||||
name = name[1:]
|
||||
}
|
||||
parts := strings.Split(name, "--")
|
||||
if len(parts) == 0 || parts[0] != containerNamePrefix {
|
||||
return
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
containerName = unescapeDash(parts[1])
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
manifestID = unescapeDash(parts[2])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeEnvironmentVariables(container *api.Container) []string {
|
||||
var result []string
|
||||
for _, value := range container.Env {
|
||||
@@ -358,27 +274,13 @@ func makePortsAndBindings(container *api.Container) (map[docker.Port]struct{}, m
|
||||
return exposedPorts, portBindings
|
||||
}
|
||||
|
||||
// Parses image name including an tag and returns image name and tag
|
||||
// TODO: Future Docker versions can parse the tag on daemon side, see:
|
||||
// https://github.com/dotcloud/docker/issues/6876
|
||||
// So this can be deprecated at some point.
|
||||
func parseImageName(image string) (string, string) {
|
||||
tag := ""
|
||||
parts := strings.SplitN(image, "/", 2)
|
||||
repo := ""
|
||||
if len(parts) == 2 {
|
||||
repo = parts[0]
|
||||
image = parts[1]
|
||||
func milliCpuToShares(milliCpu int) int {
|
||||
// Conceptually (milliCpu / milliCpuToCpu) * sharesPerCpu, but factored to improve rounding.
|
||||
shares := (milliCpu * sharesPerCpu) / milliCpuToCpu
|
||||
if shares < minShares {
|
||||
return minShares
|
||||
}
|
||||
parts = strings.SplitN(image, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
image = parts[0]
|
||||
tag = parts[1]
|
||||
}
|
||||
if repo != "" {
|
||||
image = fmt.Sprintf("%s/%s", repo, image)
|
||||
}
|
||||
return image, tag
|
||||
return shares
|
||||
}
|
||||
|
||||
// Run a single container from a manifest. Returns the docker container ID
|
||||
@@ -390,13 +292,15 @@ func (kl *Kubelet) runContainer(manifest *api.ContainerManifest, container *api.
|
||||
opts := docker.CreateContainerOptions{
|
||||
Name: buildDockerName(manifest, container),
|
||||
Config: &docker.Config{
|
||||
Cmd: container.Command,
|
||||
Env: envVariables,
|
||||
ExposedPorts: exposedPorts,
|
||||
Hostname: container.Name,
|
||||
Image: container.Image,
|
||||
ExposedPorts: exposedPorts,
|
||||
Env: envVariables,
|
||||
Memory: int64(container.Memory),
|
||||
CpuShares: int64(milliCpuToShares(container.CPU)),
|
||||
Volumes: volumes,
|
||||
WorkingDir: container.WorkingDir,
|
||||
Cmd: container.Command,
|
||||
},
|
||||
}
|
||||
dockerContainer, err := kl.DockerClient.CreateContainer(opts)
|
||||
@@ -735,7 +639,7 @@ func (kl *Kubelet) syncManifest(manifest *api.ContainerManifest, keepChannel cha
|
||||
glog.V(1).Infof("health check errored: %v", err)
|
||||
continue
|
||||
}
|
||||
if healthy != CheckHealthy {
|
||||
if healthy != health.Healthy {
|
||||
glog.V(1).Infof("manifest %s container %s is unhealthy.", manifest.ID, container.Name)
|
||||
if err != nil {
|
||||
glog.V(1).Infof("Failed to get container info %v, for %s", err, containerID)
|
||||
@@ -939,44 +843,29 @@ func (kl *Kubelet) getDockerIDFromPodIDAndContainerName(podID, containerName str
|
||||
return "", errors.New("couldn't find container")
|
||||
}
|
||||
|
||||
func getCadvisorContainerInfoRequest(req *info.ContainerInfoRequest) *info.ContainerInfoRequest {
|
||||
ret := &info.ContainerInfoRequest{
|
||||
NumStats: req.NumStats,
|
||||
CpuUsagePercentiles: req.CpuUsagePercentiles,
|
||||
MemoryUsagePercentages: req.MemoryUsagePercentages,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// This method takes a container's absolute path and returns the stats for the
|
||||
// container. The container's absolute path refers to its hierarchy in the
|
||||
// cgroup file system. e.g. The root container, which represents the whole
|
||||
// machine, has path "/"; all docker containers have path "/docker/<docker id>"
|
||||
func (kl *Kubelet) statsFromContainerPath(containerPath string) (*api.ContainerStats, error) {
|
||||
info, err := kl.CadvisorClient.ContainerInfo(containerPath)
|
||||
|
||||
func (kl *Kubelet) statsFromContainerPath(containerPath string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
cinfo, err := kl.CadvisorClient.ContainerInfo(containerPath, getCadvisorContainerInfoRequest(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// When the stats data for the container is not available yet.
|
||||
if info.StatsPercentiles == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := new(api.ContainerStats)
|
||||
ret.MaxMemoryUsage = info.StatsPercentiles.MaxMemoryUsage
|
||||
if len(info.StatsPercentiles.CpuUsagePercentiles) > 0 {
|
||||
percentiles := make([]api.Percentile, len(info.StatsPercentiles.CpuUsagePercentiles))
|
||||
for i, p := range info.StatsPercentiles.CpuUsagePercentiles {
|
||||
percentiles[i].Percentage = p.Percentage
|
||||
percentiles[i].Value = p.Value
|
||||
}
|
||||
ret.CPUUsagePercentiles = percentiles
|
||||
}
|
||||
if len(info.StatsPercentiles.MemoryUsagePercentiles) > 0 {
|
||||
percentiles := make([]api.Percentile, len(info.StatsPercentiles.MemoryUsagePercentiles))
|
||||
for i, p := range info.StatsPercentiles.MemoryUsagePercentiles {
|
||||
percentiles[i].Percentage = p.Percentage
|
||||
percentiles[i].Value = p.Value
|
||||
}
|
||||
ret.MemoryUsagePercentiles = percentiles
|
||||
}
|
||||
return ret, nil
|
||||
return cinfo, nil
|
||||
}
|
||||
|
||||
// GetContainerStats returns stats (from Cadvisor) for a container.
|
||||
func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) {
|
||||
func (kl *Kubelet) GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
if kl.CadvisorClient == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -984,24 +873,24 @@ func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.Containe
|
||||
if err != nil || len(dockerID) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID)))
|
||||
return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID)), req)
|
||||
}
|
||||
|
||||
// GetMachineStats returns stats (from Cadvisor) of current machine.
|
||||
func (kl *Kubelet) GetMachineStats() (*api.ContainerStats, error) {
|
||||
return kl.statsFromContainerPath("/")
|
||||
func (kl *Kubelet) GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
return kl.statsFromContainerPath("/", req)
|
||||
}
|
||||
|
||||
func (kl *Kubelet) healthy(container api.Container, dockerContainer *docker.APIContainers) (HealthCheckStatus, error) {
|
||||
func (kl *Kubelet) healthy(container api.Container, dockerContainer *docker.APIContainers) (health.Status, error) {
|
||||
// Give the container 60 seconds to start up.
|
||||
if container.LivenessProbe == nil {
|
||||
return CheckHealthy, nil
|
||||
return health.Healthy, nil
|
||||
}
|
||||
if time.Now().Unix()-dockerContainer.Created < container.LivenessProbe.InitialDelaySeconds {
|
||||
return CheckHealthy, nil
|
||||
return health.Healthy, nil
|
||||
}
|
||||
if kl.HealthChecker == nil {
|
||||
return CheckHealthy, nil
|
||||
return health.Healthy, nil
|
||||
}
|
||||
return kl.HealthChecker.HealthCheck(container)
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -27,7 +28,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
|
||||
"github.com/google/cadvisor/info"
|
||||
"gopkg.in/v1/yaml"
|
||||
)
|
||||
|
||||
@@ -41,8 +43,8 @@ type Server struct {
|
||||
// kubeletInterface contains all the kubelet methods required by the server.
|
||||
// For testablitiy.
|
||||
type kubeletInterface interface {
|
||||
GetContainerStats(podID, containerName string) (*api.ContainerStats, error)
|
||||
GetMachineStats() (*api.ContainerStats, error)
|
||||
GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
GetPodInfo(name string) (api.PodInfo, error)
|
||||
}
|
||||
|
||||
@@ -51,13 +53,14 @@ func (s *Server) error(w http.ResponseWriter, err error) {
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
defer apiserver.MakeLogged(req, &w).Log()
|
||||
defer httplog.MakeLogged(req, &w).Log()
|
||||
|
||||
u, err := url.ParseRequestURI(req.RequestURI)
|
||||
if err != nil {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
// TODO: use an http.ServeMux instead of a switch.
|
||||
switch {
|
||||
case u.Path == "/container" || u.Path == "/containers":
|
||||
defer req.Body.Close()
|
||||
@@ -113,18 +116,25 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
func (s *Server) serveStats(w http.ResponseWriter, req *http.Request) {
|
||||
// /stats/<podid>/<containerName>
|
||||
components := strings.Split(strings.TrimPrefix(path.Clean(req.URL.Path), "/"), "/")
|
||||
var stats *api.ContainerStats
|
||||
var stats *info.ContainerInfo
|
||||
var err error
|
||||
var query info.ContainerInfoRequest
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
err = decoder.Decode(&query)
|
||||
if err != nil && err != io.EOF {
|
||||
s.error(w, err)
|
||||
return
|
||||
}
|
||||
switch len(components) {
|
||||
case 1:
|
||||
// Machine stats
|
||||
stats, err = s.Kubelet.GetMachineStats()
|
||||
stats, err = s.Kubelet.GetMachineStats(&query)
|
||||
case 2:
|
||||
// pod stats
|
||||
// TODO(monnand) Implement this
|
||||
errors.New("pod level status currently unimplemented")
|
||||
case 3:
|
||||
stats, err = s.Kubelet.GetContainerStats(components[1], components[2])
|
||||
stats, err = s.Kubelet.GetContainerInfo(components[1], components[2], &query)
|
||||
default:
|
||||
http.Error(w, "unknown resource.", http.StatusNotFound)
|
||||
return
|
||||
|
@@ -29,24 +29,25 @@ import (
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/google/cadvisor/info"
|
||||
)
|
||||
|
||||
type fakeKubelet struct {
|
||||
infoFunc func(name string) (api.PodInfo, error)
|
||||
containerStatsFunc func(podID, containerName string) (*api.ContainerStats, error)
|
||||
machineStatsFunc func() (*api.ContainerStats, error)
|
||||
containerStatsFunc func(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
machineStatsFunc func(query *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
}
|
||||
|
||||
func (fk *fakeKubelet) GetPodInfo(name string) (api.PodInfo, error) {
|
||||
return fk.infoFunc(name)
|
||||
}
|
||||
|
||||
func (fk *fakeKubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) {
|
||||
return fk.containerStatsFunc(podID, containerName)
|
||||
func (fk *fakeKubelet) GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
return fk.containerStatsFunc(podID, containerName, req)
|
||||
}
|
||||
|
||||
func (fk *fakeKubelet) GetMachineStats() (*api.ContainerStats, error) {
|
||||
return fk.machineStatsFunc()
|
||||
func (fk *fakeKubelet) GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
return fk.machineStatsFunc(req)
|
||||
}
|
||||
|
||||
type serverTestFramework struct {
|
||||
@@ -148,22 +149,24 @@ func TestPodInfo(t *testing.T) {
|
||||
|
||||
func TestContainerStats(t *testing.T) {
|
||||
fw := makeServerTest()
|
||||
expectedStats := &api.ContainerStats{
|
||||
MaxMemoryUsage: 1024001,
|
||||
CPUUsagePercentiles: []api.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
MemoryUsagePercentiles: []api.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
expectedStats := &info.ContainerInfo{
|
||||
StatsPercentiles: &info.ContainerStatsPercentiles{
|
||||
MaxMemoryUsage: 1024001,
|
||||
CpuUsagePercentiles: []info.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
MemoryUsagePercentiles: []info.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedPodID := "somepod"
|
||||
expectedContainerName := "goodcontainer"
|
||||
fw.fakeKubelet.containerStatsFunc = func(podID, containerName string) (*api.ContainerStats, error) {
|
||||
fw.fakeKubelet.containerStatsFunc = func(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
if podID != expectedPodID || containerName != expectedContainerName {
|
||||
return nil, fmt.Errorf("bad podID or containerName: podID=%v; containerName=%v", podID, containerName)
|
||||
}
|
||||
@@ -175,7 +178,7 @@ func TestContainerStats(t *testing.T) {
|
||||
t.Fatalf("Got error GETing: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var receivedStats api.ContainerStats
|
||||
var receivedStats info.ContainerInfo
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err = decoder.Decode(&receivedStats)
|
||||
if err != nil {
|
||||
@@ -188,20 +191,22 @@ func TestContainerStats(t *testing.T) {
|
||||
|
||||
func TestMachineStats(t *testing.T) {
|
||||
fw := makeServerTest()
|
||||
expectedStats := &api.ContainerStats{
|
||||
MaxMemoryUsage: 1024001,
|
||||
CPUUsagePercentiles: []api.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
MemoryUsagePercentiles: []api.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
expectedStats := &info.ContainerInfo{
|
||||
StatsPercentiles: &info.ContainerStatsPercentiles{
|
||||
MaxMemoryUsage: 1024001,
|
||||
CpuUsagePercentiles: []info.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
MemoryUsagePercentiles: []info.Percentile{
|
||||
{50, 150},
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
},
|
||||
}
|
||||
fw.fakeKubelet.machineStatsFunc = func() (*api.ContainerStats, error) {
|
||||
fw.fakeKubelet.machineStatsFunc = func(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
return expectedStats, nil
|
||||
}
|
||||
|
||||
@@ -210,7 +215,7 @@ func TestMachineStats(t *testing.T) {
|
||||
t.Fatalf("Got error GETing: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var receivedStats api.ContainerStats
|
||||
var receivedStats info.ContainerInfo
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err = decoder.Decode(&receivedStats)
|
||||
if err != nil {
|
||||
|
@@ -26,6 +26,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/health"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/coreos/go-etcd/etcd"
|
||||
@@ -324,9 +325,7 @@ func TestGetKubeletStateFromEtcdNotFound(t *testing.T) {
|
||||
reader := startReading(channel)
|
||||
fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{},
|
||||
E: &etcd.EtcdError{
|
||||
ErrorCode: 100,
|
||||
},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
err := kubelet.getKubeletStateFromEtcd("/registry/hosts/machine/kubelet", channel)
|
||||
expectNoError(t, err)
|
||||
@@ -424,8 +423,8 @@ func TestSyncManifestsDeletes(t *testing.T) {
|
||||
|
||||
type FalseHealthChecker struct{}
|
||||
|
||||
func (f *FalseHealthChecker) HealthCheck(container api.Container) (HealthCheckStatus, error) {
|
||||
return CheckUnhealthy, nil
|
||||
func (f *FalseHealthChecker) HealthCheck(container api.Container) (health.Status, error) {
|
||||
return health.Unhealthy, nil
|
||||
}
|
||||
|
||||
func TestSyncManifestsUnhealthy(t *testing.T) {
|
||||
@@ -913,8 +912,8 @@ type mockCadvisorClient struct {
|
||||
}
|
||||
|
||||
// ContainerInfo is a mock implementation of CadvisorInterface.ContainerInfo.
|
||||
func (c *mockCadvisorClient) ContainerInfo(name string) (*info.ContainerInfo, error) {
|
||||
args := c.Called(name)
|
||||
func (c *mockCadvisorClient) ContainerInfo(name string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
args := c.Called(name, req)
|
||||
return args.Get(0).(*info.ContainerInfo), args.Error(1)
|
||||
}
|
||||
|
||||
@@ -926,7 +925,7 @@ func (c *mockCadvisorClient) MachineInfo() (*info.MachineInfo, error) {
|
||||
|
||||
func areSamePercentiles(
|
||||
cadvisorPercentiles []info.Percentile,
|
||||
kubePercentiles []api.Percentile,
|
||||
kubePercentiles []info.Percentile,
|
||||
t *testing.T,
|
||||
) {
|
||||
if len(cadvisorPercentiles) != len(kubePercentiles) {
|
||||
@@ -966,7 +965,7 @@ func TestGetContainerStats(t *testing.T) {
|
||||
{80, 180},
|
||||
{90, 190},
|
||||
},
|
||||
CPUUsagePercentiles: []info.Percentile{
|
||||
CpuUsagePercentiles: []info.Percentile{
|
||||
{51, 101},
|
||||
{81, 181},
|
||||
{91, 191},
|
||||
@@ -975,7 +974,9 @@ func TestGetContainerStats(t *testing.T) {
|
||||
}
|
||||
|
||||
mockCadvisor := &mockCadvisorClient{}
|
||||
mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, nil)
|
||||
req := &info.ContainerInfoRequest{}
|
||||
cadvisorReq := getCadvisorContainerInfoRequest(req)
|
||||
mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, nil)
|
||||
|
||||
kubelet, _, fakeDocker := makeTestKubelet(t)
|
||||
kubelet.CadvisorClient = mockCadvisor
|
||||
@@ -988,15 +989,15 @@ func TestGetContainerStats(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
stats, err := kubelet.GetContainerStats("qux", "foo")
|
||||
stats, err := kubelet.GetContainerInfo("qux", "foo", req)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if stats.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage {
|
||||
if stats.StatsPercentiles.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage {
|
||||
t.Errorf("wrong max memory usage")
|
||||
}
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.CPUUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.MemoryUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.StatsPercentiles.CpuUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.StatsPercentiles.MemoryUsagePercentiles, t)
|
||||
mockCadvisor.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -1020,7 +1021,9 @@ func TestGetMachineStats(t *testing.T) {
|
||||
}
|
||||
|
||||
mockCadvisor := &mockCadvisorClient{}
|
||||
mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, nil)
|
||||
req := &info.ContainerInfoRequest{}
|
||||
cadvisorReq := getCadvisorContainerInfoRequest(req)
|
||||
mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, nil)
|
||||
|
||||
kubelet := Kubelet{
|
||||
DockerClient: &fakeDocker,
|
||||
@@ -1029,15 +1032,15 @@ func TestGetMachineStats(t *testing.T) {
|
||||
}
|
||||
|
||||
// If the container name is an empty string, then it means the root container.
|
||||
stats, err := kubelet.GetMachineStats()
|
||||
stats, err := kubelet.GetMachineStats(req)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if stats.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage {
|
||||
if stats.StatsPercentiles.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage {
|
||||
t.Errorf("wrong max memory usage")
|
||||
}
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.CPUUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.MemoryUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.StatsPercentiles.CpuUsagePercentiles, t)
|
||||
areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.StatsPercentiles.MemoryUsagePercentiles, t)
|
||||
mockCadvisor.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -1052,19 +1055,19 @@ func TestGetContainerStatsWithoutCadvisor(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
stats, _ := kubelet.GetContainerStats("qux", "foo")
|
||||
stats, _ := kubelet.GetContainerInfo("qux", "foo", nil)
|
||||
// When there's no cAdvisor, the stats should be either nil or empty
|
||||
if stats == nil {
|
||||
return
|
||||
}
|
||||
if stats.MaxMemoryUsage != 0 {
|
||||
t.Errorf("MaxMemoryUsage is %v even if there's no cadvisor", stats.MaxMemoryUsage)
|
||||
if stats.StatsPercentiles.MaxMemoryUsage != 0 {
|
||||
t.Errorf("MaxMemoryUsage is %v even if there's no cadvisor", stats.StatsPercentiles.MaxMemoryUsage)
|
||||
}
|
||||
if len(stats.CPUUsagePercentiles) > 0 {
|
||||
t.Errorf("Cpu usage percentiles is not empty (%+v) even if there's no cadvisor", stats.CPUUsagePercentiles)
|
||||
if len(stats.StatsPercentiles.CpuUsagePercentiles) > 0 {
|
||||
t.Errorf("Cpu usage percentiles is not empty (%+v) even if there's no cadvisor", stats.StatsPercentiles.CpuUsagePercentiles)
|
||||
}
|
||||
if len(stats.MemoryUsagePercentiles) > 0 {
|
||||
t.Errorf("Memory usage percentiles is not empty (%+v) even if there's no cadvisor", stats.MemoryUsagePercentiles)
|
||||
if len(stats.StatsPercentiles.MemoryUsagePercentiles) > 0 {
|
||||
t.Errorf("Memory usage percentiles is not empty (%+v) even if there's no cadvisor", stats.StatsPercentiles.MemoryUsagePercentiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1074,8 +1077,10 @@ func TestGetContainerStatsWhenCadvisorFailed(t *testing.T) {
|
||||
|
||||
containerInfo := &info.ContainerInfo{}
|
||||
mockCadvisor := &mockCadvisorClient{}
|
||||
req := &info.ContainerInfoRequest{}
|
||||
cadvisorReq := getCadvisorContainerInfoRequest(req)
|
||||
expectedErr := fmt.Errorf("some error")
|
||||
mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, expectedErr)
|
||||
mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, expectedErr)
|
||||
|
||||
kubelet, _, fakeDocker := makeTestKubelet(t)
|
||||
kubelet.CadvisorClient = mockCadvisor
|
||||
@@ -1088,7 +1093,7 @@ func TestGetContainerStatsWhenCadvisorFailed(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
stats, err := kubelet.GetContainerStats("qux", "foo")
|
||||
stats, err := kubelet.GetContainerInfo("qux", "foo", req)
|
||||
if stats != nil {
|
||||
t.Errorf("non-nil stats on error")
|
||||
}
|
||||
@@ -1109,7 +1114,7 @@ func TestGetContainerStatsOnNonExistContainer(t *testing.T) {
|
||||
kubelet.CadvisorClient = mockCadvisor
|
||||
fakeDocker.containerList = []docker.APIContainers{}
|
||||
|
||||
stats, _ := kubelet.GetContainerStats("qux", "foo")
|
||||
stats, _ := kubelet.GetContainerInfo("qux", "foo", nil)
|
||||
if stats != nil {
|
||||
t.Errorf("non-nil stats on non exist container")
|
||||
}
|
||||
|
@@ -23,14 +23,15 @@ import (
|
||||
|
||||
// Labels allows you to present labels independently from their storage.
|
||||
type Labels interface {
|
||||
// Get returns the value for the provided label.
|
||||
Get(label string) (value string)
|
||||
}
|
||||
|
||||
// A map of label:value. Implements Labels.
|
||||
// Set is a map of label:value. It implements Labels.
|
||||
type Set map[string]string
|
||||
|
||||
// All labels listed as a human readable string. Conveniently, exactly the format
|
||||
// that ParseSelector takes.
|
||||
// String returns all labels listed as a human readable string.
|
||||
// Conveniently, exactly the format that ParseSelector takes.
|
||||
func (ls Set) String() string {
|
||||
selector := make([]string, 0, len(ls))
|
||||
for key, value := range ls {
|
||||
@@ -41,12 +42,12 @@ func (ls Set) String() string {
|
||||
return strings.Join(selector, ",")
|
||||
}
|
||||
|
||||
// Implement Labels interface.
|
||||
// Get returns the value in the map for the provided label.
|
||||
func (ls Set) Get(label string) string {
|
||||
return ls[label]
|
||||
}
|
||||
|
||||
// Convenience function: convert these labels to a selector.
|
||||
// AsSelector converts labels into a selectors.
|
||||
func (ls Set) AsSelector() Selector {
|
||||
return SelectorFromSet(ls)
|
||||
}
|
||||
|
@@ -22,12 +22,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Represents a selector.
|
||||
// Selector represents a label selector.
|
||||
type Selector interface {
|
||||
// Returns true if this selector matches the given set of labels.
|
||||
// Matches returns true if this selector matches the given set of labels.
|
||||
Matches(Labels) bool
|
||||
|
||||
// Prints a human readable version of this selector.
|
||||
// String returns a human readable string that represents this selector.
|
||||
String() string
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func try(selectorPiece, op string) (lhs, rhs string, ok bool) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Given a Set, return a Selector which will match exactly that Set.
|
||||
// SelectorFromSet returns a Selector which will match exactly the given Set.
|
||||
func SelectorFromSet(ls Set) Selector {
|
||||
items := make([]Selector, 0, len(ls))
|
||||
for label, value := range ls {
|
||||
@@ -99,7 +99,8 @@ func SelectorFromSet(ls Set) Selector {
|
||||
return andTerm(items)
|
||||
}
|
||||
|
||||
// Takes a string repsenting a selector and returns an object suitable for matching, or an error.
|
||||
// ParseSelector takes a string repsenting a selector and returns an
|
||||
// object suitable for matching, or an error.
|
||||
func ParseSelector(selector string) (Selector, error) {
|
||||
parts := strings.Split(selector, ",")
|
||||
sort.StringSlice(parts).Sort()
|
||||
|
@@ -43,7 +43,7 @@ type Master struct {
|
||||
storage map[string]apiserver.RESTStorage
|
||||
}
|
||||
|
||||
// Returns a memory (not etcd) backed apiserver.
|
||||
// NewMemoryServer returns a new instance of Master backed with memory (not etcd).
|
||||
func NewMemoryServer(minions []string, podInfoGetter client.PodInfoGetter, cloud cloudprovider.Interface) *Master {
|
||||
m := &Master{
|
||||
podRegistry: registry.MakeMemoryRegistry(),
|
||||
@@ -55,7 +55,7 @@ func NewMemoryServer(minions []string, podInfoGetter client.PodInfoGetter, cloud
|
||||
return m
|
||||
}
|
||||
|
||||
// Returns a new apiserver.
|
||||
// New returns a new instance of Master connected to the given etcdServer.
|
||||
func New(etcdServers, minions []string, podInfoGetter client.PodInfoGetter, cloud cloudprovider.Interface, minionRegexp string) *Master {
|
||||
etcdClient := etcd.NewClient(etcdServers)
|
||||
minionRegistry := minionRegistryMaker(minions, cloud, minionRegexp)
|
||||
@@ -84,7 +84,7 @@ func (m *Master) init(cloud cloudprovider.Interface, podInfoGetter client.PodInf
|
||||
m.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
podCache := NewPodCache(podInfoGetter, m.podRegistry, time.Second*30)
|
||||
go podCache.Loop()
|
||||
s := scheduler.MakeRandomFitScheduler(m.podRegistry, m.random)
|
||||
s := scheduler.NewRandomFitScheduler(m.podRegistry, m.random)
|
||||
m.storage = map[string]apiserver.RESTStorage{
|
||||
"pods": registry.MakePodRegistryStorage(m.podRegistry, podInfoGetter, s, m.minionRegistry, cloud, podCache),
|
||||
"replicationControllers": registry.NewControllerRegistryStorage(m.controllerRegistry, m.podRegistry),
|
||||
@@ -94,7 +94,7 @@ func (m *Master) init(cloud cloudprovider.Interface, podInfoGetter client.PodInf
|
||||
|
||||
}
|
||||
|
||||
// Runs master. Never returns.
|
||||
// Run begins serving the Kubernetes API. It never returns.
|
||||
func (m *Master) Run(myAddress, apiPrefix string) error {
|
||||
endpoints := registry.MakeEndpointController(m.serviceRegistry, m.podRegistry)
|
||||
go util.Forever(func() { endpoints.SyncServiceEndpoints() }, time.Second*10)
|
||||
@@ -109,8 +109,9 @@ func (m *Master) Run(myAddress, apiPrefix string) error {
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
||||
// Instead of calling Run, call ConstructHandler to get a handler for your own
|
||||
// server. Intended for testing. Only call once.
|
||||
// ConstructHandler returns an http.Handler which serves the Kubernetes API.
|
||||
// Instead of calling Run, you can call this function to get a handler for your own server.
|
||||
// It is intended for testing. Only call once.
|
||||
func (m *Master) ConstructHandler(apiPrefix string) http.Handler {
|
||||
endpoints := registry.MakeEndpointController(m.serviceRegistry, m.podRegistry)
|
||||
go util.Forever(func() { endpoints.SyncServiceEndpoints() }, time.Second*10)
|
||||
|
@@ -40,6 +40,7 @@ type PodCache struct {
|
||||
podLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewPodCache returns a new PodCache which watches container information registered in the given PodRegistry.
|
||||
func NewPodCache(info client.PodInfoGetter, pods registry.PodRegistry, period time.Duration) *PodCache {
|
||||
return &PodCache{
|
||||
containerInfo: info,
|
||||
@@ -49,7 +50,7 @@ func NewPodCache(info client.PodInfoGetter, pods registry.PodRegistry, period ti
|
||||
}
|
||||
}
|
||||
|
||||
// Implements the PodInfoGetter interface.
|
||||
// GetPodInfo Implements the PodInfoGetter.GetPodInfo.
|
||||
// The returned value should be treated as read-only.
|
||||
func (p *PodCache) GetPodInfo(host, podID string) (api.PodInfo, error) {
|
||||
p.podLock.Lock()
|
||||
@@ -57,9 +58,8 @@ func (p *PodCache) GetPodInfo(host, podID string) (api.PodInfo, error) {
|
||||
value, ok := p.podInfo[podID]
|
||||
if !ok {
|
||||
return nil, errors.New("no cached pod info")
|
||||
} else {
|
||||
return value, nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (p *PodCache) updatePodInfo(host, id string) error {
|
||||
@@ -73,7 +73,7 @@ func (p *PodCache) updatePodInfo(host, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update information about all containers. Either called by Loop() below, or one-off.
|
||||
// UpdateAllContainers updates information about all containers. Either called by Loop() below, or one-off.
|
||||
func (p *PodCache) UpdateAllContainers() {
|
||||
pods, err := p.pods.ListPods(labels.Everything())
|
||||
if err != nil {
|
||||
@@ -88,7 +88,8 @@ func (p *PodCache) UpdateAllContainers() {
|
||||
}
|
||||
}
|
||||
|
||||
// Loop runs forever, it is expected to be placed in a go routine.
|
||||
// Loop begins watching updates of container information.
|
||||
// It runs forever, and is expected to be placed in a go routine.
|
||||
func (p *PodCache) Loop() {
|
||||
util.Forever(func() { p.UpdateAllContainers() }, p.period)
|
||||
}
|
||||
|
@@ -24,40 +24,44 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Operation is a type of operation of services or endpoints.
|
||||
type Operation int
|
||||
|
||||
// These are the available operation types.
|
||||
const (
|
||||
SET Operation = iota
|
||||
ADD
|
||||
REMOVE
|
||||
)
|
||||
|
||||
// Defines an operation sent on the channel. You can add or remove single services by
|
||||
// sending an array of size one and Op == ADD|REMOVE. For setting the state of the system
|
||||
// to a given state for this source configuration, set Services as desired and Op to SET,
|
||||
// which will reset the system state to that specified in this operation for this source
|
||||
// channel. To remove all services, set Services to empty array and Op to SET
|
||||
// ServiceUpdate describes an operation of services, sent on the channel.
|
||||
// You can add or remove single services by sending an array of size one and Op == ADD|REMOVE.
|
||||
// For setting the state of the system to a given state for this source configuration, set Services as desired and Op to SET,
|
||||
// which will reset the system state to that specified in this operation for this source channel.
|
||||
// To remove all services, set Services to empty array and Op to SET
|
||||
type ServiceUpdate struct {
|
||||
Services []api.Service
|
||||
Op Operation
|
||||
}
|
||||
|
||||
// Defines an operation sent on the channel. You can add or remove single endpoints by
|
||||
// sending an array of size one and Op == ADD|REMOVE. For setting the state of the system
|
||||
// to a given state for this source configuration, set Endpoints as desired and Op to SET,
|
||||
// which will reset the system state to that specified in this operation for this source
|
||||
// channel. To remove all endpoints, set Endpoints to empty array and Op to SET
|
||||
// EndpointsUpdate describes an operation of endpoints, sent on the channel.
|
||||
// You can add or remove single endpoints by sending an array of size one and Op == ADD|REMOVE.
|
||||
// For setting the state of the system to a given state for this source configuration, set Endpoints as desired and Op to SET,
|
||||
// which will reset the system state to that specified in this operation for this source channel.
|
||||
// To remove all endpoints, set Endpoints to empty array and Op to SET
|
||||
type EndpointsUpdate struct {
|
||||
Endpoints []api.Endpoints
|
||||
Op Operation
|
||||
}
|
||||
|
||||
// ServiceConfigHandler is an abstract interface of objects which receive update notifications for the set of services.
|
||||
type ServiceConfigHandler interface {
|
||||
// Sent when a configuration has been changed by one of the sources. This is the
|
||||
// union of all the configuration sources.
|
||||
// OnUpdate gets called when a configuration has been changed by one of the sources.
|
||||
// This is the union of all the configuration sources.
|
||||
OnUpdate(services []api.Service)
|
||||
}
|
||||
|
||||
// EndpointsConfigHandler is an abstract interface of objects which receive update notifications for the set of endpoints.
|
||||
type EndpointsConfigHandler interface {
|
||||
// OnUpdate gets called when endpoints configuration is changed for a given
|
||||
// service on any of the configuration sources. An example is when a new
|
||||
@@ -65,6 +69,8 @@ type EndpointsConfigHandler interface {
|
||||
OnUpdate(endpoints []api.Endpoints)
|
||||
}
|
||||
|
||||
// ServiceConfig tracks a set of service configurations and their endpoint configurations.
|
||||
// It accepts "set", "add" and "remove" operations of services and endpoints via channels, and invokes registered handlers on change.
|
||||
type ServiceConfig struct {
|
||||
// Configuration sources and their lock.
|
||||
configSourceLock sync.RWMutex
|
||||
@@ -94,6 +100,8 @@ type ServiceConfig struct {
|
||||
endpointsNotifyChannel chan string
|
||||
}
|
||||
|
||||
// NewServiceConfig creates a new ServiceConfig.
|
||||
// It immediately runs the created ServiceConfig.
|
||||
func NewServiceConfig() *ServiceConfig {
|
||||
config := &ServiceConfig{
|
||||
serviceConfigSources: make(map[string]chan ServiceUpdate),
|
||||
@@ -109,22 +117,26 @@ func NewServiceConfig() *ServiceConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
// Run begins a loop to accept new service configurations and new endpoint configurations.
|
||||
// It never returns.
|
||||
func (impl *ServiceConfig) Run() {
|
||||
glog.Infof("Starting the config Run loop")
|
||||
for {
|
||||
select {
|
||||
case source := <-impl.serviceNotifyChannel:
|
||||
glog.Infof("Got new service configuration from source %s", source)
|
||||
impl.NotifyServiceUpdate()
|
||||
impl.notifyServiceUpdate()
|
||||
case source := <-impl.endpointsNotifyChannel:
|
||||
glog.Infof("Got new endpoint configuration from source %s", source)
|
||||
impl.NotifyEndpointsUpdate()
|
||||
impl.notifyEndpointsUpdate()
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (impl *ServiceConfig) ServiceChannelListener(source string, listenChannel chan ServiceUpdate) {
|
||||
// serviceChannelListener begins a loop to handle incoming ServiceUpdate notifications from the channel.
|
||||
// It never returns.
|
||||
func (impl *ServiceConfig) serviceChannelListener(source string, listenChannel chan ServiceUpdate) {
|
||||
// Represents the current services configuration for this channel.
|
||||
serviceMap := make(map[string]api.Service)
|
||||
for {
|
||||
@@ -160,7 +172,9 @@ func (impl *ServiceConfig) ServiceChannelListener(source string, listenChannel c
|
||||
}
|
||||
}
|
||||
|
||||
func (impl *ServiceConfig) EndpointsChannelListener(source string, listenChannel chan EndpointsUpdate) {
|
||||
// endpointsChannelListener begins a loop to handle incoming EndpointsUpdate notifications from the channel.
|
||||
// It never returns.
|
||||
func (impl *ServiceConfig) endpointsChannelListener(source string, listenChannel chan EndpointsUpdate) {
|
||||
endpointMap := make(map[string]api.Endpoints)
|
||||
for {
|
||||
select {
|
||||
@@ -214,11 +228,11 @@ func (impl *ServiceConfig) GetServiceConfigurationChannel(source string) chan Se
|
||||
}
|
||||
newChannel := make(chan ServiceUpdate)
|
||||
impl.serviceConfigSources[source] = newChannel
|
||||
go impl.ServiceChannelListener(source, newChannel)
|
||||
go impl.serviceChannelListener(source, newChannel)
|
||||
return newChannel
|
||||
}
|
||||
|
||||
// GetEndpointConfigurationChannel returns a channel where a configuration source
|
||||
// GetEndpointsConfigurationChannel returns a channel where a configuration source
|
||||
// can send updates of new endpoint configurations. Multiple calls with the same
|
||||
// source will return the same channel. This allows change and state based sources
|
||||
// to use the same channel. Difference source names however will be treated as a
|
||||
@@ -235,11 +249,11 @@ func (impl *ServiceConfig) GetEndpointsConfigurationChannel(source string) chan
|
||||
}
|
||||
newChannel := make(chan EndpointsUpdate)
|
||||
impl.endpointsConfigSources[source] = newChannel
|
||||
go impl.EndpointsChannelListener(source, newChannel)
|
||||
go impl.endpointsChannelListener(source, newChannel)
|
||||
return newChannel
|
||||
}
|
||||
|
||||
// Register ServiceConfigHandler to receive updates of changes to services.
|
||||
// RegisterServiceHandler registers the ServiceConfigHandler to receive updates of changes to services.
|
||||
func (impl *ServiceConfig) RegisterServiceHandler(handler ServiceConfigHandler) {
|
||||
impl.handlerLock.Lock()
|
||||
defer impl.handlerLock.Unlock()
|
||||
@@ -255,7 +269,7 @@ func (impl *ServiceConfig) RegisterServiceHandler(handler ServiceConfigHandler)
|
||||
panic("Only up to 10 service handlers supported for now")
|
||||
}
|
||||
|
||||
// Register ServiceConfigHandler to receive updates of changes to services.
|
||||
// RegisterEndpointsHandler registers the EndpointsConfigHandler to receive updates of changes to services.
|
||||
func (impl *ServiceConfig) RegisterEndpointsHandler(handler EndpointsConfigHandler) {
|
||||
impl.handlerLock.Lock()
|
||||
defer impl.handlerLock.Unlock()
|
||||
@@ -271,8 +285,9 @@ func (impl *ServiceConfig) RegisterEndpointsHandler(handler EndpointsConfigHandl
|
||||
panic("Only up to 10 endpoint handlers supported for now")
|
||||
}
|
||||
|
||||
func (impl *ServiceConfig) NotifyServiceUpdate() {
|
||||
services := make([]api.Service, 0)
|
||||
// notifyServiceUpdate calls the registered ServiceConfigHandlers with the current states of services.
|
||||
func (impl *ServiceConfig) notifyServiceUpdate() {
|
||||
services := []api.Service{}
|
||||
impl.configLock.RLock()
|
||||
for _, sourceServices := range impl.serviceConfig {
|
||||
for _, value := range sourceServices {
|
||||
@@ -291,8 +306,9 @@ func (impl *ServiceConfig) NotifyServiceUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
func (impl *ServiceConfig) NotifyEndpointsUpdate() {
|
||||
endpoints := make([]api.Endpoints, 0)
|
||||
// notifyEndpointsUpdate calls the registered EndpointsConfigHandlers with the current states of endpoints.
|
||||
func (impl *ServiceConfig) notifyEndpointsUpdate() {
|
||||
endpoints := []api.Endpoints{}
|
||||
impl.configLock.RLock()
|
||||
for _, sourceEndpoints := range impl.endpointConfig {
|
||||
for _, value := range sourceEndpoints {
|
||||
|
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// Watches etcd and gets the full configuration on preset intervals.
|
||||
// Expects the list of exposed services to live under:
|
||||
// It expects the list of exposed services to live under:
|
||||
// registry/services
|
||||
// which in etcd is exposed like so:
|
||||
// http://<etcd server>/v2/keys/registry/services
|
||||
@@ -30,7 +30,7 @@ limitations under the License.
|
||||
// '[ { "machine": <host>, "name": <name", "port": <port> },
|
||||
// { "machine": <host2>, "name": <name2", "port": <port2> }
|
||||
// ]',
|
||||
//
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -44,14 +44,17 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
const RegistryRoot = "registry/services"
|
||||
// registryRoot is the key prefix for service configs in etcd.
|
||||
const registryRoot = "registry/services"
|
||||
|
||||
// ConfigSourceEtcd communicates with a etcd via the client, and sends the change notification of services and endpoints to the specified channels.
|
||||
type ConfigSourceEtcd struct {
|
||||
client *etcd.Client
|
||||
serviceChannel chan ServiceUpdate
|
||||
endpointsChannel chan EndpointsUpdate
|
||||
}
|
||||
|
||||
// NewConfigSourceEtcd creates a new ConfigSourceEtcd and immediately runs the created ConfigSourceEtcd in a goroutine.
|
||||
func NewConfigSourceEtcd(client *etcd.Client, serviceChannel chan ServiceUpdate, endpointsChannel chan EndpointsUpdate) ConfigSourceEtcd {
|
||||
config := ConfigSourceEtcd{
|
||||
client: client,
|
||||
@@ -62,13 +65,14 @@ func NewConfigSourceEtcd(client *etcd.Client, serviceChannel chan ServiceUpdate,
|
||||
return config
|
||||
}
|
||||
|
||||
// Run begins watching for new services and their endpoints on etcd.
|
||||
func (impl ConfigSourceEtcd) Run() {
|
||||
// Initially, just wait for the etcd to come up before doing anything more complicated.
|
||||
var services []api.Service
|
||||
var endpoints []api.Endpoints
|
||||
var err error
|
||||
for {
|
||||
services, endpoints, err = impl.GetServices()
|
||||
services, endpoints, err = impl.getServices()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -87,10 +91,10 @@ func (impl ConfigSourceEtcd) Run() {
|
||||
|
||||
// Ok, so we got something back from etcd. Let's set up a watch for new services, and
|
||||
// their endpoints
|
||||
go impl.WatchForChanges()
|
||||
go impl.watchForChanges()
|
||||
|
||||
for {
|
||||
services, endpoints, err = impl.GetServices()
|
||||
services, endpoints, err = impl.getServices()
|
||||
if err != nil {
|
||||
glog.Errorf("ConfigSourceEtcd: Failed to get services: %v", err)
|
||||
} else {
|
||||
@@ -107,12 +111,12 @@ func (impl ConfigSourceEtcd) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the list of services and their endpoints from etcd.
|
||||
// getServices finds the list of services and their endpoints from etcd.
|
||||
// This operation is akin to a set a known good at regular intervals.
|
||||
func (impl ConfigSourceEtcd) GetServices() ([]api.Service, []api.Endpoints, error) {
|
||||
response, err := impl.client.Get(RegistryRoot+"/specs", true, false)
|
||||
func (impl ConfigSourceEtcd) getServices() ([]api.Service, []api.Endpoints, error) {
|
||||
response, err := impl.client.Get(registryRoot+"/specs", true, false)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get the key %s: %v", RegistryRoot, err)
|
||||
glog.Errorf("Failed to get the key %s: %v", registryRoot, err)
|
||||
return make([]api.Service, 0), make([]api.Endpoints, 0), err
|
||||
}
|
||||
if response.Node.Dir == true {
|
||||
@@ -129,7 +133,7 @@ func (impl ConfigSourceEtcd) GetServices() ([]api.Service, []api.Endpoints, erro
|
||||
continue
|
||||
}
|
||||
retServices[i] = svc
|
||||
endpoints, err := impl.GetEndpoints(svc.ID)
|
||||
endpoints, err := impl.getEndpoints(svc.ID)
|
||||
if err != nil {
|
||||
glog.Errorf("Couldn't get endpoints for %s : %v skipping", svc.ID, err)
|
||||
}
|
||||
@@ -138,23 +142,23 @@ func (impl ConfigSourceEtcd) GetServices() ([]api.Service, []api.Endpoints, erro
|
||||
}
|
||||
return retServices, retEndpoints, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("did not get the root of the registry %s", RegistryRoot)
|
||||
return nil, nil, fmt.Errorf("did not get the root of the registry %s", registryRoot)
|
||||
}
|
||||
|
||||
func (impl ConfigSourceEtcd) GetEndpoints(service string) (api.Endpoints, error) {
|
||||
key := fmt.Sprintf(RegistryRoot + "/endpoints/" + service)
|
||||
// getEndpoints finds the list of endpoints of the service from etcd.
|
||||
func (impl ConfigSourceEtcd) getEndpoints(service string) (api.Endpoints, error) {
|
||||
key := fmt.Sprintf(registryRoot + "/endpoints/" + service)
|
||||
response, err := impl.client.Get(key, true, false)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get the key: %s %v", key, err)
|
||||
return api.Endpoints{}, err
|
||||
}
|
||||
// Parse all the endpoint specifications in this value.
|
||||
return ParseEndpoints(response.Node.Value)
|
||||
return parseEndpoints(response.Node.Value)
|
||||
}
|
||||
|
||||
// EtcdResponseToServiceAndLocalport takes an etcd response and pulls it apart to find
|
||||
// service
|
||||
func EtcdResponseToService(response *etcd.Response) (*api.Service, error) {
|
||||
// etcdResponseToService takes an etcd response and pulls it apart to find service.
|
||||
func etcdResponseToService(response *etcd.Response) (*api.Service, error) {
|
||||
if response.Node == nil {
|
||||
return nil, fmt.Errorf("invalid response from etcd: %#v", response)
|
||||
}
|
||||
@@ -166,31 +170,31 @@ func EtcdResponseToService(response *etcd.Response) (*api.Service, error) {
|
||||
return &svc, err
|
||||
}
|
||||
|
||||
func ParseEndpoints(jsonString string) (api.Endpoints, error) {
|
||||
func parseEndpoints(jsonString string) (api.Endpoints, error) {
|
||||
var e api.Endpoints
|
||||
err := json.Unmarshal([]byte(jsonString), &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (impl ConfigSourceEtcd) WatchForChanges() {
|
||||
func (impl ConfigSourceEtcd) watchForChanges() {
|
||||
glog.Info("Setting up a watch for new services")
|
||||
watchChannel := make(chan *etcd.Response)
|
||||
go impl.client.Watch("/registry/services/", 0, true, watchChannel, nil)
|
||||
for {
|
||||
watchResponse := <-watchChannel
|
||||
impl.ProcessChange(watchResponse)
|
||||
impl.processChange(watchResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func (impl ConfigSourceEtcd) ProcessChange(response *etcd.Response) {
|
||||
func (impl ConfigSourceEtcd) processChange(response *etcd.Response) {
|
||||
glog.Infof("Processing a change in service configuration... %s", *response)
|
||||
|
||||
// If it's a new service being added (signified by a localport being added)
|
||||
// then process it as such
|
||||
if strings.Contains(response.Node.Key, "/endpoints/") {
|
||||
impl.ProcessEndpointResponse(response)
|
||||
impl.processEndpointResponse(response)
|
||||
} else if response.Action == "set" {
|
||||
service, err := EtcdResponseToService(response)
|
||||
service, err := etcdResponseToService(response)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to parse %s Port: %s", response, err)
|
||||
return
|
||||
@@ -208,13 +212,12 @@ func (impl ConfigSourceEtcd) ProcessChange(response *etcd.Response) {
|
||||
serviceUpdate := ServiceUpdate{Op: REMOVE, Services: []api.Service{{JSONBase: api.JSONBase{ID: parts[3]}}}}
|
||||
impl.serviceChannel <- serviceUpdate
|
||||
return
|
||||
} else {
|
||||
glog.Infof("Unknown service delete: %#v", parts)
|
||||
}
|
||||
glog.Infof("Unknown service delete: %#v", parts)
|
||||
}
|
||||
}
|
||||
|
||||
func (impl ConfigSourceEtcd) ProcessEndpointResponse(response *etcd.Response) {
|
||||
func (impl ConfigSourceEtcd) processEndpointResponse(response *etcd.Response) {
|
||||
glog.Infof("Processing a change in endpoint configuration... %s", *response)
|
||||
var endpoints api.Endpoints
|
||||
err := json.Unmarshal([]byte(response.Node.Value), &endpoints)
|
||||
|
@@ -26,15 +26,15 @@ import (
|
||||
|
||||
const TomcatContainerEtcdKey = "/registry/services/tomcat/endpoints/tomcat-3bd5af34"
|
||||
const TomcatService = "tomcat"
|
||||
const TomcatContainerId = "tomcat-3bd5af34"
|
||||
const TomcatContainerID = "tomcat-3bd5af34"
|
||||
|
||||
func ValidateJsonParsing(t *testing.T, jsonString string, expectedEndpoints api.Endpoints, expectError bool) {
|
||||
endpoints, err := ParseEndpoints(jsonString)
|
||||
func validateJSONParsing(t *testing.T, jsonString string, expectedEndpoints api.Endpoints, expectError bool) {
|
||||
endpoints, err := parseEndpoints(jsonString)
|
||||
if err == nil && expectError {
|
||||
t.Errorf("ValidateJsonParsing did not get expected error when parsing %s", jsonString)
|
||||
t.Errorf("validateJSONParsing did not get expected error when parsing %s", jsonString)
|
||||
}
|
||||
if err != nil && !expectError {
|
||||
t.Errorf("ValidateJsonParsing got unexpected error %+v when parsing %s", err, jsonString)
|
||||
t.Errorf("validateJSONParsing got unexpected error %+v when parsing %s", err, jsonString)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedEndpoints, endpoints) {
|
||||
t.Errorf("Didn't get expected endpoints %+v got: %+v", expectedEndpoints, endpoints)
|
||||
@@ -42,7 +42,7 @@ func ValidateJsonParsing(t *testing.T, jsonString string, expectedEndpoints api.
|
||||
}
|
||||
|
||||
func TestParseJsonEndpoints(t *testing.T) {
|
||||
ValidateJsonParsing(t, "", api.Endpoints{}, true)
|
||||
validateJSONParsing(t, "", api.Endpoints{}, true)
|
||||
endpoints := api.Endpoints{
|
||||
Name: "foo",
|
||||
Endpoints: []string{"foo", "bar", "baz"},
|
||||
@@ -51,6 +51,6 @@ func TestParseJsonEndpoints(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %#v", err)
|
||||
}
|
||||
ValidateJsonParsing(t, string(data), endpoints, false)
|
||||
// ValidateJsonParsing(t, "[{\"port\":8000,\"name\":\"mysql\",\"machine\":\"foo\"},{\"port\":9000,\"name\":\"mysql\",\"machine\":\"bar\"}]", []string{"foo:8000", "bar:9000"}, false)
|
||||
validateJSONParsing(t, string(data), endpoints, false)
|
||||
// validateJSONParsing(t, "[{\"port\":8000,\"name\":\"mysql\",\"machine\":\"foo\"},{\"port\":9000,\"name\":\"mysql\",\"machine\":\"bar\"}]", []string{"foo:8000", "bar:9000"}, false)
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ limitations under the License.
|
||||
// }
|
||||
//]
|
||||
//}
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -41,22 +42,23 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// TODO: kill this struct.
|
||||
type ServiceJSON struct {
|
||||
Name string
|
||||
Port int
|
||||
Endpoints []string
|
||||
}
|
||||
type ConfigFile struct {
|
||||
Services []ServiceJSON
|
||||
// serviceConfig is a deserialized form of the config file format which ConfigSourceFile accepts.
|
||||
type serviceConfig struct {
|
||||
Services []struct {
|
||||
Name string `json: "name"`
|
||||
Port int `json: "port"`
|
||||
Endpoints []string `json: "endpoints"`
|
||||
} `json: "service"`
|
||||
}
|
||||
|
||||
// ConfigSourceFile periodically reads service configurations in JSON from a file, and sends the services and endpoints defined in the file to the specified channels.
|
||||
type ConfigSourceFile struct {
|
||||
serviceChannel chan ServiceUpdate
|
||||
endpointsChannel chan EndpointsUpdate
|
||||
filename string
|
||||
}
|
||||
|
||||
// NewConfigSourceFile creates a new ConfigSourceFile and let it immediately runs the created ConfigSourceFile in a goroutine.
|
||||
func NewConfigSourceFile(filename string, serviceChannel chan ServiceUpdate, endpointsChannel chan EndpointsUpdate) ConfigSourceFile {
|
||||
config := ConfigSourceFile{
|
||||
filename: filename,
|
||||
@@ -67,6 +69,7 @@ func NewConfigSourceFile(filename string, serviceChannel chan ServiceUpdate, end
|
||||
return config
|
||||
}
|
||||
|
||||
// Run begins watching the config file.
|
||||
func (impl ConfigSourceFile) Run() {
|
||||
glog.Infof("Watching file %s", impl.filename)
|
||||
var lastData []byte
|
||||
@@ -85,7 +88,7 @@ func (impl ConfigSourceFile) Run() {
|
||||
}
|
||||
lastData = data
|
||||
|
||||
config := new(ConfigFile)
|
||||
config := &serviceConfig{}
|
||||
if err = json.Unmarshal(data, config); err != nil {
|
||||
glog.Errorf("Couldn't unmarshal configuration from file : %s %v", data, err)
|
||||
continue
|
||||
|
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package proxy implements the layer-3 network proxy
|
||||
// Package proxy implements the layer-3 network proxy.
|
||||
package proxy
|
||||
|
@@ -22,6 +22,8 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// LoadBalancer represents a load balancer that decides where to route
|
||||
// the incoming services for a particular service to.
|
||||
type LoadBalancer interface {
|
||||
// LoadBalance takes an incoming request and figures out where to route it to.
|
||||
// Determination is based on destination service (for example, 'mysql') as
|
||||
|
@@ -33,11 +33,12 @@ type Proxier struct {
|
||||
serviceMap map[string]int
|
||||
}
|
||||
|
||||
// NewProxier returns a newly created and correctly initialized instance of Proxier.
|
||||
func NewProxier(loadBalancer LoadBalancer) *Proxier {
|
||||
return &Proxier{loadBalancer: loadBalancer, serviceMap: make(map[string]int)}
|
||||
}
|
||||
|
||||
func CopyBytes(in, out *net.TCPConn) {
|
||||
func copyBytes(in, out *net.TCPConn) {
|
||||
glog.Infof("Copying from %v <-> %v <-> %v <-> %v",
|
||||
in.RemoteAddr(), in.LocalAddr(), out.LocalAddr(), out.RemoteAddr())
|
||||
_, err := io.Copy(in, out)
|
||||
@@ -49,14 +50,17 @@ func CopyBytes(in, out *net.TCPConn) {
|
||||
out.CloseWrite()
|
||||
}
|
||||
|
||||
// Create a bidirectional byte shuffler. Copies bytes to/from each connection.
|
||||
func ProxyConnection(in, out *net.TCPConn) {
|
||||
// proxyConnection creates a bidirectional byte shuffler.
|
||||
// It copies bytes to/from each connection.
|
||||
func proxyConnection(in, out *net.TCPConn) {
|
||||
glog.Infof("Creating proxy between %v <-> %v <-> %v <-> %v",
|
||||
in.RemoteAddr(), in.LocalAddr(), out.LocalAddr(), out.RemoteAddr())
|
||||
go CopyBytes(in, out)
|
||||
go CopyBytes(out, in)
|
||||
go copyBytes(in, out)
|
||||
go copyBytes(out, in)
|
||||
}
|
||||
|
||||
// AcceptHandler begins accepting incoming connections from listener and proxying the connections to the load-balanced endpoints.
|
||||
// It never returns.
|
||||
func (proxier Proxier) AcceptHandler(service string, listener net.Listener) {
|
||||
for {
|
||||
inConn, err := listener.Accept()
|
||||
@@ -83,12 +87,12 @@ func (proxier Proxier) AcceptHandler(service string, listener net.Listener) {
|
||||
inConn.Close()
|
||||
continue
|
||||
}
|
||||
ProxyConnection(inConn.(*net.TCPConn), outConn.(*net.TCPConn))
|
||||
proxyConnection(inConn.(*net.TCPConn), outConn.(*net.TCPConn))
|
||||
}
|
||||
}
|
||||
|
||||
// AddService starts listening for a new service on a given port.
|
||||
func (proxier Proxier) AddService(service string, port int) error {
|
||||
// addService starts listening for a new service on a given port.
|
||||
func (proxier Proxier) addService(service string, port int) error {
|
||||
// Make sure we can start listening on the port before saying all's well.
|
||||
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
@@ -117,18 +121,21 @@ func (proxier Proxier) addServiceCommon(service string, l net.Listener) {
|
||||
go proxier.AcceptHandler(service, l)
|
||||
}
|
||||
|
||||
// OnUpdate recieves update notices for the updated services and start listening newly added services.
|
||||
// It implements "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config".ServiceConfigHandler.OnUpdate.
|
||||
func (proxier Proxier) OnUpdate(services []api.Service) {
|
||||
glog.Infof("Received update notice: %+v", services)
|
||||
for _, service := range services {
|
||||
port, exists := proxier.serviceMap[service.ID]
|
||||
if !exists || port != service.Port {
|
||||
glog.Infof("Adding a new service %s on port %d", service.ID, service.Port)
|
||||
err := proxier.AddService(service.ID, service.Port)
|
||||
if err == nil {
|
||||
proxier.serviceMap[service.ID] = service.Port
|
||||
} else {
|
||||
glog.Infof("Failed to start listening for %s on %d", service.ID, service.Port)
|
||||
}
|
||||
if exists && port == service.Port {
|
||||
continue
|
||||
}
|
||||
glog.Infof("Adding a new service %s on port %d", service.ID, service.Port)
|
||||
err := proxier.addService(service.ID, service.Port)
|
||||
if err != nil {
|
||||
glog.Infof("Failed to start listening for %s on %d", service.ID, service.Port)
|
||||
continue
|
||||
}
|
||||
proxier.serviceMap[service.ID] = service.Port
|
||||
}
|
||||
}
|
||||
|
@@ -29,22 +29,25 @@ import (
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// LoadBalancerRR is a round-robin load balancer. It implements LoadBalancer.
|
||||
type LoadBalancerRR struct {
|
||||
lock sync.RWMutex
|
||||
endpointsMap map[string][]string
|
||||
rrIndex map[string]int
|
||||
}
|
||||
|
||||
// NewLoadBalancerRR returns a newly created and correctly initialized instance of LoadBalancerRR.
|
||||
func NewLoadBalancerRR() *LoadBalancerRR {
|
||||
return &LoadBalancerRR{endpointsMap: make(map[string][]string), rrIndex: make(map[string]int)}
|
||||
}
|
||||
|
||||
// LoadBalance selects an endpoint of the service by round-robin algorithm.
|
||||
func (impl LoadBalancerRR) LoadBalance(service string, srcAddr net.Addr) (string, error) {
|
||||
impl.lock.RLock()
|
||||
endpoints, exists := impl.endpointsMap[service]
|
||||
index := impl.rrIndex[service]
|
||||
impl.lock.RUnlock()
|
||||
if exists == false {
|
||||
if !exists {
|
||||
return "", errors.New("no service entry for:" + service)
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
@@ -55,7 +58,7 @@ func (impl LoadBalancerRR) LoadBalance(service string, srcAddr net.Addr) (string
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (impl LoadBalancerRR) IsValid(spec string) bool {
|
||||
func (impl LoadBalancerRR) isValid(spec string) bool {
|
||||
_, port, err := net.SplitHostPort(spec)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -67,16 +70,19 @@ func (impl LoadBalancerRR) IsValid(spec string) bool {
|
||||
return value > 0
|
||||
}
|
||||
|
||||
func (impl LoadBalancerRR) FilterValidEndpoints(endpoints []string) []string {
|
||||
func (impl LoadBalancerRR) filterValidEndpoints(endpoints []string) []string {
|
||||
var result []string
|
||||
for _, spec := range endpoints {
|
||||
if impl.IsValid(spec) {
|
||||
if impl.isValid(spec) {
|
||||
result = append(result, spec)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// OnUpdate updates the registered endpoints with the new
|
||||
// endpoint information, removes the registered endpoints
|
||||
// no longer present in the provided endpoints.
|
||||
func (impl LoadBalancerRR) OnUpdate(endpoints []api.Endpoints) {
|
||||
tmp := make(map[string]bool)
|
||||
impl.lock.Lock()
|
||||
@@ -84,7 +90,7 @@ func (impl LoadBalancerRR) OnUpdate(endpoints []api.Endpoints) {
|
||||
// First update / add all new endpoints for services.
|
||||
for _, value := range endpoints {
|
||||
existingEndpoints, exists := impl.endpointsMap[value.Name]
|
||||
validEndpoints := impl.FilterValidEndpoints(value.Endpoints)
|
||||
validEndpoints := impl.filterValidEndpoints(value.Endpoints)
|
||||
if !exists || !reflect.DeepEqual(existingEndpoints, validEndpoints) {
|
||||
glog.Infof("LoadBalancerRR: Setting endpoints for %s to %+v", value.Name, value.Endpoints)
|
||||
impl.endpointsMap[value.Name] = validEndpoints
|
||||
|
@@ -24,16 +24,16 @@ import (
|
||||
|
||||
func TestLoadBalanceValidateWorks(t *testing.T) {
|
||||
loadBalancer := NewLoadBalancerRR()
|
||||
if loadBalancer.IsValid("") {
|
||||
if loadBalancer.isValid("") {
|
||||
t.Errorf("Didn't fail for empty string")
|
||||
}
|
||||
if loadBalancer.IsValid("foobar") {
|
||||
if loadBalancer.isValid("foobar") {
|
||||
t.Errorf("Didn't fail with no port")
|
||||
}
|
||||
if loadBalancer.IsValid("foobar:-1") {
|
||||
if loadBalancer.isValid("foobar:-1") {
|
||||
t.Errorf("Didn't fail with a negative port")
|
||||
}
|
||||
if !loadBalancer.IsValid("foobar:8080") {
|
||||
if !loadBalancer.isValid("foobar:8080") {
|
||||
t.Errorf("Failed a valid config.")
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestLoadBalanceValidateWorks(t *testing.T) {
|
||||
func TestLoadBalanceFilterWorks(t *testing.T) {
|
||||
loadBalancer := NewLoadBalancerRR()
|
||||
endpoints := []string{"foobar:1", "foobar:2", "foobar:-1", "foobar:3", "foobar:-2"}
|
||||
filtered := loadBalancer.FilterValidEndpoints(endpoints)
|
||||
filtered := loadBalancer.filterValidEndpoints(endpoints)
|
||||
|
||||
if len(filtered) != 3 {
|
||||
t.Errorf("Failed to filter to the correct size")
|
||||
@@ -59,7 +59,7 @@ func TestLoadBalanceFilterWorks(t *testing.T) {
|
||||
|
||||
func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) {
|
||||
loadBalancer := NewLoadBalancerRR()
|
||||
endpoints := make([]api.Endpoints, 0)
|
||||
var endpoints []api.Endpoints
|
||||
loadBalancer.OnUpdate(endpoints)
|
||||
endpoint, err := loadBalancer.LoadBalance("foo", nil)
|
||||
if err == nil {
|
||||
|
@@ -53,9 +53,7 @@ func TestEtcdGetPodNotFound(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{
|
||||
ErrorCode: 100,
|
||||
},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
_, err := registry.GetPod("foo")
|
||||
@@ -70,7 +68,7 @@ func TestEtcdCreatePod(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{}), 0)
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
@@ -133,13 +131,13 @@ func TestEtcdCreatePodWithContainersError(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 200},
|
||||
E: tools.EtcdErrorValueRequired,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
err := registry.CreatePod("machine", api.Pod{
|
||||
@@ -154,7 +152,7 @@ func TestEtcdCreatePodWithContainersError(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Unexpected non-error")
|
||||
}
|
||||
if err != nil && err.(*etcd.EtcdError).ErrorCode != 100 {
|
||||
if !tools.IsEtcdNotFound(err) {
|
||||
t.Errorf("Unexpected error: %#v", err)
|
||||
}
|
||||
}
|
||||
@@ -165,13 +163,13 @@ func TestEtcdCreatePodWithContainersNotFound(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
fakeClient.Data["/registry/hosts/machine/kubelet"] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
err := registry.CreatePod("machine", api.Pod{
|
||||
@@ -213,7 +211,7 @@ func TestEtcdCreatePodWithExistingContainers(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]api.ContainerManifest{
|
||||
{
|
||||
@@ -329,7 +327,7 @@ func TestEtcdListPodsNotFound(t *testing.T) {
|
||||
key := "/registry/hosts/machine/pods"
|
||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
pods, err := registry.ListPods(labels.Everything())
|
||||
@@ -374,7 +372,7 @@ func TestEtcdListControllersNotFound(t *testing.T) {
|
||||
key := "/registry/controllers"
|
||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
controllers, err := registry.ListControllers()
|
||||
@@ -389,7 +387,7 @@ func TestEtcdListServicesNotFound(t *testing.T) {
|
||||
key := "/registry/services/specs"
|
||||
fakeClient.Data[key] = tools.EtcdResponseWithError{
|
||||
R: &etcd.Response{},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
services, err := registry.ListServices()
|
||||
@@ -442,9 +440,7 @@ func TestEtcdGetControllerNotFound(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{
|
||||
ErrorCode: 100,
|
||||
},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
ctrl, err := registry.GetController("foo")
|
||||
@@ -538,7 +534,7 @@ func TestEtcdCreateService(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
err := registry.CreateService(api.Service{
|
||||
@@ -572,9 +568,7 @@ func TestEtcdGetServiceNotFound(t *testing.T) {
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{
|
||||
ErrorCode: 100,
|
||||
},
|
||||
E: tools.EtcdErrorNotFound,
|
||||
}
|
||||
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
|
||||
_, err := registry.GetService("foo")
|
||||
|
86
pkg/registry/healthy_minion_registry.go
Normal file
86
pkg/registry/healthy_minion_registry.go
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/health"
|
||||
)
|
||||
|
||||
type HealthyMinionRegistry struct {
|
||||
delegate MinionRegistry
|
||||
client health.HTTPGetInterface
|
||||
port int
|
||||
}
|
||||
|
||||
func NewHealthyMinionRegistry(delegate MinionRegistry, client *http.Client) MinionRegistry {
|
||||
return &HealthyMinionRegistry{
|
||||
delegate: delegate,
|
||||
client: client,
|
||||
port: 10250,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthyMinionRegistry) makeMinionURL(minion string) string {
|
||||
return fmt.Sprintf("http://%s:%d/healthz", minion, h.port)
|
||||
}
|
||||
|
||||
func (h *HealthyMinionRegistry) List() (currentMinions []string, err error) {
|
||||
var result []string
|
||||
list, err := h.delegate.List()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, minion := range list {
|
||||
status, err := health.Check(h.makeMinionURL(minion), h.client)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if status == health.Healthy {
|
||||
result = append(result, minion)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *HealthyMinionRegistry) Insert(minion string) error {
|
||||
return h.delegate.Insert(minion)
|
||||
}
|
||||
|
||||
func (h *HealthyMinionRegistry) Delete(minion string) error {
|
||||
return h.delegate.Delete(minion)
|
||||
}
|
||||
|
||||
func (h *HealthyMinionRegistry) Contains(minion string) (bool, error) {
|
||||
contains, err := h.delegate.Contains(minion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !contains {
|
||||
return false, nil
|
||||
}
|
||||
status, err := health.Check(h.makeMinionURL(minion), h.client)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if status == health.Unhealthy {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
96
pkg/registry/healthy_minion_registry_test.go
Normal file
96
pkg/registry/healthy_minion_registry_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. 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 registry
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type alwaysYes struct{}
|
||||
|
||||
func (alwaysYes) Get(url string) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestBasicDelegation(t *testing.T) {
|
||||
mockMinionRegistry := MockMinionRegistry{
|
||||
minions: []string{"m1", "m2", "m3"},
|
||||
}
|
||||
healthy := HealthyMinionRegistry{
|
||||
delegate: &mockMinionRegistry,
|
||||
client: alwaysYes{},
|
||||
}
|
||||
|
||||
list, err := healthy.List()
|
||||
expectNoError(t, err)
|
||||
if !reflect.DeepEqual(list, mockMinionRegistry.minions) {
|
||||
t.Errorf("Expected %v, Got %v", mockMinionRegistry.minions, list)
|
||||
}
|
||||
err = healthy.Insert("foo")
|
||||
expectNoError(t, err)
|
||||
|
||||
ok, err := healthy.Contains("m1")
|
||||
expectNoError(t, err)
|
||||
if !ok {
|
||||
t.Errorf("Unexpected absence of 'm1'")
|
||||
}
|
||||
|
||||
ok, err = healthy.Contains("m5")
|
||||
expectNoError(t, err)
|
||||
if ok {
|
||||
t.Errorf("Unexpected presence of 'm5'")
|
||||
}
|
||||
}
|
||||
|
||||
type notMinion struct {
|
||||
minion string
|
||||
}
|
||||
|
||||
func (n *notMinion) Get(url string) (*http.Response, error) {
|
||||
if url != "http://"+n.minion+":10250/healthz" {
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
} else {
|
||||
return &http.Response{StatusCode: http.StatusInternalServerError}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestFiltering(t *testing.T) {
|
||||
mockMinionRegistry := MockMinionRegistry{
|
||||
minions: []string{"m1", "m2", "m3"},
|
||||
}
|
||||
healthy := HealthyMinionRegistry{
|
||||
delegate: &mockMinionRegistry,
|
||||
client: ¬Minion{minion: "m1"},
|
||||
port: 10250,
|
||||
}
|
||||
|
||||
expected := []string{"m2", "m3"}
|
||||
list, err := healthy.List()
|
||||
expectNoError(t, err)
|
||||
if !reflect.DeepEqual(list, expected) {
|
||||
t.Errorf("Expected %v, Got %v", expected, list)
|
||||
}
|
||||
ok, err := healthy.Contains("m1")
|
||||
expectNoError(t, err)
|
||||
if ok {
|
||||
t.Errorf("Unexpected presence of 'm1'")
|
||||
}
|
||||
}
|
@@ -75,3 +75,53 @@ func (registry *MockPodRegistry) DeletePod(podId string) error {
|
||||
defer registry.Unlock()
|
||||
return registry.err
|
||||
}
|
||||
|
||||
type MockMinionRegistry struct {
|
||||
err error
|
||||
minion string
|
||||
minions []string
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func MakeMockMinionRegistry(minions []string) *MockMinionRegistry {
|
||||
return &MockMinionRegistry{
|
||||
minions: minions,
|
||||
}
|
||||
}
|
||||
|
||||
func (registry *MockMinionRegistry) List() ([]string, error) {
|
||||
registry.Lock()
|
||||
defer registry.Unlock()
|
||||
return registry.minions, registry.err
|
||||
}
|
||||
|
||||
func (registry *MockMinionRegistry) Insert(minion string) error {
|
||||
registry.Lock()
|
||||
defer registry.Unlock()
|
||||
registry.minion = minion
|
||||
return registry.err
|
||||
}
|
||||
|
||||
func (registry *MockMinionRegistry) Contains(minion string) (bool, error) {
|
||||
registry.Lock()
|
||||
defer registry.Unlock()
|
||||
for _, name := range registry.minions {
|
||||
if name == minion {
|
||||
return true, registry.err
|
||||
}
|
||||
}
|
||||
return false, registry.err
|
||||
}
|
||||
|
||||
func (registry *MockMinionRegistry) Delete(minion string) error {
|
||||
registry.Lock()
|
||||
defer registry.Unlock()
|
||||
var newList []string
|
||||
for _, name := range registry.minions {
|
||||
if name != minion {
|
||||
newList = append(newList, name)
|
||||
}
|
||||
}
|
||||
registry.minions = newList
|
||||
return registry.err
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ type RandomFitScheduler struct {
|
||||
randomLock sync.Mutex
|
||||
}
|
||||
|
||||
func MakeRandomFitScheduler(podLister PodLister, random *rand.Rand) Scheduler {
|
||||
func NewRandomFitScheduler(podLister PodLister, random *rand.Rand) Scheduler {
|
||||
return &RandomFitScheduler{
|
||||
podLister: podLister,
|
||||
random: random,
|
||||
|
@@ -28,7 +28,7 @@ func TestRandomFitSchedulerNothingScheduled(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(0))
|
||||
st := schedulerTester{
|
||||
t: t,
|
||||
scheduler: MakeRandomFitScheduler(&fakeRegistry, r),
|
||||
scheduler: NewRandomFitScheduler(&fakeRegistry, r),
|
||||
minionLister: FakeMinionLister{"m1", "m2", "m3"},
|
||||
}
|
||||
st.expectSchedule(api.Pod{}, "m3")
|
||||
@@ -41,7 +41,7 @@ func TestRandomFitSchedulerFirstScheduled(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(0))
|
||||
st := schedulerTester{
|
||||
t: t,
|
||||
scheduler: MakeRandomFitScheduler(fakeRegistry, r),
|
||||
scheduler: NewRandomFitScheduler(fakeRegistry, r),
|
||||
minionLister: FakeMinionLister{"m1", "m2", "m3"},
|
||||
}
|
||||
st.expectSchedule(makePod("", 8080), "m3")
|
||||
@@ -56,7 +56,7 @@ func TestRandomFitSchedulerFirstScheduledComplicated(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(0))
|
||||
st := schedulerTester{
|
||||
t: t,
|
||||
scheduler: MakeRandomFitScheduler(fakeRegistry, r),
|
||||
scheduler: NewRandomFitScheduler(fakeRegistry, r),
|
||||
minionLister: FakeMinionLister{"m1", "m2", "m3"},
|
||||
}
|
||||
st.expectSchedule(makePod("", 8080, 8081), "m3")
|
||||
@@ -71,7 +71,7 @@ func TestRandomFitSchedulerFirstScheduledImpossible(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(0))
|
||||
st := schedulerTester{
|
||||
t: t,
|
||||
scheduler: MakeRandomFitScheduler(fakeRegistry, r),
|
||||
scheduler: NewRandomFitScheduler(fakeRegistry, r),
|
||||
minionLister: FakeMinionLister{"m1", "m2", "m3"},
|
||||
}
|
||||
st.expectFailure(makePod("", 8080, 8081))
|
||||
|
@@ -25,6 +25,16 @@ import (
|
||||
"github.com/coreos/go-etcd/etcd"
|
||||
)
|
||||
|
||||
const (
|
||||
EtcdErrorCodeNotFound = 100
|
||||
EtcdErrorCodeValueRequired = 200
|
||||
)
|
||||
|
||||
var (
|
||||
EtcdErrorNotFound = &etcd.EtcdError{ErrorCode: EtcdErrorCodeNotFound}
|
||||
EtcdErrorValueRequired = &etcd.EtcdError{ErrorCode: EtcdErrorCodeValueRequired}
|
||||
)
|
||||
|
||||
// EtcdClient is an injectable interface for testing.
|
||||
type EtcdClient interface {
|
||||
AddChild(key, data string, ttl uint64) (*etcd.Response, error)
|
||||
|
@@ -36,7 +36,7 @@ func TestIsNotFoundErr(t *testing.T) {
|
||||
t.Errorf("Expected %#v to return %v, but it did not", err, isNotFound)
|
||||
}
|
||||
}
|
||||
try(&etcd.EtcdError{ErrorCode: 100}, true)
|
||||
try(EtcdErrorNotFound, true)
|
||||
try(&etcd.EtcdError{ErrorCode: 101}, false)
|
||||
try(nil, false)
|
||||
try(fmt.Errorf("some other kind of error"), false)
|
||||
|
@@ -74,7 +74,7 @@ func (f *FakeEtcdClient) Get(key string, sort, recursive bool) (*etcd.Response,
|
||||
result := f.Data[key]
|
||||
if result.R == nil {
|
||||
f.t.Errorf("Unexpected get for %s", key)
|
||||
return &etcd.Response{}, &etcd.EtcdError{ErrorCode: 100} // Key not found
|
||||
return &etcd.Response{}, EtcdErrorNotFound
|
||||
}
|
||||
f.t.Logf("returning %v: %v %#v", key, result.R, result.E)
|
||||
return result.R, result.E
|
||||
@@ -105,7 +105,7 @@ func (f *FakeEtcdClient) Delete(key string, recursive bool) (*etcd.Response, err
|
||||
R: &etcd.Response{
|
||||
Node: nil,
|
||||
},
|
||||
E: &etcd.EtcdError{ErrorCode: 100},
|
||||
E: EtcdErrorNotFound,
|
||||
}
|
||||
|
||||
f.DeletedKeys = append(f.DeletedKeys, key)
|
||||
|
3
third_party/deps.sh
vendored
3
third_party/deps.sh
vendored
@@ -5,6 +5,8 @@ TOP_PACKAGES="
|
||||
code.google.com/p/goauth2/compute/serviceaccount
|
||||
code.google.com/p/goauth2/oauth
|
||||
code.google.com/p/google-api-go-client/compute/v1
|
||||
github.com/google/cadvisor/info
|
||||
github.com/google/cadvisor/client
|
||||
"
|
||||
|
||||
DEP_PACKAGES="
|
||||
@@ -13,7 +15,6 @@ DEP_PACKAGES="
|
||||
code.google.com/p/google-api-go-client/googleapi
|
||||
github.com/coreos/go-log/log
|
||||
github.com/coreos/go-systemd/journal
|
||||
github.com/google/cadvisor/info
|
||||
"
|
||||
|
||||
PACKAGES="$TOP_PACKAGES $DEP_PACKAGES"
|
||||
|
1
third_party/src/github.com/google/cadvisor/.gitignore
vendored
Normal file
1
third_party/src/github.com/google/cadvisor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.swp
|
@@ -1,13 +1,12 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
before_script:
|
||||
- go get github.com/stretchr/testify/mock
|
||||
- go get github.com/kr/pretty
|
||||
- wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb
|
||||
- sudo dpkg -i influxdb_latest_amd64.deb
|
||||
- sudo service influxdb start
|
||||
script:
|
||||
- go test -v -race github.com/google/cadvisor/container
|
||||
- go test -v github.com/google/cadvisor/info
|
||||
- go test -v github.com/google/cadvisor/client
|
||||
- go test -v github.com/google/cadvisor/sampling
|
||||
- go test -v github.com/google/cadvisor/storage/memory
|
||||
- go test -v -race github.com/google/cadvisor/...
|
||||
- go build github.com/google/cadvisor
|
||||
|
20
third_party/src/github.com/google/cadvisor/CHANGELOG.md
vendored
Normal file
20
third_party/src/github.com/google/cadvisor/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.2 (2014-07-10)
|
||||
- Added Storage Driver concept (flag: storage_driver), default is the in-memory driver
|
||||
- Implemented InfluxDB storage driver
|
||||
- Support in REST API for specifying number of stats to return
|
||||
- Allow running without lmctfy (flag: allow_lmctfy)
|
||||
- Bugfixes
|
||||
|
||||
## 0.1.0 (2014-06-14)
|
||||
- Support for container aliases
|
||||
- Sampling historical usage and exporting that in the REST API
|
||||
- Bugfixes for UI
|
||||
|
||||
## 0.0.0 (2014-06-10)
|
||||
- Initial version of cAdvisor
|
||||
- Web UI with auto-updating stats
|
||||
- v1.0 REST API with container and machine information
|
||||
- Support for Docker containers
|
||||
- Support for lmctfy containers
|
@@ -6,6 +6,10 @@
|
||||
|
||||
# Please keep the list sorted by first name.
|
||||
|
||||
Jason Swindle <jason@swindle.me>
|
||||
Johan Euphrosine <proppy@google.com>
|
||||
Kamil Yurtsever <kyurtsever@google.com>
|
||||
Nan Deng <dengnan@google.com>
|
||||
Rohit Jnagal <jnagal@google.com>
|
||||
Victor Marmol <vmarmol@google.com>
|
||||
Zohaib Maya <zohaib@google.com>
|
||||
|
32
third_party/src/github.com/google/cadvisor/advice/interference/detector.go
vendored
Normal file
32
third_party/src/github.com/google/cadvisor/advice/interference/detector.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2014 Google Inc. 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 inference
|
||||
|
||||
import "github.com/google/cadvisor/info"
|
||||
|
||||
// InterferenceDectector detects if there's a container which
|
||||
// interferences with a set of containers. The detector tracks
|
||||
// a set of containers and find the victims and antagonist.
|
||||
type InterferenceDetector interface {
|
||||
// Tracks the behavior of the container.
|
||||
AddContainer(ref info.ContainerReference)
|
||||
|
||||
// Returns a list of possible interferences. The upper layer may take action
|
||||
// based on the interference.
|
||||
Detect() ([]*info.Interference, error)
|
||||
|
||||
// The name of the detector.
|
||||
Name() string
|
||||
}
|
@@ -19,12 +19,13 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/info"
|
||||
"github.com/google/cadvisor/manager"
|
||||
)
|
||||
|
||||
@@ -34,9 +35,11 @@ const (
|
||||
MachineApi = "machine"
|
||||
)
|
||||
|
||||
func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
|
||||
func HandleRequest(m manager.Manager, w http.ResponseWriter, r *http.Request) error {
|
||||
start := time.Now()
|
||||
|
||||
u := r.URL
|
||||
|
||||
// Get API request type.
|
||||
requestType := u.Path[len(ApiResource):]
|
||||
i := strings.Index(requestType, "/")
|
||||
@@ -46,7 +49,8 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
|
||||
requestType = requestType[:i]
|
||||
}
|
||||
|
||||
if requestType == MachineApi {
|
||||
switch {
|
||||
case requestType == MachineApi:
|
||||
log.Printf("Api - Machine")
|
||||
|
||||
// Get the MachineInfo
|
||||
@@ -60,14 +64,20 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
|
||||
fmt.Fprintf(w, "Failed to marshall MachineInfo with error: %s", err)
|
||||
}
|
||||
w.Write(out)
|
||||
} else if requestType == ContainersApi {
|
||||
case requestType == ContainersApi:
|
||||
// The container name is the path after the requestType
|
||||
containerName := requestArgs
|
||||
|
||||
log.Printf("Api - Container(%s)", containerName)
|
||||
|
||||
var query info.ContainerInfoRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&query)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("unable to decode the json value: ", err)
|
||||
}
|
||||
// Get the container.
|
||||
cont, err := m.GetContainerInfo(containerName)
|
||||
cont, err := m.GetContainerInfo(containerName, &query)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Failed to get container \"%s\" with error: %s", containerName, err)
|
||||
return err
|
||||
@@ -79,7 +89,7 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error {
|
||||
fmt.Fprintf(w, "Failed to marshall container %q with error: %s", containerName, err)
|
||||
}
|
||||
w.Write(out)
|
||||
} else {
|
||||
default:
|
||||
return fmt.Errorf("unknown API request type %q", requestType)
|
||||
}
|
||||
|
||||
|
@@ -27,32 +27,46 @@ import (
|
||||
"github.com/google/cadvisor/manager"
|
||||
"github.com/google/cadvisor/pages"
|
||||
"github.com/google/cadvisor/pages/static"
|
||||
"github.com/google/cadvisor/storage/memory"
|
||||
)
|
||||
|
||||
var argPort = flag.Int("port", 8080, "port to listen")
|
||||
var argSampleSize = flag.Int("samples", 1024, "number of samples we want to keep")
|
||||
var argHistoryDuration = flag.Int("history_duration", 60, "number of seconds of container history to keep")
|
||||
var argAllowLmctfy = flag.Bool("allow_lmctfy", true, "whether to allow lmctfy as a container handler")
|
||||
|
||||
var argDbDriver = flag.String("storage_driver", "memory", "storage driver to use. Options are: memory (default) and influxdb")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
storage := memory.New(*argSampleSize, *argHistoryDuration)
|
||||
// TODO(monnand): Add stats writer for manager
|
||||
containerManager, err := manager.New(storage)
|
||||
storageDriver, err := NewStorageDriver(*argDbDriver)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %s", err)
|
||||
}
|
||||
|
||||
containerManager, err := manager.New(storageDriver)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a Container Manager: %s", err)
|
||||
}
|
||||
|
||||
if err := lmctfy.Register("/"); err != nil {
|
||||
log.Printf("lmctfy registration failed: %v.", err)
|
||||
log.Print("Running in docker only mode.")
|
||||
if err := docker.Register(containerManager, "/"); err != nil {
|
||||
log.Printf("Docker registration failed: %v.", err)
|
||||
log.Fatalf("Unable to continue without docker or lmctfy.")
|
||||
// Register lmctfy for the root if allowed and available.
|
||||
registeredRoot := false
|
||||
if *argAllowLmctfy {
|
||||
if err := lmctfy.Register("/"); err != nil {
|
||||
log.Printf("lmctfy registration failed: %v.", err)
|
||||
log.Print("Running in docker only mode.")
|
||||
} else {
|
||||
registeredRoot = true
|
||||
}
|
||||
}
|
||||
|
||||
// Register Docker for root if we were unable to register lmctfy.
|
||||
if !registeredRoot {
|
||||
if err := docker.Register(containerManager, "/"); err != nil {
|
||||
log.Printf("Docker registration failed: %v.", err)
|
||||
log.Fatalf("Unable to continue without root handler.")
|
||||
}
|
||||
}
|
||||
|
||||
// Register Docker for all Docker containers.
|
||||
if err := docker.Register(containerManager, "/docker"); err != nil {
|
||||
// Ignore this error because we should work with lmctfy only
|
||||
log.Printf("Docker registration failed: %v.", err)
|
||||
@@ -69,7 +83,7 @@ func main() {
|
||||
|
||||
// Handler for the API.
|
||||
http.HandleFunc(api.ApiResource, func(w http.ResponseWriter, r *http.Request) {
|
||||
err := api.HandleRequest(containerManager, w, r.URL)
|
||||
err := api.HandleRequest(containerManager, w, r)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%s", err)
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
package cadvisor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -49,7 +50,7 @@ func (self *Client) machineInfoUrl() string {
|
||||
func (self *Client) MachineInfo() (minfo *info.MachineInfo, err error) {
|
||||
u := self.machineInfoUrl()
|
||||
ret := new(info.MachineInfo)
|
||||
err = self.httpGetJsonData(ret, u, "machine info")
|
||||
err = self.httpGetJsonData(ret, nil, u, "machine info")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -64,8 +65,19 @@ func (self *Client) containerInfoUrl(name string) string {
|
||||
return strings.Join([]string{self.baseUrl, "containers", name}, "/")
|
||||
}
|
||||
|
||||
func (self *Client) httpGetJsonData(data interface{}, url, infoName string) error {
|
||||
resp, err := http.Get(url)
|
||||
func (self *Client) httpGetJsonData(data, postData interface{}, url, infoName string) error {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
if postData != nil {
|
||||
data, err := json.Marshal(postData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal data: %v", err)
|
||||
}
|
||||
resp, err = http.Post(url, "application/json", bytes.NewBuffer(data))
|
||||
} else {
|
||||
resp, err = http.Get(url)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to get %v: %v", infoName, err)
|
||||
return err
|
||||
@@ -84,10 +96,12 @@ func (self *Client) httpGetJsonData(data interface{}, url, infoName string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Client) ContainerInfo(name string) (cinfo *info.ContainerInfo, err error) {
|
||||
func (self *Client) ContainerInfo(
|
||||
name string,
|
||||
query *info.ContainerInfoRequest) (cinfo *info.ContainerInfo, err error) {
|
||||
u := self.containerInfoUrl(name)
|
||||
ret := new(info.ContainerInfo)
|
||||
err = self.httpGetJsonData(ret, u, fmt.Sprintf("container info for %v", name))
|
||||
err = self.httpGetJsonData(ret, query, u, fmt.Sprintf("container info for %v", name))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@@ -21,33 +21,42 @@ import (
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/info"
|
||||
itest "github.com/google/cadvisor/info/test"
|
||||
"github.com/kr/pretty"
|
||||
)
|
||||
|
||||
func testGetJsonData(
|
||||
strRep string,
|
||||
emptyData interface{},
|
||||
expected interface{},
|
||||
f func() (interface{}, error),
|
||||
) error {
|
||||
err := json.Unmarshal([]byte(strRep), emptyData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid json input: %v", err)
|
||||
}
|
||||
reply, err := f()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve data: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(reply, emptyData) {
|
||||
return fmt.Errorf("retrieved wrong data: %+v != %+v", reply, emptyData)
|
||||
if !reflect.DeepEqual(reply, expected) {
|
||||
return pretty.Errorf("retrieved wrong data: %# v != %# v", reply, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cadvisorTestClient(path, reply string) (*Client, *httptest.Server, error) {
|
||||
func cadvisorTestClient(path string, expectedPostObj, expectedPostObjEmpty, replyObj interface{}, t *testing.T) (*Client, *httptest.Server, error) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == path {
|
||||
fmt.Fprint(w, reply)
|
||||
if expectedPostObj != nil {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(expectedPostObjEmpty)
|
||||
if err != nil {
|
||||
t.Errorf("Recieved invalid object: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedPostObj, expectedPostObjEmpty) {
|
||||
t.Errorf("Recieved unexpected object: %+v", expectedPostObjEmpty)
|
||||
}
|
||||
}
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode(replyObj)
|
||||
} else if r.URL.Path == "/api/v1.0/machine" {
|
||||
fmt.Fprint(w, `{"num_cores":8,"memory_capacity":31625871360}`)
|
||||
} else {
|
||||
@@ -64,693 +73,69 @@ func cadvisorTestClient(path, reply string) (*Client, *httptest.Server, error) {
|
||||
}
|
||||
|
||||
func TestGetMachineinfo(t *testing.T) {
|
||||
respStr := `{"num_cores":8,"memory_capacity":31625871360}`
|
||||
client, server, err := cadvisorTestClient("/api/v1.0/machine", respStr)
|
||||
minfo := &info.MachineInfo{
|
||||
NumCores: 8,
|
||||
MemoryCapacity: 31625871360,
|
||||
}
|
||||
client, server, err := cadvisorTestClient("/api/v1.0/machine", nil, nil, minfo, t)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get a client %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
err = testGetJsonData(respStr, &info.MachineInfo{}, func() (interface{}, error) {
|
||||
return client.MachineInfo()
|
||||
})
|
||||
returned, err := client.MachineInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(returned, minfo) {
|
||||
t.Fatalf("received unexpected machine info")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetContainerInfo(t *testing.T) {
|
||||
respStr := `
|
||||
{
|
||||
"name": "%v",
|
||||
"spec": {
|
||||
"cpu": {
|
||||
"limit": 18446744073709551000,
|
||||
"max_limit": 0,
|
||||
"mask": {
|
||||
"data": [
|
||||
18446744073709551000
|
||||
]
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"limit": 18446744073709551000,
|
||||
"swap_limit": 18446744073709551000
|
||||
}
|
||||
},
|
||||
"stats": [
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:26.434981825Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:27.538394608Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:28.640302072Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:29.74247308Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:30.844494537Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:31.946757066Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:33.050214062Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:34.15222186Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:35.25417391Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:36.355902169Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:37.457585928Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:38.559417379Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:39.662978029Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:40.764671232Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:41.866456459Z",
|
||||
"cpu": {
|
||||
"usage": {
|
||||
"total": 56896502,
|
||||
"per_cpu": [
|
||||
20479682,
|
||||
13579420,
|
||||
6025040,
|
||||
2255123,
|
||||
3635661,
|
||||
2489368,
|
||||
5158288,
|
||||
3273920
|
||||
],
|
||||
"user": 10000000,
|
||||
"system": 30000000
|
||||
},
|
||||
"load": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616,
|
||||
"container_data": {
|
||||
"pgfault": 2279
|
||||
},
|
||||
"hierarchical_data": {
|
||||
"pgfault": 2279
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"stats_summary": {
|
||||
"max_memory_usage": 495616,
|
||||
"samples": [
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:27.538394608Z",
|
||||
"duration": 1103412783,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:28.640302072Z",
|
||||
"duration": 1101907464,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:29.74247308Z",
|
||||
"duration": 1102171008,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:30.844494537Z",
|
||||
"duration": 1102021457,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:31.946757066Z",
|
||||
"duration": 1102262529,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:33.050214062Z",
|
||||
"duration": 1103456996,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:34.15222186Z",
|
||||
"duration": 1102007798,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:35.25417391Z",
|
||||
"duration": 1101952050,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:36.355902169Z",
|
||||
"duration": 1101728259,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:37.457585928Z",
|
||||
"duration": 1101683759,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:38.559417379Z",
|
||||
"duration": 1101831451,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:39.662978029Z",
|
||||
"duration": 1103560650,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:40.764671232Z",
|
||||
"duration": 1101693203,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2014-06-13T01:03:41.866456459Z",
|
||||
"duration": 1101785227,
|
||||
"cpu": {
|
||||
"usage": 0
|
||||
},
|
||||
"memory": {
|
||||
"usage": 495616
|
||||
}
|
||||
}
|
||||
],
|
||||
"memory_usage_percentiles": [
|
||||
{
|
||||
"percentage": 50,
|
||||
"value": 495616
|
||||
},
|
||||
{
|
||||
"percentage": 80,
|
||||
"value": 495616
|
||||
},
|
||||
{
|
||||
"percentage": 90,
|
||||
"value": 495616
|
||||
},
|
||||
{
|
||||
"percentage": 95,
|
||||
"value": 495616
|
||||
},
|
||||
{
|
||||
"percentage": 99,
|
||||
"value": 495616
|
||||
}
|
||||
],
|
||||
"cpu_usage_percentiles": [
|
||||
{
|
||||
"percentage": 50,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"percentage": 80,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"percentage": 90,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"percentage": 95,
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"percentage": 99,
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
query := &info.ContainerInfoRequest{
|
||||
NumStats: 3,
|
||||
NumSamples: 2,
|
||||
CpuUsagePercentiles: []int{10, 50, 90},
|
||||
MemoryUsagePercentages: []int{10, 80, 90},
|
||||
}
|
||||
containerName := "/some/container"
|
||||
respStr = fmt.Sprintf(respStr, containerName)
|
||||
client, server, err := cadvisorTestClient(fmt.Sprintf("/api/v1.0/containers%v", containerName), respStr)
|
||||
cinfo := itest.GenerateRandomContainerInfo(containerName, 4, query, 1*time.Second)
|
||||
client, server, err := cadvisorTestClient(fmt.Sprintf("/api/v1.0/containers%v", containerName), query, &info.ContainerInfoRequest{}, cinfo, t)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get a client %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
err = testGetJsonData(respStr, &info.ContainerInfo{}, func() (interface{}, error) {
|
||||
return client.ContainerInfo(containerName)
|
||||
})
|
||||
returned, err := client.ContainerInfo(containerName, query)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We cannot use DeepEqual() to compare them directly,
|
||||
// because json en/decoded time may have precision issues.
|
||||
if !reflect.DeepEqual(returned.ContainerReference, cinfo.ContainerReference) {
|
||||
t.Errorf("received unexpected container ref")
|
||||
}
|
||||
if !reflect.DeepEqual(returned.Subcontainers, cinfo.Subcontainers) {
|
||||
t.Errorf("received unexpected subcontainers")
|
||||
}
|
||||
if !reflect.DeepEqual(returned.Spec, cinfo.Spec) {
|
||||
t.Errorf("received unexpected spec")
|
||||
}
|
||||
if !reflect.DeepEqual(returned.StatsPercentiles, cinfo.StatsPercentiles) {
|
||||
t.Errorf("received unexpected spec")
|
||||
}
|
||||
|
||||
for i, expectedStats := range cinfo.Stats {
|
||||
returnedStats := returned.Stats[i]
|
||||
if !expectedStats.Eq(returnedStats) {
|
||||
t.Errorf("received unexpected stats")
|
||||
}
|
||||
}
|
||||
|
||||
for i, expectedSample := range cinfo.Samples {
|
||||
returnedSample := returned.Samples[i]
|
||||
if !expectedSample.Eq(returnedSample) {
|
||||
t.Errorf("received unexpected sample")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ func (self *dockerContainerHandler) isDockerContainer() bool {
|
||||
}
|
||||
|
||||
// TODO(vmarmol): Switch to getting this from libcontainer once we have a solid API.
|
||||
func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) {
|
||||
func readLibcontainerSpec(id string) (spec *libcontainer.Config, err error) {
|
||||
dir := "/var/lib/docker/execdriver/native"
|
||||
configPath := path.Join(dir, id, "container.json")
|
||||
f, err := os.Open(configPath)
|
||||
@@ -127,7 +127,7 @@ func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) {
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
ret := new(libcontainer.Container)
|
||||
ret := new(libcontainer.Config)
|
||||
err = d.Decode(ret)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -136,7 +136,7 @@ func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func libcontainerConfigToContainerSpec(config *libcontainer.Container, mi *info.MachineInfo) *info.ContainerSpec {
|
||||
func libcontainerConfigToContainerSpec(config *libcontainer.Config, mi *info.MachineInfo) *info.ContainerSpec {
|
||||
spec := new(info.ContainerSpec)
|
||||
spec.Memory = new(info.MemorySpec)
|
||||
spec.Memory.Limit = math.MaxUint64
|
||||
@@ -209,6 +209,12 @@ func libcontainerToContainerStats(s *cgroups.Stats, mi *info.MachineInfo) *info.
|
||||
ret.Memory.ContainerData.Pgmajfault = v
|
||||
ret.Memory.HierarchicalData.Pgmajfault = v
|
||||
}
|
||||
if v, ok := s.MemoryStats.Stats["total_inactive_anon"]; ok {
|
||||
ret.Memory.WorkingSet = ret.Memory.Usage - v
|
||||
if v, ok := s.MemoryStats.Stats["total_active_file"]; ok {
|
||||
ret.Memory.WorkingSet -= v
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
88
third_party/src/github.com/google/cadvisor/container/test/mock.go
vendored
Normal file
88
third_party/src/github.com/google/cadvisor/container/test/mock.go
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2014 Google Inc. 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 test
|
||||
|
||||
import (
|
||||
"github.com/google/cadvisor/container"
|
||||
"github.com/google/cadvisor/info"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// This struct mocks a container handler.
|
||||
type MockContainerHandler struct {
|
||||
mock.Mock
|
||||
Name string
|
||||
Aliases []string
|
||||
}
|
||||
|
||||
// If self.Name is not empty, then ContainerReference() will return self.Name and self.Aliases.
|
||||
// Otherwise, it will use the value provided by .On().Return().
|
||||
func (self *MockContainerHandler) ContainerReference() (info.ContainerReference, error) {
|
||||
if len(self.Name) > 0 {
|
||||
var aliases []string
|
||||
if len(self.Aliases) > 0 {
|
||||
aliases = make([]string, len(self.Aliases))
|
||||
copy(aliases, self.Aliases)
|
||||
}
|
||||
return info.ContainerReference{
|
||||
Name: self.Name,
|
||||
Aliases: aliases,
|
||||
}, nil
|
||||
}
|
||||
args := self.Called()
|
||||
return args.Get(0).(info.ContainerReference), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockContainerHandler) GetSpec() (*info.ContainerSpec, error) {
|
||||
args := self.Called()
|
||||
return args.Get(0).(*info.ContainerSpec), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockContainerHandler) GetStats() (*info.ContainerStats, error) {
|
||||
args := self.Called()
|
||||
return args.Get(0).(*info.ContainerStats), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockContainerHandler) ListContainers(listType container.ListType) ([]info.ContainerReference, error) {
|
||||
args := self.Called(listType)
|
||||
return args.Get(0).([]info.ContainerReference), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockContainerHandler) ListThreads(listType container.ListType) ([]int, error) {
|
||||
args := self.Called(listType)
|
||||
return args.Get(0).([]int), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockContainerHandler) ListProcesses(listType container.ListType) ([]int, error) {
|
||||
args := self.Called(listType)
|
||||
return args.Get(0).([]int), args.Error(1)
|
||||
}
|
||||
|
||||
type FactoryForMockContainerHandler struct {
|
||||
Name string
|
||||
PrepareContainerHandlerFunc func(name string, handler *MockContainerHandler)
|
||||
}
|
||||
|
||||
func (self *FactoryForMockContainerHandler) String() string {
|
||||
return self.Name
|
||||
}
|
||||
|
||||
func (self *FactoryForMockContainerHandler) NewContainerHandler(name string) (container.ContainerHandler, error) {
|
||||
handler := &MockContainerHandler{}
|
||||
if self.PrepareContainerHandlerFunc != nil {
|
||||
self.PrepareContainerHandlerFunc(name, handler)
|
||||
}
|
||||
return handler, nil
|
||||
}
|
@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y -q --no-install-recommends pkg-config l
|
||||
# Get the lcmtfy and cAdvisor binaries.
|
||||
ADD http://storage.googleapis.com/cadvisor-bin/lmctfy/lmctfy /usr/bin/lmctfy
|
||||
ADD http://storage.googleapis.com/cadvisor-bin/lmctfy/libre2.so.0.0.0 /usr/lib/libre2.so.0
|
||||
ADD http://storage.googleapis.com/cadvisor-bin/cadvisor /usr/bin/cadvisor
|
||||
ADD http://storage.googleapis.com/cadvisor-bin/cadvisor-0.1.2 /usr/bin/cadvisor
|
||||
RUN chmod +x /usr/bin/lmctfy && chmod +x /usr/bin/cadvisor
|
||||
|
||||
EXPOSE 8080
|
||||
|
34
third_party/src/github.com/google/cadvisor/info/advice.go
vendored
Normal file
34
third_party/src/github.com/google/cadvisor/info/advice.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2014 Google Inc. 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 info
|
||||
|
||||
// This struct describes one type of relationship between containers: One
|
||||
// container, antagonist, interferes the performance of other
|
||||
// containers, victims.
|
||||
type Interference struct {
|
||||
// Absolute name of the antagonist container name. This field
|
||||
// should not be empty.
|
||||
Antagonist string `json:"antagonist"`
|
||||
|
||||
// The absolute path of the victims. This field should not be empty.
|
||||
Victims []string `json:"victims"`
|
||||
|
||||
// The name of the detector used to detect this antagonism. This field
|
||||
// should not be empty
|
||||
Detector string `json:"detector"`
|
||||
|
||||
// Human readable description of this interference
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
@@ -16,6 +16,7 @@ package info
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
@@ -57,6 +58,40 @@ type ContainerReference struct {
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerInfoQuery is used when users check a container info from the REST api.
|
||||
// It specifies how much data users want to get about a container
|
||||
type ContainerInfoRequest struct {
|
||||
// Max number of stats to return.
|
||||
NumStats int `json:"num_stats,omitempty"`
|
||||
// Max number of samples to return.
|
||||
NumSamples int `json:"num_samples,omitempty"`
|
||||
|
||||
// Different percentiles of CPU usage within a period. The values must be within [0, 100]
|
||||
CpuUsagePercentiles []int `json:"cpu_usage_percentiles,omitempty"`
|
||||
// Different percentiles of memory usage within a period. The values must be within [0, 100]
|
||||
MemoryUsagePercentages []int `json:"memory_usage_percentiles,omitempty"`
|
||||
}
|
||||
|
||||
func (self *ContainerInfoRequest) FillDefaults() *ContainerInfoRequest {
|
||||
ret := self
|
||||
if ret == nil {
|
||||
ret = new(ContainerInfoRequest)
|
||||
}
|
||||
if ret.NumStats <= 0 {
|
||||
ret.NumStats = 1024
|
||||
}
|
||||
if ret.NumSamples <= 0 {
|
||||
ret.NumSamples = 1024
|
||||
}
|
||||
if len(ret.CpuUsagePercentiles) == 0 {
|
||||
ret.CpuUsagePercentiles = []int{50, 80, 90, 99}
|
||||
}
|
||||
if len(ret.MemoryUsagePercentages) == 0 {
|
||||
ret.MemoryUsagePercentages = []int{50, 80, 90, 99}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type ContainerInfo struct {
|
||||
ContainerReference
|
||||
|
||||
@@ -119,7 +154,7 @@ type CpuStats struct {
|
||||
|
||||
// Per CPU/core usage of the container.
|
||||
// Unit: nanoseconds.
|
||||
PerCpu []uint64 `json:"per_cpu,omitempty"`
|
||||
PerCpu []uint64 `json:"per_cpu_usage,omitempty"`
|
||||
|
||||
// Time spent in user space.
|
||||
// Unit: nanoseconds
|
||||
@@ -165,6 +200,42 @@ type ContainerStats struct {
|
||||
Memory *MemoryStats `json:"memory,omitempty"`
|
||||
}
|
||||
|
||||
// Makes a deep copy of the ContainerStats and returns a pointer to the new
|
||||
// copy. Copy() will allocate a new ContainerStats object if dst is nil.
|
||||
func (self *ContainerStats) Copy(dst *ContainerStats) *ContainerStats {
|
||||
if dst == nil {
|
||||
dst = new(ContainerStats)
|
||||
}
|
||||
dst.Timestamp = self.Timestamp
|
||||
if self.Cpu != nil {
|
||||
if dst.Cpu == nil {
|
||||
dst.Cpu = new(CpuStats)
|
||||
}
|
||||
// To make a deep copy of a slice, we need to copy every value
|
||||
// in the slice. To make less memory allocation, we would like
|
||||
// to reuse the slice in dst if possible.
|
||||
percpu := dst.Cpu.Usage.PerCpu
|
||||
if len(percpu) != len(self.Cpu.Usage.PerCpu) {
|
||||
percpu = make([]uint64, len(self.Cpu.Usage.PerCpu))
|
||||
}
|
||||
dst.Cpu.Usage = self.Cpu.Usage
|
||||
dst.Cpu.Load = self.Cpu.Load
|
||||
copy(percpu, self.Cpu.Usage.PerCpu)
|
||||
dst.Cpu.Usage.PerCpu = percpu
|
||||
} else {
|
||||
dst.Cpu = nil
|
||||
}
|
||||
if self.Memory != nil {
|
||||
if dst.Memory == nil {
|
||||
dst.Memory = new(MemoryStats)
|
||||
}
|
||||
*dst.Memory = *self.Memory
|
||||
} else {
|
||||
dst.Memory = nil
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
type ContainerStatsSample struct {
|
||||
// Timetamp of the end of the sample period
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
@@ -173,6 +244,9 @@ type ContainerStatsSample struct {
|
||||
Cpu struct {
|
||||
// number of nanoseconds of CPU time used by the container
|
||||
Usage uint64 `json:"usage"`
|
||||
|
||||
// Per-core usage of the container. (unit: nanoseconds)
|
||||
PerCpuUsage []uint64 `json:"per_cpu_usage,omitempty"`
|
||||
} `json:"cpu"`
|
||||
Memory struct {
|
||||
// Units: Bytes.
|
||||
@@ -180,6 +254,67 @@ type ContainerStatsSample struct {
|
||||
} `json:"memory"`
|
||||
}
|
||||
|
||||
func timeEq(t1, t2 time.Time, tolerance time.Duration) bool {
|
||||
// t1 should not be later than t2
|
||||
if t1.After(t2) {
|
||||
t1, t2 = t2, t1
|
||||
}
|
||||
diff := t2.Sub(t1)
|
||||
if diff <= tolerance {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func durationEq(a, b time.Duration, tolerance time.Duration) bool {
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
diff := a - b
|
||||
if diff <= tolerance {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
// 10ms, i.e. 0.01s
|
||||
timePrecision time.Duration = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
// This function is useful because we do not require precise time
|
||||
// representation.
|
||||
func (a *ContainerStats) Eq(b *ContainerStats) bool {
|
||||
if !timeEq(a.Timestamp, b.Timestamp, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Cpu, b.Cpu) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Memory, b.Memory) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// This function is useful because we do not require precise time
|
||||
// representation.
|
||||
func (a *ContainerStatsSample) Eq(b *ContainerStatsSample) bool {
|
||||
if !timeEq(a.Timestamp, b.Timestamp, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !durationEq(a.Duration, b.Duration, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Cpu, b.Cpu) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Memory, b.Memory) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Percentile struct {
|
||||
Percentage int `json:"percentage"`
|
||||
Value uint64 `json:"value"`
|
||||
@@ -211,9 +346,28 @@ func NewSample(prev, current *ContainerStats) (*ContainerStatsSample, error) {
|
||||
if current.Cpu.Usage.Total < prev.Cpu.Usage.Total {
|
||||
return nil, fmt.Errorf("current CPU usage is less than prev CPU usage (cumulative).")
|
||||
}
|
||||
|
||||
var percpu []uint64
|
||||
|
||||
if len(current.Cpu.Usage.PerCpu) > 0 {
|
||||
curNumCpus := len(current.Cpu.Usage.PerCpu)
|
||||
percpu = make([]uint64, curNumCpus)
|
||||
|
||||
for i, currUsage := range current.Cpu.Usage.PerCpu {
|
||||
var prevUsage uint64 = 0
|
||||
if i < len(prev.Cpu.Usage.PerCpu) {
|
||||
prevUsage = prev.Cpu.Usage.PerCpu[i]
|
||||
}
|
||||
if currUsage < prevUsage {
|
||||
return nil, fmt.Errorf("current per-core CPU usage is less than prev per-core CPU usage (cumulative).")
|
||||
}
|
||||
percpu[i] = currUsage - prevUsage
|
||||
}
|
||||
}
|
||||
sample := new(ContainerStatsSample)
|
||||
// Calculate the diff to get the CPU usage within the time interval.
|
||||
sample.Cpu.Usage = current.Cpu.Usage.Total - prev.Cpu.Usage.Total
|
||||
sample.Cpu.PerCpuUsage = percpu
|
||||
// Memory usage is current memory usage
|
||||
sample.Memory.Usage = current.Memory.Usage
|
||||
sample.Timestamp = current.Timestamp
|
||||
|
@@ -15,6 +15,7 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -230,3 +231,68 @@ func TestAddSampleWrongCpuUsage(t *testing.T) {
|
||||
t.Errorf("generated an unexpected sample: %+v", sample)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSampleHotPluggingCpu(t *testing.T) {
|
||||
cpuPrevUsage := uint64(10)
|
||||
cpuCurrentUsage := uint64(15)
|
||||
memCurrentUsage := uint64(200)
|
||||
prevTime := time.Now()
|
||||
|
||||
prev := createStats(cpuPrevUsage, memCurrentUsage, prevTime)
|
||||
current := createStats(cpuCurrentUsage, memCurrentUsage, prevTime.Add(1*time.Second))
|
||||
current.Cpu.Usage.PerCpu = append(current.Cpu.Usage.PerCpu, 10)
|
||||
|
||||
sample, err := NewSample(prev, current)
|
||||
if err != nil {
|
||||
t.Errorf("should be able to generate a sample. but received error: %v", err)
|
||||
}
|
||||
if len(sample.Cpu.PerCpuUsage) != 2 {
|
||||
t.Fatalf("Should have 2 cores.")
|
||||
}
|
||||
if sample.Cpu.PerCpuUsage[0] != cpuCurrentUsage-cpuPrevUsage {
|
||||
t.Errorf("First cpu usage is %v. should be %v", sample.Cpu.PerCpuUsage[0], cpuCurrentUsage-cpuPrevUsage)
|
||||
}
|
||||
if sample.Cpu.PerCpuUsage[1] != 10 {
|
||||
t.Errorf("Second cpu usage is %v. should be 10", sample.Cpu.PerCpuUsage[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSampleHotUnpluggingCpu(t *testing.T) {
|
||||
cpuPrevUsage := uint64(10)
|
||||
cpuCurrentUsage := uint64(15)
|
||||
memCurrentUsage := uint64(200)
|
||||
prevTime := time.Now()
|
||||
|
||||
prev := createStats(cpuPrevUsage, memCurrentUsage, prevTime)
|
||||
current := createStats(cpuCurrentUsage, memCurrentUsage, prevTime.Add(1*time.Second))
|
||||
prev.Cpu.Usage.PerCpu = append(prev.Cpu.Usage.PerCpu, 10)
|
||||
|
||||
sample, err := NewSample(prev, current)
|
||||
if err != nil {
|
||||
t.Errorf("should be able to generate a sample. but received error: %v", err)
|
||||
}
|
||||
if len(sample.Cpu.PerCpuUsage) != 1 {
|
||||
t.Fatalf("Should have 1 cores.")
|
||||
}
|
||||
if sample.Cpu.PerCpuUsage[0] != cpuCurrentUsage-cpuPrevUsage {
|
||||
t.Errorf("First cpu usage is %v. should be %v", sample.Cpu.PerCpuUsage[0], cpuCurrentUsage-cpuPrevUsage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStatsCopy(t *testing.T) {
|
||||
stats := createStats(100, 101, time.Now())
|
||||
shadowStats := stats.Copy(nil)
|
||||
if !reflect.DeepEqual(stats, shadowStats) {
|
||||
t.Errorf("Copy() returned different object")
|
||||
}
|
||||
stats.Cpu.Usage.PerCpu[0] = shadowStats.Cpu.Usage.PerCpu[0] + 1
|
||||
stats.Cpu.Load = shadowStats.Cpu.Load + 1
|
||||
stats.Memory.Usage = shadowStats.Memory.Usage + 1
|
||||
if reflect.DeepEqual(stats, shadowStats) {
|
||||
t.Errorf("Copy() did not deeply copy the object")
|
||||
}
|
||||
stats = shadowStats.Copy(stats)
|
||||
if !reflect.DeepEqual(stats, shadowStats) {
|
||||
t.Errorf("Copy() returned different object")
|
||||
}
|
||||
}
|
||||
|
127
third_party/src/github.com/google/cadvisor/info/test/datagen.go
vendored
Normal file
127
third_party/src/github.com/google/cadvisor/info/test/datagen.go
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2014 Google Inc. 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 test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/info"
|
||||
)
|
||||
|
||||
func GenerateRandomStats(numStats, numCores int, duration time.Duration) []*info.ContainerStats {
|
||||
ret := make([]*info.ContainerStats, numStats)
|
||||
perCoreUsages := make([]uint64, numCores)
|
||||
currentTime := time.Now()
|
||||
for i := range perCoreUsages {
|
||||
perCoreUsages[i] = uint64(rand.Int63n(1000))
|
||||
}
|
||||
for i := 0; i < numStats; i++ {
|
||||
stats := new(info.ContainerStats)
|
||||
stats.Cpu = new(info.CpuStats)
|
||||
stats.Memory = new(info.MemoryStats)
|
||||
stats.Timestamp = currentTime
|
||||
currentTime = currentTime.Add(duration)
|
||||
|
||||
percore := make([]uint64, numCores)
|
||||
for i := range perCoreUsages {
|
||||
perCoreUsages[i] += uint64(rand.Int63n(1000))
|
||||
percore[i] = perCoreUsages[i]
|
||||
stats.Cpu.Usage.Total += percore[i]
|
||||
}
|
||||
stats.Cpu.Usage.PerCpu = percore
|
||||
stats.Cpu.Usage.User = stats.Cpu.Usage.Total
|
||||
stats.Cpu.Usage.System = 0
|
||||
stats.Memory.Usage = uint64(rand.Int63n(4096))
|
||||
ret[i] = stats
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func GenerateRandomContainerSpec(numCores int) *info.ContainerSpec {
|
||||
ret := &info.ContainerSpec{
|
||||
Cpu: &info.CpuSpec{},
|
||||
Memory: &info.MemorySpec{},
|
||||
}
|
||||
ret.Cpu.Limit = uint64(1000 + rand.Int63n(2000))
|
||||
ret.Cpu.MaxLimit = uint64(1000 + rand.Int63n(2000))
|
||||
n := (numCores + 63) / 64
|
||||
ret.Cpu.Mask.Data = make([]uint64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ret.Cpu.Mask.Data[i] = math.MaxUint64
|
||||
}
|
||||
|
||||
ret.Memory.Limit = uint64(4096 + rand.Int63n(4096))
|
||||
return ret
|
||||
}
|
||||
|
||||
func GenerateRandomContainerInfo(containerName string, numCores int, query *info.ContainerInfoRequest, duration time.Duration) *info.ContainerInfo {
|
||||
stats := GenerateRandomStats(query.NumStats, numCores, duration)
|
||||
samples, _ := NewSamplesFromStats(stats...)
|
||||
if len(samples) > query.NumSamples {
|
||||
samples = samples[:query.NumSamples]
|
||||
}
|
||||
cpuPercentiles := make([]info.Percentile, 0, len(query.CpuUsagePercentiles))
|
||||
|
||||
// TODO(monnand): This will generate percentiles where 50%tile data may
|
||||
// be larger than 90%tile data.
|
||||
for _, p := range query.CpuUsagePercentiles {
|
||||
percentile := info.Percentile{p, uint64(rand.Int63n(1000))}
|
||||
cpuPercentiles = append(cpuPercentiles, percentile)
|
||||
}
|
||||
memPercentiles := make([]info.Percentile, 0, len(query.MemoryUsagePercentages))
|
||||
for _, p := range query.MemoryUsagePercentages {
|
||||
percentile := info.Percentile{p, uint64(rand.Int63n(1000))}
|
||||
memPercentiles = append(memPercentiles, percentile)
|
||||
}
|
||||
|
||||
percentiles := &info.ContainerStatsPercentiles{
|
||||
MaxMemoryUsage: uint64(rand.Int63n(4096)),
|
||||
MemoryUsagePercentiles: memPercentiles,
|
||||
CpuUsagePercentiles: cpuPercentiles,
|
||||
}
|
||||
|
||||
spec := GenerateRandomContainerSpec(numCores)
|
||||
|
||||
ret := &info.ContainerInfo{
|
||||
ContainerReference: info.ContainerReference{
|
||||
Name: containerName,
|
||||
},
|
||||
Spec: spec,
|
||||
StatsPercentiles: percentiles,
|
||||
Samples: samples,
|
||||
Stats: stats,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func NewSamplesFromStats(stats ...*info.ContainerStats) ([]*info.ContainerStatsSample, error) {
|
||||
if len(stats) < 2 {
|
||||
return nil, nil
|
||||
}
|
||||
samples := make([]*info.ContainerStatsSample, 0, len(stats)-1)
|
||||
for i, s := range stats[1:] {
|
||||
prev := stats[i]
|
||||
sample, err := info.NewSample(prev, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to generate sample from %+v and %+v: %v",
|
||||
prev, s, err)
|
||||
}
|
||||
samples = append(samples, sample)
|
||||
}
|
||||
return samples, nil
|
||||
}
|
@@ -15,4 +15,4 @@
|
||||
package info
|
||||
|
||||
// Version of cAdvisor.
|
||||
const VERSION = "0.1.0"
|
||||
const VERSION = "0.1.2"
|
||||
|
237
third_party/src/github.com/google/cadvisor/manager/container_test.go
vendored
Normal file
237
third_party/src/github.com/google/cadvisor/manager/container_test.go
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2014 Google Inc. 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.
|
||||
|
||||
// Per-container manager.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/container"
|
||||
ctest "github.com/google/cadvisor/container/test"
|
||||
"github.com/google/cadvisor/info"
|
||||
itest "github.com/google/cadvisor/info/test"
|
||||
"github.com/google/cadvisor/storage"
|
||||
stest "github.com/google/cadvisor/storage/test"
|
||||
)
|
||||
|
||||
func createContainerDataAndSetHandler(
|
||||
driver storage.StorageDriver,
|
||||
f func(*ctest.MockContainerHandler),
|
||||
t *testing.T,
|
||||
) *containerData {
|
||||
factory := &ctest.FactoryForMockContainerHandler{
|
||||
Name: "factoryForMockContainer",
|
||||
PrepareContainerHandlerFunc: func(name string, handler *ctest.MockContainerHandler) {
|
||||
handler.Name = name
|
||||
f(handler)
|
||||
},
|
||||
}
|
||||
container.RegisterContainerHandlerFactory("/", factory)
|
||||
|
||||
if driver == nil {
|
||||
driver = &stest.MockStorageDriver{}
|
||||
}
|
||||
|
||||
ret, err := NewContainerData("/container", driver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TestContainerUpdateSubcontainers(t *testing.T) {
|
||||
var handler *ctest.MockContainerHandler
|
||||
subcontainers := []info.ContainerReference{
|
||||
{Name: "/container/ee0103"},
|
||||
{Name: "/container/abcd"},
|
||||
{Name: "/container/something"},
|
||||
}
|
||||
cd := createContainerDataAndSetHandler(
|
||||
nil,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
h.On("ListContainers", container.LIST_SELF).Return(
|
||||
subcontainers,
|
||||
nil,
|
||||
)
|
||||
handler = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
err := cd.updateSubcontainers()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(cd.info.Subcontainers) != len(subcontainers) {
|
||||
t.Errorf("Received %v subcontainers, should be %v", len(cd.info.Subcontainers), len(subcontainers))
|
||||
}
|
||||
|
||||
for _, sub := range cd.info.Subcontainers {
|
||||
found := false
|
||||
for _, sub2 := range subcontainers {
|
||||
if sub.Name == sub2.Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Received unknown sub container %v", sub)
|
||||
}
|
||||
}
|
||||
|
||||
handler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestContainerUpdateSubcontainersWithError(t *testing.T) {
|
||||
var handler *ctest.MockContainerHandler
|
||||
cd := createContainerDataAndSetHandler(
|
||||
nil,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
h.On("ListContainers", container.LIST_SELF).Return(
|
||||
[]info.ContainerReference{},
|
||||
fmt.Errorf("some error"),
|
||||
)
|
||||
handler = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
err := cd.updateSubcontainers()
|
||||
if err == nil {
|
||||
t.Fatal("updateSubcontainers should return error")
|
||||
}
|
||||
if len(cd.info.Subcontainers) != 0 {
|
||||
t.Errorf("Received %v subcontainers, should be 0", len(cd.info.Subcontainers))
|
||||
}
|
||||
|
||||
handler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestContainerUpdateStats(t *testing.T) {
|
||||
var handler *ctest.MockContainerHandler
|
||||
var ref info.ContainerReference
|
||||
|
||||
driver := &stest.MockStorageDriver{}
|
||||
|
||||
statsList := itest.GenerateRandomStats(1, 4, 1*time.Second)
|
||||
stats := statsList[0]
|
||||
|
||||
cd := createContainerDataAndSetHandler(
|
||||
driver,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
h.On("GetStats").Return(
|
||||
stats,
|
||||
nil,
|
||||
)
|
||||
handler = h
|
||||
ref.Name = h.Name
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
driver.On("AddStats", ref, stats).Return(nil)
|
||||
|
||||
err := cd.updateStats()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestContainerUpdateSpec(t *testing.T) {
|
||||
var handler *ctest.MockContainerHandler
|
||||
spec := itest.GenerateRandomContainerSpec(4)
|
||||
cd := createContainerDataAndSetHandler(
|
||||
nil,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
h.On("GetSpec").Return(
|
||||
spec,
|
||||
nil,
|
||||
)
|
||||
handler = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
err := cd.updateSpec()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestContainerGetInfo(t *testing.T) {
|
||||
var handler *ctest.MockContainerHandler
|
||||
spec := itest.GenerateRandomContainerSpec(4)
|
||||
subcontainers := []info.ContainerReference{
|
||||
{Name: "/container/ee0103"},
|
||||
{Name: "/container/abcd"},
|
||||
{Name: "/container/something"},
|
||||
}
|
||||
aliases := []string{"a1", "a2"}
|
||||
cd := createContainerDataAndSetHandler(
|
||||
nil,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
h.On("GetSpec").Return(
|
||||
spec,
|
||||
nil,
|
||||
)
|
||||
h.On("ListContainers", container.LIST_SELF).Return(
|
||||
subcontainers,
|
||||
nil,
|
||||
)
|
||||
h.Aliases = aliases
|
||||
handler = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
info, err := cd.GetInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.AssertExpectations(t)
|
||||
|
||||
if len(info.Subcontainers) != len(subcontainers) {
|
||||
t.Errorf("Received %v subcontainers, should be %v", len(info.Subcontainers), len(subcontainers))
|
||||
}
|
||||
|
||||
for _, sub := range info.Subcontainers {
|
||||
found := false
|
||||
for _, sub2 := range subcontainers {
|
||||
if sub.Name == sub2.Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Received unknown sub container %v", sub)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(spec, info.Spec) {
|
||||
t.Errorf("received wrong container spec")
|
||||
}
|
||||
|
||||
if info.Name != handler.Name {
|
||||
t.Errorf("received wrong container name: received %v; should be %v", info.Name, handler.Name)
|
||||
}
|
||||
}
|
@@ -30,7 +30,7 @@ type Manager interface {
|
||||
Start() error
|
||||
|
||||
// Get information about a container.
|
||||
GetContainerInfo(containerName string) (*info.ContainerInfo, error)
|
||||
GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error)
|
||||
|
||||
// Get information about the machine.
|
||||
GetMachineInfo() (*info.MachineInfo, error)
|
||||
@@ -106,8 +106,8 @@ func (m *manager) Start() error {
|
||||
}
|
||||
|
||||
// Get a container by name.
|
||||
func (m *manager) GetContainerInfo(containerName string) (*info.ContainerInfo, error) {
|
||||
log.Printf("Get(%s)", containerName)
|
||||
func (m *manager) GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) {
|
||||
log.Printf("Get(%s); %+v", containerName, query)
|
||||
var cont *containerData
|
||||
var ok bool
|
||||
func() {
|
||||
@@ -130,21 +130,21 @@ func (m *manager) GetContainerInfo(containerName string) (*info.ContainerInfo, e
|
||||
var percentiles *info.ContainerStatsPercentiles
|
||||
var samples []*info.ContainerStatsSample
|
||||
var stats []*info.ContainerStats
|
||||
// TODO(monnand): These numbers should not be hard coded
|
||||
query = query.FillDefaults()
|
||||
percentiles, err = m.storageDriver.Percentiles(
|
||||
cinfo.Name,
|
||||
[]int{50, 80, 90, 99},
|
||||
[]int{50, 80, 90, 99},
|
||||
query.CpuUsagePercentiles,
|
||||
query.MemoryUsagePercentages,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
samples, err = m.storageDriver.Samples(cinfo.Name, 1024)
|
||||
samples, err = m.storageDriver.Samples(cinfo.Name, query.NumSamples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats, err = m.storageDriver.RecentStats(cinfo.Name, 1024)
|
||||
stats, err = m.storageDriver.RecentStats(cinfo.Name, query.NumStats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
253
third_party/src/github.com/google/cadvisor/manager/manager_test.go
vendored
Normal file
253
third_party/src/github.com/google/cadvisor/manager/manager_test.go
vendored
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2014 Google Inc. 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.
|
||||
|
||||
// Per-container manager.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/container"
|
||||
ctest "github.com/google/cadvisor/container/test"
|
||||
"github.com/google/cadvisor/info"
|
||||
itest "github.com/google/cadvisor/info/test"
|
||||
stest "github.com/google/cadvisor/storage/test"
|
||||
)
|
||||
|
||||
func createManagerAndAddContainers(
|
||||
driver *stest.MockStorageDriver,
|
||||
containers []string,
|
||||
f func(*ctest.MockContainerHandler),
|
||||
t *testing.T,
|
||||
) *manager {
|
||||
if driver == nil {
|
||||
driver = &stest.MockStorageDriver{}
|
||||
}
|
||||
factory := &ctest.FactoryForMockContainerHandler{
|
||||
Name: "factoryForManager",
|
||||
PrepareContainerHandlerFunc: func(name string, handler *ctest.MockContainerHandler) {
|
||||
handler.Name = name
|
||||
found := false
|
||||
for _, c := range containers {
|
||||
if c == name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Asked to create a container with name %v, which is unknown.", name)
|
||||
}
|
||||
f(handler)
|
||||
},
|
||||
}
|
||||
container.RegisterContainerHandlerFactory("/", factory)
|
||||
mif, err := New(driver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ret, ok := mif.(*manager); ok {
|
||||
for _, container := range containers {
|
||||
ret.containers[container], err = NewContainerData(container, driver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
t.Fatal("Wrong type")
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGetContainerInfo(t *testing.T) {
|
||||
containers := []string{
|
||||
"/c1",
|
||||
"/c2",
|
||||
}
|
||||
|
||||
query := &info.ContainerInfoRequest{
|
||||
NumStats: 256,
|
||||
NumSamples: 128,
|
||||
CpuUsagePercentiles: []int{10, 50, 90},
|
||||
MemoryUsagePercentages: []int{10, 80, 90},
|
||||
}
|
||||
|
||||
infosMap := make(map[string]*info.ContainerInfo, len(containers))
|
||||
handlerMap := make(map[string]*ctest.MockContainerHandler, len(containers))
|
||||
|
||||
for _, container := range containers {
|
||||
infosMap[container] = itest.GenerateRandomContainerInfo(container, 4, query, 1*time.Second)
|
||||
}
|
||||
|
||||
driver := &stest.MockStorageDriver{}
|
||||
m := createManagerAndAddContainers(
|
||||
driver,
|
||||
containers,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
cinfo := infosMap[h.Name]
|
||||
stats := cinfo.Stats
|
||||
samples := cinfo.Samples
|
||||
percentiles := cinfo.StatsPercentiles
|
||||
spec := cinfo.Spec
|
||||
driver.On(
|
||||
"Percentiles",
|
||||
h.Name,
|
||||
query.CpuUsagePercentiles,
|
||||
query.MemoryUsagePercentages,
|
||||
).Return(
|
||||
percentiles,
|
||||
nil,
|
||||
)
|
||||
|
||||
driver.On(
|
||||
"Samples",
|
||||
h.Name,
|
||||
query.NumSamples,
|
||||
).Return(
|
||||
samples,
|
||||
nil,
|
||||
)
|
||||
|
||||
driver.On(
|
||||
"RecentStats",
|
||||
h.Name,
|
||||
query.NumStats,
|
||||
).Return(
|
||||
stats,
|
||||
nil,
|
||||
)
|
||||
|
||||
h.On("ListContainers", container.LIST_SELF).Return(
|
||||
[]info.ContainerReference(nil),
|
||||
nil,
|
||||
)
|
||||
h.On("GetSpec").Return(
|
||||
spec,
|
||||
nil,
|
||||
)
|
||||
handlerMap[h.Name] = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
returnedInfos := make(map[string]*info.ContainerInfo, len(containers))
|
||||
|
||||
for _, container := range containers {
|
||||
cinfo, err := m.GetContainerInfo(container, query)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to get info for container %v: %v", container, err)
|
||||
}
|
||||
returnedInfos[container] = cinfo
|
||||
}
|
||||
|
||||
for container, handler := range handlerMap {
|
||||
handler.AssertExpectations(t)
|
||||
returned := returnedInfos[container]
|
||||
expected := infosMap[container]
|
||||
if !reflect.DeepEqual(returned, expected) {
|
||||
t.Errorf("returned unexpected info for container %v; returned %+v; expected %+v", container, returned, expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetContainerInfoWithDefaultValue(t *testing.T) {
|
||||
containers := []string{
|
||||
"/c1",
|
||||
"/c2",
|
||||
}
|
||||
|
||||
var query *info.ContainerInfoRequest
|
||||
query = query.FillDefaults()
|
||||
|
||||
infosMap := make(map[string]*info.ContainerInfo, len(containers))
|
||||
handlerMap := make(map[string]*ctest.MockContainerHandler, len(containers))
|
||||
|
||||
for _, container := range containers {
|
||||
infosMap[container] = itest.GenerateRandomContainerInfo(container, 4, query, 1*time.Second)
|
||||
}
|
||||
|
||||
driver := &stest.MockStorageDriver{}
|
||||
m := createManagerAndAddContainers(
|
||||
driver,
|
||||
containers,
|
||||
func(h *ctest.MockContainerHandler) {
|
||||
cinfo := infosMap[h.Name]
|
||||
stats := cinfo.Stats
|
||||
samples := cinfo.Samples
|
||||
percentiles := cinfo.StatsPercentiles
|
||||
spec := cinfo.Spec
|
||||
driver.On(
|
||||
"Percentiles",
|
||||
h.Name,
|
||||
query.CpuUsagePercentiles,
|
||||
query.MemoryUsagePercentages,
|
||||
).Return(
|
||||
percentiles,
|
||||
nil,
|
||||
)
|
||||
|
||||
driver.On(
|
||||
"Samples",
|
||||
h.Name,
|
||||
query.NumSamples,
|
||||
).Return(
|
||||
samples,
|
||||
nil,
|
||||
)
|
||||
|
||||
driver.On(
|
||||
"RecentStats",
|
||||
h.Name,
|
||||
query.NumStats,
|
||||
).Return(
|
||||
stats,
|
||||
nil,
|
||||
)
|
||||
|
||||
h.On("ListContainers", container.LIST_SELF).Return(
|
||||
[]info.ContainerReference(nil),
|
||||
nil,
|
||||
)
|
||||
h.On("GetSpec").Return(
|
||||
spec,
|
||||
nil,
|
||||
)
|
||||
handlerMap[h.Name] = h
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
returnedInfos := make(map[string]*info.ContainerInfo, len(containers))
|
||||
|
||||
for _, container := range containers {
|
||||
// nil should give us default values
|
||||
cinfo, err := m.GetContainerInfo(container, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to get info for container %v: %v", container, err)
|
||||
}
|
||||
returnedInfos[container] = cinfo
|
||||
}
|
||||
|
||||
for container, handler := range handlerMap {
|
||||
handler.AssertExpectations(t)
|
||||
returned := returnedInfos[container]
|
||||
expected := infosMap[container]
|
||||
if !reflect.DeepEqual(returned, expected) {
|
||||
t.Errorf("returned unexpected info for container %v; returned %+v; expected %+v", container, returned, expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -162,7 +162,11 @@ func ServerContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL)
|
||||
containerName := u.Path[len(ContainersPage)-1:]
|
||||
|
||||
// Get the container.
|
||||
cont, err := m.GetContainerInfo(containerName)
|
||||
reqParams := info.ContainerInfoRequest{
|
||||
NumStats: 60,
|
||||
NumSamples: 60,
|
||||
}
|
||||
cont, err := m.GetContainerInfo(containerName, &reqParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get container \"%s\" with error: %s", containerName, err)
|
||||
}
|
||||
|
@@ -128,7 +128,7 @@ function drawCpuPerCoreUsage(elementId, machineInfo, stats) {
|
||||
elements.push(cur.timestamp);
|
||||
for (var j = 0; j < machineInfo.num_cores; j++) {
|
||||
// TODO(vmarmol): This assumes we sample every second, use the timestamps.
|
||||
elements.push((cur.cpu.usage.per_cpu[j] - prev.cpu.usage.per_cpu[j]) / 1000000000);
|
||||
elements.push((cur.cpu.usage.per_cpu_usage[j] - prev.cpu.usage.per_cpu_usage[j]) / 1000000000);
|
||||
}
|
||||
data.push(elements);
|
||||
}
|
||||
|
498
third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb.go
vendored
Normal file
498
third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb.go
vendored
Normal file
@@ -0,0 +1,498 @@
|
||||
// Copyright 2014 Google Inc. 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 influxdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/info"
|
||||
"github.com/google/cadvisor/storage"
|
||||
"github.com/influxdb/influxdb-go"
|
||||
)
|
||||
|
||||
type influxdbStorage struct {
|
||||
client *influxdb.Client
|
||||
prevStats *info.ContainerStats
|
||||
machineName string
|
||||
tableName string
|
||||
windowLen time.Duration
|
||||
}
|
||||
|
||||
const (
|
||||
colTimestamp string = "timestamp"
|
||||
colMachineName string = "machine"
|
||||
colContainerName string = "container_name"
|
||||
colCpuCumulativeUsage string = "cpu_cumulative_usage"
|
||||
// Cumulative Cpu Usage in system mode
|
||||
colCpuCumulativeUsageSystem string = "cpu_cumulative_usage_system"
|
||||
// Cumulative Cpu Usage in user mode
|
||||
colCpuCumulativeUsageUser string = "cpu_cumulative_usage_user"
|
||||
// Memory Usage
|
||||
colMemoryUsage string = "memory_usage"
|
||||
// Working set size
|
||||
colMemoryWorkingSet string = "memory_working_set"
|
||||
// container page fault
|
||||
colMemoryContainerPgfault string = "memory_container_pgfault"
|
||||
// container major page fault
|
||||
colMemoryContainerPgmajfault string = "memory_container_pgmajfault"
|
||||
// hierarchical page fault
|
||||
colMemoryHierarchicalPgfault string = "memory_hierarchical_pgfault"
|
||||
// hierarchical major page fault
|
||||
colMemoryHierarchicalPgmajfault string = "memory_hierarchical_pgmajfault"
|
||||
// Cumulative per core usage
|
||||
colPerCoreCumulativeUsagePrefix string = "per_core_cumulative_usage_core_"
|
||||
// Optional: sample duration. Unit: Nanosecond.
|
||||
colSampleDuration string = "sample_duration"
|
||||
// Optional: Instant cpu usage
|
||||
colCpuInstantUsage string = "cpu_instant_usage"
|
||||
// Optional: Instant per core usage
|
||||
colPerCoreInstantUsagePrefix string = "per_core_instant_usage_core_"
|
||||
)
|
||||
|
||||
func (self *influxdbStorage) containerStatsToValues(
|
||||
ref info.ContainerReference,
|
||||
stats *info.ContainerStats,
|
||||
) (columns []string, values []interface{}) {
|
||||
|
||||
// Timestamp
|
||||
columns = append(columns, colTimestamp)
|
||||
values = append(values, stats.Timestamp.Format(time.RFC3339Nano))
|
||||
|
||||
// Machine name
|
||||
columns = append(columns, colMachineName)
|
||||
values = append(values, self.machineName)
|
||||
|
||||
// Container name
|
||||
columns = append(columns, colContainerName)
|
||||
values = append(values, ref.Name)
|
||||
|
||||
// Cumulative Cpu Usage
|
||||
columns = append(columns, colCpuCumulativeUsage)
|
||||
values = append(values, stats.Cpu.Usage.Total)
|
||||
|
||||
// Cumulative Cpu Usage in system mode
|
||||
columns = append(columns, colCpuCumulativeUsageSystem)
|
||||
values = append(values, stats.Cpu.Usage.System)
|
||||
|
||||
// Cumulative Cpu Usage in user mode
|
||||
columns = append(columns, colCpuCumulativeUsageUser)
|
||||
values = append(values, stats.Cpu.Usage.User)
|
||||
|
||||
// Memory Usage
|
||||
columns = append(columns, colMemoryUsage)
|
||||
values = append(values, stats.Memory.Usage)
|
||||
|
||||
// Working set size
|
||||
columns = append(columns, colMemoryWorkingSet)
|
||||
values = append(values, stats.Memory.WorkingSet)
|
||||
|
||||
// container page fault
|
||||
columns = append(columns, colMemoryContainerPgfault)
|
||||
values = append(values, stats.Memory.ContainerData.Pgfault)
|
||||
|
||||
// container major page fault
|
||||
columns = append(columns, colMemoryContainerPgmajfault)
|
||||
values = append(values, stats.Memory.ContainerData.Pgmajfault)
|
||||
|
||||
// hierarchical page fault
|
||||
columns = append(columns, colMemoryHierarchicalPgfault)
|
||||
values = append(values, stats.Memory.HierarchicalData.Pgfault)
|
||||
|
||||
// hierarchical major page fault
|
||||
columns = append(columns, colMemoryHierarchicalPgmajfault)
|
||||
values = append(values, stats.Memory.HierarchicalData.Pgmajfault)
|
||||
|
||||
// per cpu cumulative usage
|
||||
for i, u := range stats.Cpu.Usage.PerCpu {
|
||||
columns = append(columns, fmt.Sprintf("%v%v", colPerCoreCumulativeUsagePrefix, i))
|
||||
values = append(values, u)
|
||||
}
|
||||
|
||||
sample, err := info.NewSample(self.prevStats, stats)
|
||||
if err != nil || sample == nil {
|
||||
return columns, values
|
||||
}
|
||||
|
||||
// Optional: sample duration. Unit: Nanosecond.
|
||||
columns = append(columns, colSampleDuration)
|
||||
values = append(values, sample.Duration.String())
|
||||
|
||||
// Optional: Instant cpu usage
|
||||
columns = append(columns, colCpuInstantUsage)
|
||||
values = append(values, sample.Cpu.Usage)
|
||||
|
||||
// Optional: Instant per core usage
|
||||
for i, u := range sample.Cpu.PerCpuUsage {
|
||||
columns = append(columns, fmt.Sprintf("%v%v", colPerCoreInstantUsagePrefix, i))
|
||||
values = append(values, u)
|
||||
}
|
||||
|
||||
return columns, values
|
||||
}
|
||||
|
||||
func convertToUint64(v interface{}) (uint64, error) {
|
||||
if v == nil {
|
||||
return 0, nil
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case uint64:
|
||||
return x, nil
|
||||
case int:
|
||||
if x < 0 {
|
||||
return 0, fmt.Errorf("negative value: %v", x)
|
||||
}
|
||||
return uint64(x), nil
|
||||
case int32:
|
||||
if x < 0 {
|
||||
return 0, fmt.Errorf("negative value: %v", x)
|
||||
}
|
||||
return uint64(x), nil
|
||||
case int64:
|
||||
if x < 0 {
|
||||
return 0, fmt.Errorf("negative value: %v", x)
|
||||
}
|
||||
return uint64(x), nil
|
||||
case float64:
|
||||
if x < 0 {
|
||||
return 0, fmt.Errorf("negative value: %v", x)
|
||||
}
|
||||
return uint64(x), nil
|
||||
case uint32:
|
||||
return uint64(x), nil
|
||||
}
|
||||
return 0, fmt.Errorf("Unknown type")
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) valuesToContainerStats(columns []string, values []interface{}) (*info.ContainerStats, error) {
|
||||
stats := &info.ContainerStats{
|
||||
Cpu: &info.CpuStats{},
|
||||
Memory: &info.MemoryStats{},
|
||||
}
|
||||
perCoreUsage := make(map[int]uint64, 32)
|
||||
var err error
|
||||
for i, col := range columns {
|
||||
v := values[i]
|
||||
switch {
|
||||
case col == colTimestamp:
|
||||
if str, ok := v.(string); ok {
|
||||
stats.Timestamp, err = time.Parse(time.RFC3339Nano, str)
|
||||
}
|
||||
case col == colMachineName:
|
||||
if m, ok := v.(string); ok {
|
||||
if m != self.machineName {
|
||||
return nil, fmt.Errorf("different machine")
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("machine name field is not a string: %v", v)
|
||||
}
|
||||
// Cumulative Cpu Usage
|
||||
case col == colCpuCumulativeUsage:
|
||||
stats.Cpu.Usage.Total, err = convertToUint64(v)
|
||||
// Cumulative Cpu used by the system
|
||||
case col == colCpuCumulativeUsageSystem:
|
||||
stats.Cpu.Usage.System, err = convertToUint64(v)
|
||||
// Cumulative Cpu Usage in user mode
|
||||
case col == colCpuCumulativeUsageUser:
|
||||
stats.Cpu.Usage.User, err = convertToUint64(v)
|
||||
// Memory Usage
|
||||
case col == colMemoryUsage:
|
||||
stats.Memory.Usage, err = convertToUint64(v)
|
||||
// Working set size
|
||||
case col == colMemoryWorkingSet:
|
||||
stats.Memory.WorkingSet, err = convertToUint64(v)
|
||||
// container page fault
|
||||
case col == colMemoryContainerPgfault:
|
||||
stats.Memory.ContainerData.Pgfault, err = convertToUint64(v)
|
||||
// container major page fault
|
||||
case col == colMemoryContainerPgmajfault:
|
||||
stats.Memory.ContainerData.Pgmajfault, err = convertToUint64(v)
|
||||
// hierarchical page fault
|
||||
case col == colMemoryHierarchicalPgfault:
|
||||
stats.Memory.HierarchicalData.Pgfault, err = convertToUint64(v)
|
||||
// hierarchical major page fault
|
||||
case col == colMemoryHierarchicalPgmajfault:
|
||||
stats.Memory.HierarchicalData.Pgmajfault, err = convertToUint64(v)
|
||||
case strings.HasPrefix(col, colPerCoreCumulativeUsagePrefix):
|
||||
idxStr := col[len(colPerCoreCumulativeUsagePrefix):]
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
perCoreUsage[idx], err = convertToUint64(v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("column %v has invalid value %v: %v", col, v, err)
|
||||
}
|
||||
}
|
||||
stats.Cpu.Usage.PerCpu = make([]uint64, len(perCoreUsage))
|
||||
for idx, usage := range perCoreUsage {
|
||||
stats.Cpu.Usage.PerCpu[idx] = usage
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) valuesToContainerSample(columns []string, values []interface{}) (*info.ContainerStatsSample, error) {
|
||||
sample := &info.ContainerStatsSample{}
|
||||
perCoreUsage := make(map[int]uint64, 32)
|
||||
var err error
|
||||
for i, col := range columns {
|
||||
v := values[i]
|
||||
switch {
|
||||
case col == colTimestamp:
|
||||
if str, ok := v.(string); ok {
|
||||
sample.Timestamp, err = time.Parse(time.RFC3339Nano, str)
|
||||
}
|
||||
case col == colMachineName:
|
||||
if m, ok := v.(string); ok {
|
||||
if m != self.machineName {
|
||||
return nil, fmt.Errorf("different machine")
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("machine name field is not a string: %v", v)
|
||||
}
|
||||
// Memory Usage
|
||||
case col == colMemoryUsage:
|
||||
sample.Memory.Usage, err = convertToUint64(v)
|
||||
// sample duration. Unit: Nanosecond.
|
||||
case col == colSampleDuration:
|
||||
if v == nil {
|
||||
// this record does not have sample_duration, so it's the first stats.
|
||||
return nil, nil
|
||||
}
|
||||
sample.Duration, err = time.ParseDuration(v.(string))
|
||||
// Instant cpu usage
|
||||
case col == colCpuInstantUsage:
|
||||
sample.Cpu.Usage, err = convertToUint64(v)
|
||||
case strings.HasPrefix(col, colPerCoreInstantUsagePrefix):
|
||||
idxStr := col[len(colPerCoreInstantUsagePrefix):]
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
perCoreUsage[idx], err = convertToUint64(v)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("column %v has invalid value %v: %v", col, v, err)
|
||||
}
|
||||
}
|
||||
sample.Cpu.PerCpuUsage = make([]uint64, len(perCoreUsage))
|
||||
for idx, usage := range perCoreUsage {
|
||||
sample.Cpu.PerCpuUsage[idx] = usage
|
||||
}
|
||||
if sample.Duration.Nanoseconds() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return sample, nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error {
|
||||
series := &influxdb.Series{
|
||||
Name: self.tableName,
|
||||
// There's only one point for each stats
|
||||
Points: make([][]interface{}, 1),
|
||||
}
|
||||
if stats == nil || stats.Cpu == nil || stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
series.Columns, series.Points[0] = self.containerStatsToValues(ref, stats)
|
||||
|
||||
self.prevStats = stats.Copy(self.prevStats)
|
||||
err := self.client.WriteSeries([]*influxdb.Series{series})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error) {
|
||||
// TODO(dengnan): select only columns that we need
|
||||
// TODO(dengnan): escape names
|
||||
query := fmt.Sprintf("select * from %v where %v='%v' and %v='%v'", self.tableName, colContainerName, containerName, colMachineName, self.machineName)
|
||||
if numStats > 0 {
|
||||
query = fmt.Sprintf("%v limit %v", query, numStats)
|
||||
}
|
||||
series, err := self.client.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statsList := make([]*info.ContainerStats, 0, len(series))
|
||||
// By default, influxDB returns data in time descending order.
|
||||
// RecentStats() requires stats in time increasing order,
|
||||
// so we need to go through from the last one to the first one.
|
||||
for i := len(series) - 1; i >= 0; i-- {
|
||||
s := series[i]
|
||||
for j := len(s.Points) - 1; j >= 0; j-- {
|
||||
values := s.Points[j]
|
||||
stats, err := self.valuesToContainerStats(s.Columns, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stats == nil {
|
||||
continue
|
||||
}
|
||||
statsList = append(statsList, stats)
|
||||
}
|
||||
}
|
||||
return statsList, nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error) {
|
||||
// TODO(dengnan): select only columns that we need
|
||||
// TODO(dengnan): escape names
|
||||
query := fmt.Sprintf("select * from %v where %v='%v' and %v='%v'", self.tableName, colContainerName, containerName, colMachineName, self.machineName)
|
||||
if numSamples > 0 {
|
||||
query = fmt.Sprintf("%v limit %v", query, numSamples)
|
||||
}
|
||||
series, err := self.client.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sampleList := make([]*info.ContainerStatsSample, 0, len(series))
|
||||
for i := len(series) - 1; i >= 0; i-- {
|
||||
s := series[i]
|
||||
for j := len(s.Points) - 1; j >= 0; j-- {
|
||||
values := s.Points[j]
|
||||
sample, err := self.valuesToContainerSample(s.Columns, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sample == nil {
|
||||
continue
|
||||
}
|
||||
sampleList = append(sampleList, sample)
|
||||
}
|
||||
}
|
||||
return sampleList, nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) Close() error {
|
||||
self.client = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *influxdbStorage) Percentiles(
|
||||
containerName string,
|
||||
cpuUsagePercentiles []int,
|
||||
memUsagePercentiles []int,
|
||||
) (*info.ContainerStatsPercentiles, error) {
|
||||
selectedCol := make([]string, 0, len(cpuUsagePercentiles)+len(memUsagePercentiles)+1)
|
||||
|
||||
selectedCol = append(selectedCol, fmt.Sprintf("max(%v)", colMemoryUsage))
|
||||
for _, p := range cpuUsagePercentiles {
|
||||
selectedCol = append(selectedCol, fmt.Sprintf("percentile(%v, %v)", colCpuInstantUsage, p))
|
||||
}
|
||||
for _, p := range memUsagePercentiles {
|
||||
selectedCol = append(selectedCol, fmt.Sprintf("percentile(%v, %v)", colMemoryUsage, p))
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("select %v from %v where %v='%v' and %v='%v' and time > now() - %v",
|
||||
strings.Join(selectedCol, ","),
|
||||
self.tableName,
|
||||
colContainerName,
|
||||
containerName,
|
||||
colMachineName,
|
||||
self.machineName,
|
||||
fmt.Sprintf("%vs", self.windowLen.Seconds()),
|
||||
)
|
||||
series, err := self.client.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(series) != 1 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(series[0].Points) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
point := series[0].Points[0]
|
||||
|
||||
ret := new(info.ContainerStatsPercentiles)
|
||||
ret.MaxMemoryUsage, err = convertToUint64(point[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid max memory usage: %v", err)
|
||||
}
|
||||
retrievedCpuPercentiles := point[2 : 2+len(cpuUsagePercentiles)]
|
||||
for i, p := range cpuUsagePercentiles {
|
||||
v, err := convertToUint64(retrievedCpuPercentiles[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cpu usage: %v", err)
|
||||
}
|
||||
ret.CpuUsagePercentiles = append(
|
||||
ret.CpuUsagePercentiles,
|
||||
info.Percentile{
|
||||
Percentage: p,
|
||||
Value: v,
|
||||
},
|
||||
)
|
||||
}
|
||||
retrievedMemoryPercentiles := point[2+len(cpuUsagePercentiles):]
|
||||
for i, p := range memUsagePercentiles {
|
||||
v, err := convertToUint64(retrievedMemoryPercentiles[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid memory usage: %v", err)
|
||||
}
|
||||
ret.MemoryUsagePercentiles = append(
|
||||
ret.MemoryUsagePercentiles,
|
||||
info.Percentile{
|
||||
Percentage: p,
|
||||
Value: v,
|
||||
},
|
||||
)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// machineName: A unique identifier to identify the host that current cAdvisor
|
||||
// instance is running on.
|
||||
// influxdbHost: The host which runs influxdb.
|
||||
// percentilesDuration: Time window which will be considered when calls Percentiles()
|
||||
func New(machineName,
|
||||
tablename,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
influxdbHost string,
|
||||
isSecure bool,
|
||||
percentilesDuration time.Duration,
|
||||
) (storage.StorageDriver, error) {
|
||||
config := &influxdb.ClientConfig{
|
||||
Host: influxdbHost,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Database: database,
|
||||
IsSecure: isSecure,
|
||||
}
|
||||
client, err := influxdb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO(monnand): With go 1.3, we cannot compress data now.
|
||||
client.DisableCompression()
|
||||
if percentilesDuration.Seconds() < 1.0 {
|
||||
percentilesDuration = 5 * time.Minute
|
||||
}
|
||||
|
||||
ret := &influxdbStorage{
|
||||
client: client,
|
||||
windowLen: percentilesDuration,
|
||||
machineName: machineName,
|
||||
tableName: tablename,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
136
third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb_test.go
vendored
Normal file
136
third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb_test.go
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2014 Google Inc. 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 influxdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/storage"
|
||||
"github.com/google/cadvisor/storage/test"
|
||||
"github.com/influxdb/influxdb-go"
|
||||
)
|
||||
|
||||
func runStorageTest(f func(storage.StorageDriver, *testing.T), t *testing.T) {
|
||||
machineName := "machineA"
|
||||
tablename := "t"
|
||||
database := "cadvisor"
|
||||
username := "root"
|
||||
password := "root"
|
||||
hostname := "localhost:8086"
|
||||
percentilesDuration := 10 * time.Minute
|
||||
rootConfig := &influxdb.ClientConfig{
|
||||
Host: hostname,
|
||||
Username: username,
|
||||
Password: password,
|
||||
IsSecure: false,
|
||||
}
|
||||
rootClient, err := influxdb.NewClient(rootConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// create the data base first.
|
||||
rootClient.CreateDatabase(database)
|
||||
config := &influxdb.ClientConfig{
|
||||
Host: hostname,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Database: database,
|
||||
IsSecure: false,
|
||||
}
|
||||
client, err := influxdb.NewClient(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client.DisableCompression()
|
||||
deleteAll := fmt.Sprintf("drop series %v", tablename)
|
||||
_, err = client.Query(deleteAll)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// delete all data by the end of the call
|
||||
defer client.Query(deleteAll)
|
||||
|
||||
driver, err := New(machineName,
|
||||
tablename,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
hostname,
|
||||
false,
|
||||
percentilesDuration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// generate another container's data on same machine.
|
||||
test.StorageDriverFillRandomStatsFunc("containerOnSameMachine", 100, driver, t)
|
||||
|
||||
// generate another container's data on another machine.
|
||||
driverForAnotherMachine, err := New("machineB",
|
||||
tablename,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
hostname,
|
||||
false,
|
||||
percentilesDuration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer driverForAnotherMachine.Close()
|
||||
test.StorageDriverFillRandomStatsFunc("containerOnAnotherMachine", 100, driverForAnotherMachine, t)
|
||||
f(driver, t)
|
||||
}
|
||||
|
||||
func TestSampleCpuUsage(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestSampleCpuUsage, t)
|
||||
}
|
||||
|
||||
func TestRetrievePartialRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestRetrievePartialRecentStats, t)
|
||||
}
|
||||
|
||||
func TestSamplesWithoutSample(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestSamplesWithoutSample, t)
|
||||
}
|
||||
|
||||
func TestRetrieveAllRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestRetrieveAllRecentStats, t)
|
||||
}
|
||||
|
||||
func TestNoRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestNoRecentStats, t)
|
||||
}
|
||||
|
||||
func TestNoSamples(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestNoSamples, t)
|
||||
}
|
||||
|
||||
func TestPercentiles(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestPercentiles, t)
|
||||
}
|
||||
|
||||
func TestMaxMemoryUsage(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestMaxMemoryUsage, t)
|
||||
}
|
||||
|
||||
func TestPercentilesWithoutSample(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestPercentilesWithoutSample, t)
|
||||
}
|
||||
|
||||
func TestPercentilesWithoutStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestPercentilesWithoutStats, t)
|
||||
}
|
@@ -41,20 +41,7 @@ func (self *containerStorage) updatePrevStats(stats *info.ContainerStats) {
|
||||
self.prevStats = nil
|
||||
return
|
||||
}
|
||||
if self.prevStats == nil {
|
||||
self.prevStats = &info.ContainerStats{
|
||||
Cpu: &info.CpuStats{},
|
||||
Memory: &info.MemoryStats{},
|
||||
}
|
||||
}
|
||||
// make a deep copy.
|
||||
self.prevStats.Timestamp = stats.Timestamp
|
||||
*self.prevStats.Cpu = *stats.Cpu
|
||||
self.prevStats.Cpu.Usage.PerCpu = make([]uint64, len(stats.Cpu.Usage.PerCpu))
|
||||
for i, perCpu := range stats.Cpu.Usage.PerCpu {
|
||||
self.prevStats.Cpu.Usage.PerCpu[i] = perCpu
|
||||
}
|
||||
*self.prevStats.Memory = *stats.Memory
|
||||
self.prevStats = stats.Copy(self.prevStats)
|
||||
}
|
||||
|
||||
func (self *containerStorage) AddStats(stats *info.ContainerStats) error {
|
||||
@@ -75,9 +62,9 @@ func (self *containerStorage) AddStats(stats *info.ContainerStats) error {
|
||||
}
|
||||
}
|
||||
if self.recentStats.Len() >= self.maxNumStats {
|
||||
self.recentStats.Remove(self.recentStats.Front())
|
||||
self.recentStats.Remove(self.recentStats.Back())
|
||||
}
|
||||
self.recentStats.PushBack(stats)
|
||||
self.recentStats.PushFront(stats)
|
||||
self.updatePrevStats(stats)
|
||||
return nil
|
||||
}
|
||||
@@ -88,18 +75,25 @@ func (self *containerStorage) RecentStats(numStats int) ([]*info.ContainerStats,
|
||||
if self.recentStats.Len() < numStats || numStats < 0 {
|
||||
numStats = self.recentStats.Len()
|
||||
}
|
||||
ret := make([]*info.ContainerStats, 0, numStats)
|
||||
|
||||
// Stats in the recentStats list are stored in reverse chronological
|
||||
// order, i.e. most recent stats is in the front.
|
||||
// numStats will always <= recentStats.Len() so that there will be
|
||||
// always at least numStats available stats to retrieve. We traverse
|
||||
// the recentStats list from its head and fill the ret slice in
|
||||
// reverse order so that the returned slice will be in chronological
|
||||
// order. The order of the returned slice is not specified by the
|
||||
// StorageDriver interface, so it is not necessary for other storage
|
||||
// drivers to return the slice in the same order.
|
||||
ret := make([]*info.ContainerStats, numStats)
|
||||
e := self.recentStats.Front()
|
||||
for i := 0; i < numStats; i++ {
|
||||
for i := numStats - 1; i >= 0; i-- {
|
||||
data, ok := e.Value.(*info.ContainerStats)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("The %vth element is not a ContainerStats", i)
|
||||
}
|
||||
ret = append(ret, data)
|
||||
ret[i] = data
|
||||
e = e.Next()
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -162,23 +156,34 @@ type InMemoryStorage struct {
|
||||
func (self *InMemoryStorage) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error {
|
||||
var cstore *containerStorage
|
||||
var ok bool
|
||||
self.lock.Lock()
|
||||
if cstore, ok = self.containerStorageMap[ref.Name]; !ok {
|
||||
cstore = newContainerStore(ref, self.maxNumSamples, self.maxNumStats)
|
||||
self.containerStorageMap[ref.Name] = cstore
|
||||
}
|
||||
self.lock.Unlock()
|
||||
|
||||
func() {
|
||||
self.lock.Lock()
|
||||
defer self.lock.Unlock()
|
||||
if cstore, ok = self.containerStorageMap[ref.Name]; !ok {
|
||||
cstore = newContainerStore(ref, self.maxNumSamples, self.maxNumStats)
|
||||
self.containerStorageMap[ref.Name] = cstore
|
||||
}
|
||||
}()
|
||||
return cstore.AddStats(stats)
|
||||
}
|
||||
|
||||
func (self *InMemoryStorage) Samples(name string, numSamples int) ([]*info.ContainerStatsSample, error) {
|
||||
var cstore *containerStorage
|
||||
var ok bool
|
||||
self.lock.RLock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return nil, fmt.Errorf("unable to find data for container %v", name)
|
||||
|
||||
err := func() error {
|
||||
self.lock.RLock()
|
||||
defer self.lock.RUnlock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return fmt.Errorf("unable to find data for container %v", name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self.lock.RUnlock()
|
||||
|
||||
return cstore.Samples(numSamples)
|
||||
}
|
||||
@@ -186,11 +191,17 @@ func (self *InMemoryStorage) Samples(name string, numSamples int) ([]*info.Conta
|
||||
func (self *InMemoryStorage) RecentStats(name string, numStats int) ([]*info.ContainerStats, error) {
|
||||
var cstore *containerStorage
|
||||
var ok bool
|
||||
self.lock.RLock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return nil, fmt.Errorf("unable to find data for container %v", name)
|
||||
err := func() error {
|
||||
self.lock.RLock()
|
||||
defer self.lock.RUnlock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return fmt.Errorf("unable to find data for container %v", name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self.lock.RUnlock()
|
||||
|
||||
return cstore.RecentStats(numStats)
|
||||
}
|
||||
@@ -198,11 +209,17 @@ func (self *InMemoryStorage) RecentStats(name string, numStats int) ([]*info.Con
|
||||
func (self *InMemoryStorage) Percentiles(name string, cpuPercentiles, memPercentiles []int) (*info.ContainerStatsPercentiles, error) {
|
||||
var cstore *containerStorage
|
||||
var ok bool
|
||||
self.lock.RLock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return nil, fmt.Errorf("unable to find data for container %v", name)
|
||||
err := func() error {
|
||||
self.lock.RLock()
|
||||
defer self.lock.RUnlock()
|
||||
if cstore, ok = self.containerStorageMap[name]; !ok {
|
||||
return fmt.Errorf("unable to find data for container %v", name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self.lock.RUnlock()
|
||||
|
||||
return cstore.Percentiles(cpuPercentiles, memPercentiles)
|
||||
}
|
||||
|
@@ -44,6 +44,32 @@ func TestSamplesWithoutSample(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestSamplesWithoutSample, t)
|
||||
}
|
||||
|
||||
func TestPercentilessWithoutSample(t *testing.T) {
|
||||
func TestPercentilesWithoutSample(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestPercentilesWithoutSample, t)
|
||||
}
|
||||
|
||||
func TestPercentiles(t *testing.T) {
|
||||
N := 100
|
||||
driver := New(N, N)
|
||||
test.StorageDriverTestPercentiles(driver, t)
|
||||
}
|
||||
|
||||
func TestRetrievePartialRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestRetrievePartialRecentStats, t)
|
||||
}
|
||||
|
||||
func TestRetrieveAllRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestRetrieveAllRecentStats, t)
|
||||
}
|
||||
|
||||
func TestNoRecentStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestNoRecentStats, t)
|
||||
}
|
||||
|
||||
func TestNoSamples(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestNoSamples, t)
|
||||
}
|
||||
|
||||
func TestPercentilesWithoutStats(t *testing.T) {
|
||||
runStorageTest(test.StorageDriverTestPercentilesWithoutStats, t)
|
||||
}
|
||||
|
@@ -21,7 +21,9 @@ type StorageDriver interface {
|
||||
|
||||
// Read most recent stats. numStats indicates max number of stats
|
||||
// returned. The returned stats must be consecutive observed stats. If
|
||||
// numStats < 0, then return all stats stored in the storage.
|
||||
// numStats < 0, then return all stats stored in the storage. The
|
||||
// returned stats should be sorted in time increasing order, i.e. Most
|
||||
// recent stats should be the last.
|
||||
RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error)
|
||||
|
||||
// Read the specified percentiles of CPU and memory usage of the container.
|
||||
@@ -31,7 +33,7 @@ type StorageDriver interface {
|
||||
// Returns samples of the container stats. If numSamples < 0, then
|
||||
// the number of returned samples is implementation defined. Otherwise, the driver
|
||||
// should return at most numSamples samples.
|
||||
Samples(containername string, numSamples int) ([]*info.ContainerStatsSample, error)
|
||||
Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error)
|
||||
|
||||
// Close will clear the state of the storage driver. The elements
|
||||
// stored in the underlying storage may or may not be deleted depending
|
||||
|
57
third_party/src/github.com/google/cadvisor/storage/test/mock.go
vendored
Normal file
57
third_party/src/github.com/google/cadvisor/storage/test/mock.go
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2014 Google Inc. 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 test
|
||||
|
||||
import (
|
||||
"github.com/google/cadvisor/info"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockStorageDriver struct {
|
||||
mock.Mock
|
||||
MockCloseMethod bool
|
||||
}
|
||||
|
||||
func (self *MockStorageDriver) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error {
|
||||
args := self.Called(ref, stats)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (self *MockStorageDriver) RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error) {
|
||||
args := self.Called(containerName, numStats)
|
||||
return args.Get(0).([]*info.ContainerStats), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockStorageDriver) Percentiles(
|
||||
containerName string,
|
||||
cpuUsagePercentiles []int,
|
||||
memUsagePercentiles []int,
|
||||
) (*info.ContainerStatsPercentiles, error) {
|
||||
args := self.Called(containerName, cpuUsagePercentiles, memUsagePercentiles)
|
||||
return args.Get(0).(*info.ContainerStatsPercentiles), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockStorageDriver) Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error) {
|
||||
args := self.Called(containerName, numSamples)
|
||||
return args.Get(0).([]*info.ContainerStatsSample), args.Error(1)
|
||||
}
|
||||
|
||||
func (self *MockStorageDriver) Close() error {
|
||||
if self.MockCloseMethod {
|
||||
args := self.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -16,6 +16,7 @@ package test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -43,6 +44,7 @@ func buildTrace(cpu, mem []uint64, duration time.Duration) []*info.ContainerStat
|
||||
stats.Cpu.Usage.Total = cpuTotalUsage
|
||||
stats.Cpu.Usage.User = stats.Cpu.Usage.Total
|
||||
stats.Cpu.Usage.System = 0
|
||||
stats.Cpu.Usage.PerCpu = []uint64{cpuTotalUsage}
|
||||
|
||||
stats.Memory.Usage = mem[i]
|
||||
|
||||
@@ -51,10 +53,133 @@ func buildTrace(cpu, mem []uint64, duration time.Duration) []*info.ContainerStat
|
||||
return ret
|
||||
}
|
||||
|
||||
// The underlying driver must be able to hold more than 10 samples.
|
||||
func timeEq(t1, t2 time.Time, tolerance time.Duration) bool {
|
||||
// t1 should not be later than t2
|
||||
if t1.After(t2) {
|
||||
t1, t2 = t2, t1
|
||||
}
|
||||
diff := t2.Sub(t1)
|
||||
if diff <= tolerance {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func durationEq(a, b time.Duration, tolerance time.Duration) bool {
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
diff := a - b
|
||||
if diff <= tolerance {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
// 10ms, i.e. 0.01s
|
||||
timePrecision time.Duration = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
// This function is useful because we do not require precise time
|
||||
// representation.
|
||||
func statsEq(a, b *info.ContainerStats) bool {
|
||||
if !timeEq(a.Timestamp, b.Timestamp, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Cpu, b.Cpu) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Memory, b.Memory) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// This function is useful because we do not require precise time
|
||||
// representation.
|
||||
func sampleEq(a, b *info.ContainerStatsSample) bool {
|
||||
if !timeEq(a.Timestamp, b.Timestamp, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !durationEq(a.Duration, b.Duration, timePrecision) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Cpu, b.Cpu) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(a.Memory, b.Memory) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func samplesInTrace(samples []*info.ContainerStatsSample, cpuTrace, memTrace []uint64, samplePeriod time.Duration, t *testing.T) {
|
||||
for _, sample := range samples {
|
||||
if sample.Duration != samplePeriod {
|
||||
t.Errorf("sample duration is %v, not %v", sample.Duration, samplePeriod)
|
||||
}
|
||||
cpuUsage := sample.Cpu.Usage
|
||||
memUsage := sample.Memory.Usage
|
||||
found := false
|
||||
for _, u := range cpuTrace {
|
||||
if u == cpuUsage {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("unable to find cpu usage %v", cpuUsage)
|
||||
}
|
||||
found = false
|
||||
for _, u := range memTrace {
|
||||
if u == memUsage {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("unable to find mem usage %v", memUsage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function will generate random stats and write
|
||||
// them into the storage. The function will not close the driver
|
||||
func StorageDriverFillRandomStatsFunc(
|
||||
containerName string,
|
||||
N int,
|
||||
driver storage.StorageDriver,
|
||||
t *testing.T,
|
||||
) {
|
||||
cpuTrace := make([]uint64, 0, N)
|
||||
memTrace := make([]uint64, 0, N)
|
||||
|
||||
// We need N+1 observations to get N samples
|
||||
for i := 0; i < N+1; i++ {
|
||||
cpuTrace = append(cpuTrace, uint64(rand.Intn(1000)))
|
||||
memTrace = append(memTrace, uint64(rand.Intn(1000)))
|
||||
}
|
||||
|
||||
samplePeriod := 1 * time.Second
|
||||
|
||||
ref := info.ContainerReference{
|
||||
Name: containerName,
|
||||
}
|
||||
|
||||
trace := buildTrace(cpuTrace, memTrace, samplePeriod)
|
||||
|
||||
for _, stats := range trace {
|
||||
err := driver.AddStats(ref, stats)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add stats: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestSampleCpuUsage(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
N := 10
|
||||
N := 100
|
||||
cpuTrace := make([]uint64, 0, N)
|
||||
memTrace := make([]uint64, 0, N)
|
||||
|
||||
@@ -73,28 +198,37 @@ func StorageDriverTestSampleCpuUsage(driver storage.StorageDriver, t *testing.T)
|
||||
trace := buildTrace(cpuTrace, memTrace, samplePeriod)
|
||||
|
||||
for _, stats := range trace {
|
||||
driver.AddStats(ref, stats)
|
||||
err := driver.AddStats(ref, stats)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add stats: %v", err)
|
||||
}
|
||||
// set the trace to something else. The stats stored in the
|
||||
// storage should not be affected.
|
||||
stats.Cpu.Usage.Total = 0
|
||||
stats.Cpu.Usage.System = 0
|
||||
stats.Cpu.Usage.User = 0
|
||||
}
|
||||
|
||||
samples, err := driver.Samples(ref.Name, N)
|
||||
if err != nil {
|
||||
t.Errorf("unable to sample stats: %v", err)
|
||||
}
|
||||
for _, sample := range samples {
|
||||
if sample.Duration != samplePeriod {
|
||||
t.Errorf("sample duration is %v, not %v", sample.Duration, samplePeriod)
|
||||
}
|
||||
cpuUsage := sample.Cpu.Usage
|
||||
found := false
|
||||
for _, u := range cpuTrace {
|
||||
if u == cpuUsage {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("unable to find cpu usage %v", cpuUsage)
|
||||
}
|
||||
if len(samples) == 0 {
|
||||
t.Fatal("should at least store one sample")
|
||||
}
|
||||
samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t)
|
||||
|
||||
samples, err = driver.Samples(ref.Name, -1)
|
||||
if err != nil {
|
||||
t.Errorf("unable to sample stats: %v", err)
|
||||
}
|
||||
samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t)
|
||||
|
||||
samples, err = driver.Samples(ref.Name, N-5)
|
||||
if err != nil {
|
||||
t.Errorf("unable to sample stats: %v", err)
|
||||
}
|
||||
samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t)
|
||||
}
|
||||
|
||||
func StorageDriverTestMaxMemoryUsage(driver storage.StorageDriver, t *testing.T) {
|
||||
@@ -114,7 +248,16 @@ func StorageDriverTestMaxMemoryUsage(driver storage.StorageDriver, t *testing.T)
|
||||
trace := buildTrace(cpuTrace, memTrace, 1*time.Second)
|
||||
|
||||
for _, stats := range trace {
|
||||
driver.AddStats(ref, stats)
|
||||
err := driver.AddStats(ref, stats)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add stats: %v", err)
|
||||
}
|
||||
// set the trace to something else. The stats stored in the
|
||||
// storage should not be affected.
|
||||
stats.Cpu.Usage.Total = 0
|
||||
stats.Cpu.Usage.System = 0
|
||||
stats.Cpu.Usage.User = 0
|
||||
stats.Memory.Usage = 0
|
||||
}
|
||||
|
||||
percentiles, err := driver.Percentiles(ref.Name, []int{50}, []int{50})
|
||||
@@ -168,3 +311,174 @@ func StorageDriverTestPercentilesWithoutSample(driver storage.StorageDriver, t *
|
||||
t.Errorf("There should be no percentiles")
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestPercentiles(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
N := 100
|
||||
cpuTrace := make([]uint64, N)
|
||||
memTrace := make([]uint64, N)
|
||||
for i := 1; i < N+1; i++ {
|
||||
cpuTrace[i-1] = uint64(i)
|
||||
memTrace[i-1] = uint64(i)
|
||||
}
|
||||
|
||||
trace := buildTrace(cpuTrace, memTrace, 1*time.Second)
|
||||
|
||||
ref := info.ContainerReference{
|
||||
Name: "container",
|
||||
}
|
||||
for _, stats := range trace {
|
||||
driver.AddStats(ref, stats)
|
||||
}
|
||||
percentages := []int{
|
||||
80,
|
||||
90,
|
||||
50,
|
||||
}
|
||||
percentiles, err := driver.Percentiles(ref.Name, percentages, percentages)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, x := range percentiles.CpuUsagePercentiles {
|
||||
for _, y := range percentiles.CpuUsagePercentiles {
|
||||
// lower percentage, smaller value
|
||||
if x.Percentage < y.Percentage && x.Value > y.Value {
|
||||
t.Errorf("%v percent is %v; while %v percent is %v",
|
||||
x.Percentage, x.Value, y.Percentage, y.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, x := range percentiles.MemoryUsagePercentiles {
|
||||
for _, y := range percentiles.MemoryUsagePercentiles {
|
||||
if x.Percentage < y.Percentage && x.Value > y.Value {
|
||||
t.Errorf("%v percent is %v; while %v percent is %v",
|
||||
x.Percentage, x.Value, y.Percentage, y.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestRetrievePartialRecentStats(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
N := 100
|
||||
memTrace := make([]uint64, N)
|
||||
cpuTrace := make([]uint64, N)
|
||||
for i := 0; i < N; i++ {
|
||||
memTrace[i] = uint64(i + 1)
|
||||
cpuTrace[i] = uint64(1)
|
||||
}
|
||||
|
||||
ref := info.ContainerReference{
|
||||
Name: "container",
|
||||
}
|
||||
|
||||
trace := buildTrace(cpuTrace, memTrace, 1*time.Second)
|
||||
|
||||
for _, stats := range trace {
|
||||
driver.AddStats(ref, stats)
|
||||
}
|
||||
|
||||
recentStats, err := driver.RecentStats(ref.Name, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(recentStats) == 0 {
|
||||
t.Fatal("should at least store one stats")
|
||||
}
|
||||
|
||||
if len(recentStats) > 10 {
|
||||
t.Fatalf("returned %v stats, not 10.", len(recentStats))
|
||||
}
|
||||
|
||||
actualRecentStats := trace[len(trace)-len(recentStats):]
|
||||
|
||||
// The returned stats should be sorted in time increasing order
|
||||
for i, s := range actualRecentStats {
|
||||
r := recentStats[i]
|
||||
if !statsEq(s, r) {
|
||||
t.Errorf("unexpected stats %+v with memory usage %v", r, r.Memory.Usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestRetrieveAllRecentStats(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
N := 100
|
||||
memTrace := make([]uint64, N)
|
||||
cpuTrace := make([]uint64, N)
|
||||
for i := 0; i < N; i++ {
|
||||
memTrace[i] = uint64(i + 1)
|
||||
cpuTrace[i] = uint64(1)
|
||||
}
|
||||
|
||||
ref := info.ContainerReference{
|
||||
Name: "container",
|
||||
}
|
||||
|
||||
trace := buildTrace(cpuTrace, memTrace, 1*time.Second)
|
||||
|
||||
for _, stats := range trace {
|
||||
driver.AddStats(ref, stats)
|
||||
}
|
||||
|
||||
recentStats, err := driver.RecentStats(ref.Name, -1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(recentStats) == 0 {
|
||||
t.Fatal("should at least store one stats")
|
||||
}
|
||||
if len(recentStats) > N {
|
||||
t.Fatalf("returned %v stats, not 100.", len(recentStats))
|
||||
}
|
||||
|
||||
actualRecentStats := trace[len(trace)-len(recentStats):]
|
||||
|
||||
// The returned stats should be sorted in time increasing order
|
||||
for i, s := range actualRecentStats {
|
||||
r := recentStats[i]
|
||||
if !statsEq(s, r) {
|
||||
t.Errorf("unexpected stats %+v with memory usage %v", r, r.Memory.Usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestNoRecentStats(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
nonExistContainer := "somerandomecontainer"
|
||||
stats, _ := driver.RecentStats(nonExistContainer, -1)
|
||||
if len(stats) > 0 {
|
||||
t.Errorf("RecentStats() returns %v stats on non exist container", len(stats))
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestNoSamples(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
nonExistContainer := "somerandomecontainer"
|
||||
samples, _ := driver.Samples(nonExistContainer, -1)
|
||||
if len(samples) > 0 {
|
||||
t.Errorf("Samples() returns %v samples on non exist container", len(samples))
|
||||
}
|
||||
}
|
||||
|
||||
func StorageDriverTestPercentilesWithoutStats(driver storage.StorageDriver, t *testing.T) {
|
||||
defer driver.Close()
|
||||
nonExistContainer := "somerandomecontainer"
|
||||
percentiles, _ := driver.Percentiles(nonExistContainer, []int{50, 80}, []int{50, 80})
|
||||
if percentiles == nil {
|
||||
return
|
||||
}
|
||||
if percentiles.MaxMemoryUsage != 0 {
|
||||
t.Errorf("Percentiles() reports max memory usage > 0 when there's no stats.")
|
||||
}
|
||||
for _, p := range percentiles.CpuUsagePercentiles {
|
||||
if p.Value != 0 {
|
||||
t.Errorf("Percentiles() reports cpu usage is %v when there's no stats.", p.Value)
|
||||
}
|
||||
}
|
||||
for _, p := range percentiles.MemoryUsagePercentiles {
|
||||
if p.Value != 0 {
|
||||
t.Errorf("Percentiles() reports memory usage is %v when there's no stats.", p.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
71
third_party/src/github.com/google/cadvisor/storagedriver.go
vendored
Normal file
71
third_party/src/github.com/google/cadvisor/storagedriver.go
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/cadvisor/storage"
|
||||
"github.com/google/cadvisor/storage/influxdb"
|
||||
"github.com/google/cadvisor/storage/memory"
|
||||
)
|
||||
|
||||
var argSampleSize = flag.Int("samples", 1024, "number of samples we want to keep")
|
||||
var argHistoryDuration = flag.Int("history_duration", 60, "number of seconds of container history to keep")
|
||||
var argDbUsername = flag.String("storage_driver_user", "root", "database username")
|
||||
var argDbPassword = flag.String("storage_driver_password", "root", "database password")
|
||||
var argDbHost = flag.String("storage_driver_host", "localhost:8086", "database host:port")
|
||||
var argDbName = flag.String("storage_driver_name", "cadvisor", "database name")
|
||||
var argDbIsSecure = flag.Bool("storage_driver_secure", false, "use secure connection with database")
|
||||
|
||||
func NewStorageDriver(driverName string) (storage.StorageDriver, error) {
|
||||
var storageDriver storage.StorageDriver
|
||||
var err error
|
||||
switch driverName {
|
||||
case "":
|
||||
// empty string by default is the in memory store
|
||||
fallthrough
|
||||
case "memory":
|
||||
storageDriver = memory.New(*argSampleSize, *argHistoryDuration)
|
||||
return storageDriver, nil
|
||||
case "influxdb":
|
||||
var hostname string
|
||||
hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storageDriver, err = influxdb.New(
|
||||
hostname,
|
||||
"cadvisorTable",
|
||||
*argDbName,
|
||||
*argDbUsername,
|
||||
*argDbPassword,
|
||||
*argDbHost,
|
||||
*argDbIsSecure,
|
||||
// TODO(monnand): One hour? Or user-defined?
|
||||
1*time.Hour,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("Unknown database driver: %v", *argDbDriver)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storageDriver, nil
|
||||
}
|
Reference in New Issue
Block a user