First commit

This commit is contained in:
Joe Beda
2014-06-06 16:40:48 -07:00
commit 2c4b3a562c
250 changed files with 47501 additions and 0 deletions

149
pkg/api/types.go Normal file
View File

@@ -0,0 +1,149 @@
/*
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 api includes all types used to communicate between the various
// parts of the Kubernetes system.
package api
// ContainerManifest corresponds to the Container Manifest format, documented at:
// https://developers.google.com/compute/docs/containers#container_manifest
// This is used as the representation of Kubernete's workloads.
type ContainerManifest struct {
Version string `yaml:"version" json:"version"`
Volumes []Volume `yaml:"volumes" json:"volumes"`
Containers []Container `yaml:"containers" json:"containers"`
Id string `yaml:"id,omitempty" json:"id,omitempty"`
}
type Volume struct {
Name string `yaml:"name" json:"name"`
}
type Port struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
HostPort int `yaml:"hostPort,omitempty" json:"hostPort,omitempty"`
ContainerPort int `yaml:"containerPort,omitempty" json:"containerPort,omitempty"`
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"`
}
type VolumeMount struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"`
MountPath string `yaml:"mountPath,omitempty" json:"mountPath,omitempty"`
}
type EnvVar struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
}
// Container represents a single container that is expected to be run on the host.
type Container struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Image string `yaml:"image,omitempty" json:"image,omitempty"`
Command string `yaml:"command,omitempty" json:"command,omitempty"`
WorkingDir string `yaml:"workingDir,omitempty" json:"workingDir,omitempty"`
Ports []Port `yaml:"ports,omitempty" json:"ports,omitempty"`
Env []EnvVar `yaml:"env,omitempty" json:"env,omitempty"`
Memory int `yaml:"memory,omitempty" json:"memory,omitempty"`
CPU int `yaml:"cpu,omitempty" json:"cpu,omitempty"`
VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty" json:"volumeMounts,omitempty"`
}
// Event is the representation of an event logged to etcd backends
type Event struct {
Event string `json:"event,omitempty"`
Manifest *ContainerManifest `json:"manifest,omitempty"`
Container *Container `json:"container,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// The below types are used by kube_client and api_server.
// JSONBase is shared by all objects sent to, or returned from the client
type JSONBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"`
SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"`
}
// TaskState is the state of a task, used as either input (desired state) or output (current state)
type TaskState struct {
Manifest ContainerManifest `json:"manifest,omitempty" yaml:"manifest,omitempty"`
Status string `json:"status,omitempty" yaml:"status,omitempty"`
Host string `json:"host,omitempty" yaml:"host,omitempty"`
HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"`
Info interface{} `json:"info,omitempty" yaml:"info,omitempty"`
}
type TaskList struct {
JSONBase
Items []Task `json:"items" yaml:"items,omitempty"`
}
// Task is a single task, used as either input (create, update) or as output (list, get)
type Task struct {
JSONBase
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
DesiredState TaskState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"`
CurrentState TaskState `json:"currentState,omitempty" yaml:"currentState,omitempty"`
}
// ReplicationControllerState is the state of a replication controller, either input (create, update) or as output (list, get)
type ReplicationControllerState struct {
Replicas int `json:"replicas" yaml:"replicas"`
ReplicasInSet map[string]string `json:"replicasInSet,omitempty" yaml:"replicasInSet,omitempty"`
TaskTemplate TaskTemplate `json:"taskTemplate,omitempty" yaml:"taskTemplate,omitempty"`
}
type ReplicationControllerList struct {
JSONBase
Items []ReplicationController `json:"items,omitempty" yaml:"items,omitempty"`
}
// ReplicationController represents the configuration of a replication controller
type ReplicationController struct {
JSONBase
DesiredState ReplicationControllerState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}
// TaskTemplate holds the information used for creating tasks
type TaskTemplate struct {
DesiredState TaskState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}
// ServiceList holds a list of services
type ServiceList struct {
Items []Service `json:"items" yaml:"items"`
}
// Defines a service abstraction by a name (for example, mysql) consisting of local port
// (for example 3306) that the proxy listens on, and the labels that define the service.
type Service struct {
JSONBase
Port int `json:"port,omitempty" yaml:"port,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}
// Defines the endpoints that implement the actual service, for example:
// Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"]
type Endpoints struct {
Name string
Endpoints []string
}

209
pkg/apiserver/api_server.go Normal file
View File

@@ -0,0 +1,209 @@
/*
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 apiserver is ...
package apiserver
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
)
// RESTStorage is a generic interface for RESTful storage services
type RESTStorage interface {
List(*url.URL) (interface{}, error)
Get(id string) (interface{}, error)
Delete(id string) error
Extract(body string) (interface{}, error)
Create(interface{}) error
Update(interface{}) error
}
// Status is a return value for calls that don't return other objects
type Status struct {
success bool
}
// ApiServer is an HTTPHandler that delegates to RESTStorage objects.
// It handles URLs of the form:
// ${prefix}/${storage_key}[/${object_name}]
// Where 'prefix' is an arbitrary string, and 'storage_key' points to a RESTStorage object stored in storage.
//
// 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
}
// 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,
}
}
func (server *ApiServer) handleIndex(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
// TODO: serve this out of a file?
data := "<html><body>Welcome to Kubernetes</body></html>"
fmt.Fprint(w, data)
}
// HTTP Handler interface
func (server *ApiServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log.Printf("%s %s", req.Method, req.RequestURI)
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, server.prefix) {
server.notFound(req, w)
return
}
requestParts := strings.Split(url.Path[len(server.prefix):], "/")[1:]
if len(requestParts) < 1 {
server.notFound(req, w)
return
}
storage := server.storage[requestParts[0]]
if storage == nil {
server.notFound(req, w)
return
} else {
server.handleREST(requestParts, url, req, w, storage)
}
}
func (server *ApiServer) notFound(req *http.Request, w http.ResponseWriter) {
w.WriteHeader(404)
fmt.Fprintf(w, "Not Found: %#v", req)
}
func (server *ApiServer) write(statusCode int, object interface{}, w http.ResponseWriter) {
w.WriteHeader(statusCode)
output, err := json.MarshalIndent(object, "", " ")
if err != nil {
server.error(err, w)
return
}
w.Write(output)
}
func (server *ApiServer) error(err error, w http.ResponseWriter) {
w.WriteHeader(500)
fmt.Fprintf(w, "Internal Error: %#v", err)
}
func (server *ApiServer) readBody(req *http.Request) (string, error) {
defer req.Body.Close()
body, err := ioutil.ReadAll(req.Body)
return string(body), err
}
func (server *ApiServer) handleREST(parts []string, url *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) {
switch req.Method {
case "GET":
switch len(parts) {
case 1:
controllers, err := storage.List(url)
if err != nil {
server.error(err, w)
return
}
server.write(200, controllers, w)
case 2:
task, err := storage.Get(parts[1])
if err != nil {
server.error(err, w)
return
}
if task == nil {
server.notFound(req, w)
return
}
server.write(200, task, w)
default:
server.notFound(req, w)
}
return
case "POST":
if len(parts) != 1 {
server.notFound(req, w)
return
}
body, err := server.readBody(req)
if err != nil {
server.error(err, w)
return
}
obj, err := storage.Extract(body)
if err != nil {
server.error(err, w)
return
}
storage.Create(obj)
server.write(200, obj, w)
return
case "DELETE":
if len(parts) != 2 {
server.notFound(req, w)
return
}
err := storage.Delete(parts[1])
if err != nil {
server.error(err, w)
return
}
server.write(200, Status{success: true}, w)
return
case "PUT":
if len(parts) != 2 {
server.notFound(req, w)
return
}
body, err := server.readBody(req)
if err != nil {
server.error(err, w)
}
obj, err := storage.Extract(body)
if err != nil {
server.error(err, w)
return
}
err = storage.Update(obj)
if err != nil {
server.error(err, w)
return
}
server.write(200, obj, w)
return
default:
server.notFound(req, w)
}
}

View File

@@ -0,0 +1,282 @@
/*
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 apiserver
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
)
// TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove.
func expectNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
type Simple struct {
Name string
}
type SimpleList struct {
Items []Simple
}
type SimpleRESTStorage struct {
err error
list []Simple
item Simple
deleted string
updated Simple
}
func (storage *SimpleRESTStorage) List(*url.URL) (interface{}, error) {
result := SimpleList{
Items: storage.list,
}
return result, storage.err
}
func (storage *SimpleRESTStorage) Get(id string) (interface{}, error) {
return storage.item, storage.err
}
func (storage *SimpleRESTStorage) Delete(id string) error {
storage.deleted = id
return storage.err
}
func (storage *SimpleRESTStorage) Extract(body string) (interface{}, error) {
var item Simple
json.Unmarshal([]byte(body), &item)
return item, storage.err
}
func (storage *SimpleRESTStorage) Create(interface{}) error {
return storage.err
}
func (storage *SimpleRESTStorage) Update(object interface{}) error {
storage.updated = object.(Simple)
return storage.err
}
func extractBody(response *http.Response, object interface{}) (string, error) {
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return string(body), err
}
err = json.Unmarshal(body, object)
return string(body), err
}
func TestSimpleList(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
resp, err := http.Get(server.URL + "/prefix/version/simple")
expectNoError(t, err)
if resp.StatusCode != 200 {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp)
}
}
func TestErrorList(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{
err: fmt.Errorf("Test Error"),
}
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
resp, err := http.Get(server.URL + "/prefix/version/simple")
expectNoError(t, err)
if resp.StatusCode != 500 {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp)
}
}
func TestNonEmptyList(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{
list: []Simple{
Simple{
Name: "foo",
},
},
}
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
resp, err := http.Get(server.URL + "/prefix/version/simple")
expectNoError(t, err)
if resp.StatusCode != 200 {
t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp)
}
var listOut SimpleList
body, err := extractBody(resp, &listOut)
if len(listOut.Items) != 1 {
t.Errorf("Unexpected response: %#v", listOut)
}
if listOut.Items[0].Name != simpleStorage.list[0].Name {
t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body))
}
}
func TestGet(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{
item: Simple{
Name: "foo",
},
}
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
resp, err := http.Get(server.URL + "/prefix/version/simple/id")
var itemOut Simple
body, err := extractBody(resp, &itemOut)
expectNoError(t, err)
if itemOut.Name != simpleStorage.item.Name {
t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simpleStorage.item, string(body))
}
}
func TestDelete(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil)
_, err = client.Do(request)
expectNoError(t, err)
if simpleStorage.deleted != ID {
t.Errorf("Unexpected delete: %s, expected %s (%s)", simpleStorage.deleted, ID)
}
}
func TestUpdate(t *testing.T) {
storage := map[string]RESTStorage{}
simpleStorage := SimpleRESTStorage{}
ID := "id"
storage["simple"] = &simpleStorage
handler := New(storage, "/prefix/version")
server := httptest.NewServer(handler)
item := Simple{
Name: "bar",
}
body, err := json.Marshal(item)
expectNoError(t, err)
client := http.Client{}
request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body))
_, err = client.Do(request)
expectNoError(t, err)
if simpleStorage.updated.Name != item.Name {
t.Errorf("Unexpected update value %#v, expected %#v.", simpleStorage.updated, item)
}
}
func TestBadPath(t *testing.T) {
handler := New(map[string]RESTStorage{}, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
request, err := http.NewRequest("GET", server.URL+"/foobar", nil)
expectNoError(t, err)
response, err := client.Do(request)
expectNoError(t, err)
if response.StatusCode != 404 {
t.Errorf("Unexpected response %#v", response)
}
}
func TestMissingPath(t *testing.T) {
handler := New(map[string]RESTStorage{}, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
request, err := http.NewRequest("GET", server.URL+"/prefix/version", nil)
expectNoError(t, err)
response, err := client.Do(request)
expectNoError(t, err)
if response.StatusCode != 404 {
t.Errorf("Unexpected response %#v", response)
}
}
func TestMissingStorage(t *testing.T) {
handler := New(map[string]RESTStorage{
"foo": &SimpleRESTStorage{},
}, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
request, err := http.NewRequest("GET", server.URL+"/prefix/version/foobar", nil)
expectNoError(t, err)
response, err := client.Do(request)
expectNoError(t, err)
if response.StatusCode != 404 {
t.Errorf("Unexpected response %#v", response)
}
}
func TestCreate(t *testing.T) {
handler := New(map[string]RESTStorage{
"foo": &SimpleRESTStorage{},
}, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
simple := Simple{Name: "foo"}
data, _ := json.Marshal(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 != 200 {
t.Errorf("Unexpected response %#v", response)
}
var itemOut Simple
body, err := extractBody(response, &itemOut)
expectNoError(t, err)
if !reflect.DeepEqual(itemOut, simple) {
t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simple, string(body))
}
}

251
pkg/client/client.go Normal file
View File

@@ -0,0 +1,251 @@
/*
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.
*/
// A client for the Kubernetes cluster management API
// There are three fundamental objects
// Task - A single running container
// TaskForce - A set of co-scheduled Task(s)
// ReplicationController - A manager for replicating TaskForces
package client
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// ClientInterface holds the methods for clients of Kubenetes, an interface to allow mock testing
type ClientInterface interface {
ListTasks(labelQuery map[string]string) (api.TaskList, error)
GetTask(name string) (api.Task, error)
DeleteTask(name string) error
CreateTask(api.Task) (api.Task, error)
UpdateTask(api.Task) (api.Task, error)
GetReplicationController(name string) (api.ReplicationController, error)
CreateReplicationController(api.ReplicationController) (api.ReplicationController, error)
UpdateReplicationController(api.ReplicationController) (api.ReplicationController, error)
DeleteReplicationController(string) error
GetService(name string) (api.Service, error)
CreateService(api.Service) (api.Service, error)
UpdateService(api.Service) (api.Service, error)
DeleteService(string) error
}
// AuthInfo is used to store authorization information
type AuthInfo struct {
User string
Password string
}
// Client is the actual implementation of a Kubernetes client.
// Host is the http://... base for the URL
type Client struct {
Host string
Auth *AuthInfo
httpClient *http.Client
}
// Underlying base implementation of performing a request.
// method is the HTTP method (e.g. "GET")
// path is the path on the host to hit
// requestBody is the body of the request. Can be nil.
// target the interface to marshal the JSON response into. Can be nil.
func (client Client) rawRequest(method, path string, requestBody io.Reader, target interface{}) ([]byte, error) {
request, err := http.NewRequest(method, client.makeURL(path), requestBody)
if err != nil {
return []byte{}, err
}
if client.Auth != nil {
request.SetBasicAuth(client.Auth.User, client.Auth.Password)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
var httpClient *http.Client
if client.httpClient != nil {
httpClient = client.httpClient
} else {
httpClient = &http.Client{Transport: tr}
}
response, err := httpClient.Do(request)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, fmt.Errorf("request [%s %s] failed (%d) %s", method, client.makeURL(path), response.StatusCode, response.Status)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return body, err
}
if target != nil {
err = json.Unmarshal(body, target)
}
if err != nil {
log.Printf("Failed to parse: %s\n", string(body))
// FIXME: no need to return err here?
}
return body, err
}
func (client Client) makeURL(path string) string {
return client.Host + "/api/v1beta1/" + path
}
func EncodeLabelQuery(labelQuery map[string]string) string {
query := make([]string, 0, len(labelQuery))
for key, value := range labelQuery {
query = append(query, key+"="+value)
}
return url.QueryEscape(strings.Join(query, ","))
}
func DecodeLabelQuery(labelQuery string) map[string]string {
result := map[string]string{}
if len(labelQuery) == 0 {
return result
}
parts := strings.Split(labelQuery, ",")
for _, part := range parts {
pieces := strings.Split(part, "=")
if len(pieces) == 2 {
result[pieces[0]] = pieces[1]
} else {
log.Printf("Invalid label query: %s", labelQuery)
}
}
return result
}
// ListTasks takes a label query, and returns the list of tasks that match that query
func (client Client) ListTasks(labelQuery map[string]string) (api.TaskList, error) {
path := "tasks"
if labelQuery != nil && len(labelQuery) > 0 {
path += "?labels=" + EncodeLabelQuery(labelQuery)
}
var result api.TaskList
_, err := client.rawRequest("GET", path, nil, &result)
return result, err
}
// GetTask takes the name of the task, and returns the corresponding Task object, and an error if it occurs
func (client Client) GetTask(name string) (api.Task, error) {
var result api.Task
_, err := client.rawRequest("GET", "tasks/"+name, nil, &result)
return result, err
}
// DeleteTask takes the name of the task, and returns an error if one occurs
func (client Client) DeleteTask(name string) error {
_, err := client.rawRequest("DELETE", "tasks/"+name, nil, nil)
return err
}
// CreateTask takes the representation of a task. Returns the server's representation of the task, and an error, if it occurs
func (client Client) CreateTask(task api.Task) (api.Task, error) {
var result api.Task
body, err := json.Marshal(task)
if err == nil {
_, err = client.rawRequest("POST", "tasks", bytes.NewBuffer(body), &result)
}
return result, err
}
// UpdateTask takes the representation of a task to update. Returns the server's representation of the task, and an error, if it occurs
func (client Client) UpdateTask(task api.Task) (api.Task, error) {
var result api.Task
body, err := json.Marshal(task)
if err == nil {
_, err = client.rawRequest("PUT", "tasks/"+task.ID, bytes.NewBuffer(body), &result)
}
return result, err
}
// GetReplicationController returns information about a particular replication controller
func (client Client) GetReplicationController(name string) (api.ReplicationController, error) {
var result api.ReplicationController
_, err := client.rawRequest("GET", "replicationControllers/"+name, nil, &result)
return result, err
}
// CreateReplicationController creates a new replication controller
func (client Client) CreateReplicationController(controller api.ReplicationController) (api.ReplicationController, error) {
var result api.ReplicationController
body, err := json.Marshal(controller)
if err == nil {
_, err = client.rawRequest("POST", "replicationControllers", bytes.NewBuffer(body), &result)
}
return result, err
}
// UpdateReplicationController updates an existing replication controller
func (client Client) UpdateReplicationController(controller api.ReplicationController) (api.ReplicationController, error) {
var result api.ReplicationController
body, err := json.Marshal(controller)
if err == nil {
_, err = client.rawRequest("PUT", "replicationControllers/"+controller.ID, bytes.NewBuffer(body), &result)
}
return result, err
}
func (client Client) DeleteReplicationController(name string) error {
_, err := client.rawRequest("DELETE", "replicationControllers/"+name, nil, nil)
return err
}
// GetReplicationController returns information about a particular replication controller
func (client Client) GetService(name string) (api.Service, error) {
var result api.Service
_, err := client.rawRequest("GET", "services/"+name, nil, &result)
return result, err
}
// CreateReplicationController creates a new replication controller
func (client Client) CreateService(svc api.Service) (api.Service, error) {
var result api.Service
body, err := json.Marshal(svc)
if err == nil {
_, err = client.rawRequest("POST", "services", bytes.NewBuffer(body), &result)
}
return result, err
}
// UpdateReplicationController updates an existing replication controller
func (client Client) UpdateService(svc api.Service) (api.Service, error) {
var result api.Service
body, err := json.Marshal(svc)
if err == nil {
_, err = client.rawRequest("PUT", "services/"+svc.ID, bytes.NewBuffer(body), &result)
}
return result, err
}
func (client Client) DeleteService(name string) error {
_, err := client.rawRequest("DELETE", "services/"+name, nil, nil)
return err
}

391
pkg/client/client_test.go Normal file
View File

@@ -0,0 +1,391 @@
/*
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 client
import (
"encoding/json"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
// TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove.
func expectNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
// TODO: Move this to a common place, it's needed in multiple tests.
var apiPath = "/api/v1beta1"
func makeUrl(suffix string) string {
return apiPath + suffix
}
func TestListEmptyTasks(t *testing.T) {
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: `{ "items": []}`,
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
taskList, err := client.ListTasks(nil)
fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil)
if err != nil {
t.Errorf("Unexpected error in listing tasks: %#v", err)
}
if len(taskList.Items) != 0 {
t.Errorf("Unexpected items in task list: %#v", taskList)
}
testServer.Close()
}
func TestListTasks(t *testing.T) {
expectedTaskList := api.TaskList{
Items: []api.Task{
api.Task{
CurrentState: api.TaskState{
Status: "Foobar",
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
},
},
}
body, _ := json.Marshal(expectedTaskList)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedTaskList, err := client.ListTasks(nil)
fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil)
if err != nil {
t.Errorf("Unexpected error in listing tasks: %#v", err)
}
if !reflect.DeepEqual(expectedTaskList, receivedTaskList) {
t.Errorf("Unexpected task list: %#v\nvs.\n%#v", receivedTaskList, expectedTaskList)
}
testServer.Close()
}
func TestListTasksLabels(t *testing.T) {
expectedTaskList := api.TaskList{
Items: []api.Task{
api.Task{
CurrentState: api.TaskState{
Status: "Foobar",
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
},
},
}
body, _ := json.Marshal(expectedTaskList)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
query := map[string]string{"foo": "bar", "name": "baz"}
receivedTaskList, err := client.ListTasks(query)
fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil)
queryString := fakeHandler.RequestReceived.URL.Query().Get("labels")
queryString, _ = url.QueryUnescape(queryString)
// TODO(bburns) : This assumes some ordering in serialization that might not always
// be true, parse it into a map.
if queryString != "foo=bar,name=baz" {
t.Errorf("Unexpected label query: %s", queryString)
}
if err != nil {
t.Errorf("Unexpected error in listing tasks: %#v", err)
}
if !reflect.DeepEqual(expectedTaskList, receivedTaskList) {
t.Errorf("Unexpected task list: %#v\nvs.\n%#v", receivedTaskList, expectedTaskList)
}
testServer.Close()
}
func TestGetTask(t *testing.T) {
expectedTask := api.Task{
CurrentState: api.TaskState{
Status: "Foobar",
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(expectedTask)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedTask, err := client.GetTask("foo")
fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "GET", nil)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
if !reflect.DeepEqual(expectedTask, receivedTask) {
t.Errorf("Received task: %#v\n doesn't match expected task: %#v", receivedTask, expectedTask)
}
testServer.Close()
}
func TestDeleteTask(t *testing.T) {
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: `{"success": true}`,
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
err := client.DeleteTask("foo")
fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "DELETE", nil)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
testServer.Close()
}
func TestCreateTask(t *testing.T) {
requestTask := api.Task{
CurrentState: api.TaskState{
Status: "Foobar",
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(requestTask)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedTask, err := client.CreateTask(requestTask)
fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "POST", nil)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
if !reflect.DeepEqual(requestTask, receivedTask) {
t.Errorf("Received task: %#v\n doesn't match expected task: %#v", receivedTask, requestTask)
}
testServer.Close()
}
func TestUpdateTask(t *testing.T) {
requestTask := api.Task{
JSONBase: api.JSONBase{ID: "foo"},
CurrentState: api.TaskState{
Status: "Foobar",
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(requestTask)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedTask, err := client.UpdateTask(requestTask)
fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "PUT", nil)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
expectEqual(t, requestTask, receivedTask)
testServer.Close()
}
func expectEqual(t *testing.T, expected, observed interface{}) {
if !reflect.DeepEqual(expected, observed) {
t.Errorf("Unexpected inequality. Expected: %#v Observed: %#v", expected, observed)
}
}
func TestEncodeDecodeLabelQuery(t *testing.T) {
queryIn := map[string]string{
"foo": "bar",
"baz": "blah",
}
queryString, _ := url.QueryUnescape(EncodeLabelQuery(queryIn))
queryOut := DecodeLabelQuery(queryString)
expectEqual(t, queryIn, queryOut)
}
func TestDecodeEmpty(t *testing.T) {
query := DecodeLabelQuery("")
if len(query) != 0 {
t.Errorf("Unexpected query: %#v", query)
}
}
func TestDecodeBad(t *testing.T) {
query := DecodeLabelQuery("foo")
if len(query) != 0 {
t.Errorf("Unexpected query: %#v", query)
}
}
func TestGetController(t *testing.T) {
expectedController := api.ReplicationController{
JSONBase: api.JSONBase{
ID: "foo",
},
DesiredState: api.ReplicationControllerState{
Replicas: 2,
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(expectedController)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedController, err := client.GetReplicationController("foo")
expectNoError(t, err)
if !reflect.DeepEqual(expectedController, receivedController) {
t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController)
}
fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "GET", nil)
testServer.Close()
}
func TestUpdateController(t *testing.T) {
expectedController := api.ReplicationController{
JSONBase: api.JSONBase{
ID: "foo",
},
DesiredState: api.ReplicationControllerState{
Replicas: 2,
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(expectedController)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedController, err := client.UpdateReplicationController(api.ReplicationController{
JSONBase: api.JSONBase{
ID: "foo",
},
})
expectNoError(t, err)
if !reflect.DeepEqual(expectedController, receivedController) {
t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController)
}
fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "PUT", nil)
testServer.Close()
}
func TestDeleteController(t *testing.T) {
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: `{"success": true}`,
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
err := client.DeleteReplicationController("foo")
fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "DELETE", nil)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
testServer.Close()
}
func TestCreateController(t *testing.T) {
expectedController := api.ReplicationController{
JSONBase: api.JSONBase{
ID: "foo",
},
DesiredState: api.ReplicationControllerState{
Replicas: 2,
},
Labels: map[string]string{
"foo": "bar",
"name": "baz",
},
}
body, _ := json.Marshal(expectedController)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
receivedController, err := client.CreateReplicationController(api.ReplicationController{
JSONBase: api.JSONBase{
ID: "foo",
},
})
expectNoError(t, err)
if !reflect.DeepEqual(expectedController, receivedController) {
t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController)
}
fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers"), "POST", nil)
testServer.Close()
}

View File

@@ -0,0 +1,61 @@
/*
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 client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type ContainerInfo interface {
GetContainerInfo(host, name string) (interface{}, error)
}
type HTTPContainerInfo struct {
Client *http.Client
Port uint
}
func (c *HTTPContainerInfo) GetContainerInfo(host, name string) (interface{}, error) {
request, err := http.NewRequest("GET", fmt.Sprintf("http://%s:%d/containerInfo?container=%s", host, c.Port, name), nil)
if err != nil {
return nil, err
}
response, err := c.Client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
var data interface{}
err = json.Unmarshal(body, &data)
return data, err
}
// Useful for testing.
type FakeContainerInfo struct {
data interface{}
err error
}
func (c *FakeContainerInfo) GetContainerInfo(host, name string) (interface{}, error) {
return c.data, c.err
}

View File

@@ -0,0 +1,54 @@
/*
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 client
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
func TestHTTPContainerInfo(t *testing.T) {
body := `{"items":[]}`
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: body,
}
testServer := httptest.NewServer(&fakeHandler)
hostUrl, err := url.Parse(testServer.URL)
expectNoError(t, err)
parts := strings.Split(hostUrl.Host, ":")
port, err := strconv.Atoi(parts[1])
expectNoError(t, err)
containerInfo := &HTTPContainerInfo{
Client: http.DefaultClient,
Port: uint(port),
}
data, err := containerInfo.GetContainerInfo(parts[0], "foo")
expectNoError(t, err)
dataString, _ := json.Marshal(data)
if string(dataString) != body {
t.Errorf("Unexpected response. Expected: %s, received %s", body, string(dataString))
}
}

254
pkg/cloudcfg/cloudcfg.go Normal file
View File

@@ -0,0 +1,254 @@
/*
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 cloudcfg is ...
package cloudcfg
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"gopkg.in/v1/yaml"
)
func promptForString(field string) string {
fmt.Printf("Please enter %s: ", field)
var result string
fmt.Scan(&result)
return result
}
// Parse an AuthInfo object from a file path
func LoadAuthInfo(path string) (client.AuthInfo, error) {
var auth client.AuthInfo
if _, err := os.Stat(path); os.IsNotExist(err) {
auth.User = promptForString("Username")
auth.Password = promptForString("Password")
data, err := json.Marshal(auth)
if err != nil {
return auth, err
}
err = ioutil.WriteFile(path, data, 0600)
return auth, err
}
data, err := ioutil.ReadFile(path)
if err != nil {
return auth, err
}
err = json.Unmarshal(data, &auth)
return auth, err
}
// Perform a rolling update of a collection of tasks.
// 'name' points to a replication controller.
// 'client' is used for updating tasks.
// 'updatePeriod' is the time between task updates.
func Update(name string, client client.ClientInterface, updatePeriod time.Duration) error {
controller, err := client.GetReplicationController(name)
if err != nil {
return err
}
labels := controller.DesiredState.ReplicasInSet
taskList, err := client.ListTasks(labels)
if err != nil {
return err
}
for _, task := range taskList.Items {
_, err = client.UpdateTask(task)
if err != nil {
return err
}
time.Sleep(updatePeriod)
}
return nil
}
// RequestWithBody is a helper method that creates an HTTP request with the specified url, method
// and a body read from 'configFile'
// FIXME: need to be public API?
func RequestWithBody(configFile, url, method string) (*http.Request, error) {
if len(configFile) == 0 {
return nil, fmt.Errorf("empty config file.")
}
data, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
return RequestWithBodyData(data, url, method)
}
// RequestWithBodyData is a helper method that creates an HTTP request with the specified url, method
// and body data
// FIXME: need to be public API?
func RequestWithBodyData(data []byte, url, method string) (*http.Request, error) {
request, err := http.NewRequest(method, url, bytes.NewBuffer(data))
request.ContentLength = int64(len(data))
return request, err
}
// Execute a request, adds authentication, and HTTPS cert ignoring.
// TODO: Make this stuff optional
// FIXME: need to be public API?
func DoRequest(request *http.Request, user, password string) (string, error) {
request.SetBasicAuth(user, password)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
response, err := client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
return string(body), err
}
// StopController stops a controller named 'name' by setting replicas to zero
func StopController(name string, client client.ClientInterface) error {
controller, err := client.GetReplicationController(name)
if err != nil {
return err
}
controller.DesiredState.Replicas = 0
controllerOut, err := client.UpdateReplicationController(controller)
if err != nil {
return err
}
data, err := yaml.Marshal(controllerOut)
if err != nil {
return err
}
fmt.Print(string(data))
return nil
}
func makePorts(spec string) []api.Port {
parts := strings.Split(spec, ",")
var result []api.Port
for _, part := range parts {
pieces := strings.Split(part, ":")
if len(pieces) != 2 {
log.Printf("Bad port spec: %s", part)
continue
}
host, err := strconv.Atoi(pieces[0])
if err != nil {
log.Printf("Host part is not integer: %s %v", pieces[0], err)
continue
}
container, err := strconv.Atoi(pieces[1])
if err != nil {
log.Printf("Container part is not integer: %s %v", pieces[1], err)
continue
}
result = append(result, api.Port{ContainerPort: container, HostPort: host})
}
return result
}
// RunController creates a new replication controller named 'name' which creates 'replicas' tasks running 'image'
func RunController(image, name string, replicas int, client client.ClientInterface, portSpec string, servicePort int) error {
controller := api.ReplicationController{
JSONBase: api.JSONBase{
ID: name,
},
DesiredState: api.ReplicationControllerState{
Replicas: replicas,
ReplicasInSet: map[string]string{
"name": name,
},
TaskTemplate: api.TaskTemplate{
DesiredState: api.TaskState{
Manifest: api.ContainerManifest{
Containers: []api.Container{
api.Container{
Image: image,
Ports: makePorts(portSpec),
},
},
},
},
Labels: map[string]string{
"name": name,
},
},
},
Labels: map[string]string{
"name": name,
},
}
controllerOut, err := client.CreateReplicationController(controller)
if err != nil {
return err
}
data, err := yaml.Marshal(controllerOut)
if err != nil {
return err
}
fmt.Print(string(data))
if servicePort > 0 {
svc, err := createService(name, servicePort, client)
if err != nil {
return err
}
data, err = yaml.Marshal(svc)
if err != nil {
return err
}
fmt.Printf(string(data))
}
return nil
}
func createService(name string, port int, client client.ClientInterface) (api.Service, error) {
svc := api.Service{
JSONBase: api.JSONBase{ID: name},
Port: port,
Labels: map[string]string{
"name": name,
},
}
svc, err := client.CreateService(svc)
return svc, err
}
// DeleteController deletes a replication controller named 'name', requires that the controller
// already be stopped
func DeleteController(name string, client client.ClientInterface) error {
controller, err := client.GetReplicationController(name)
if err != nil {
return err
}
if controller.DesiredState.Replicas != 0 {
return fmt.Errorf("controller has non-zero replicas (%d)", controller.DesiredState.Replicas)
}
return client.DeleteReplicationController(name)
}

View File

@@ -0,0 +1,308 @@
/*
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 cloudcfg
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
// TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove.
func expectNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
type Action struct {
action string
value interface{}
}
type FakeKubeClient struct {
actions []Action
tasks TaskList
ctrl ReplicationController
}
func (client *FakeKubeClient) ListTasks(labelQuery map[string]string) (TaskList, error) {
client.actions = append(client.actions, Action{action: "list-tasks"})
return client.tasks, nil
}
func (client *FakeKubeClient) GetTask(name string) (Task, error) {
client.actions = append(client.actions, Action{action: "get-task", value: name})
return Task{}, nil
}
func (client *FakeKubeClient) DeleteTask(name string) error {
client.actions = append(client.actions, Action{action: "delete-task", value: name})
return nil
}
func (client *FakeKubeClient) CreateTask(task Task) (Task, error) {
client.actions = append(client.actions, Action{action: "create-task"})
return Task{}, nil
}
func (client *FakeKubeClient) UpdateTask(task Task) (Task, error) {
client.actions = append(client.actions, Action{action: "update-task", value: task.ID})
return Task{}, nil
}
func (client *FakeKubeClient) GetReplicationController(name string) (ReplicationController, error) {
client.actions = append(client.actions, Action{action: "get-controller", value: name})
return client.ctrl, nil
}
func (client *FakeKubeClient) CreateReplicationController(controller ReplicationController) (ReplicationController, error) {
client.actions = append(client.actions, Action{action: "create-controller", value: controller})
return ReplicationController{}, nil
}
func (client *FakeKubeClient) UpdateReplicationController(controller ReplicationController) (ReplicationController, error) {
client.actions = append(client.actions, Action{action: "update-controller", value: controller})
return ReplicationController{}, nil
}
func (client *FakeKubeClient) DeleteReplicationController(controller string) error {
client.actions = append(client.actions, Action{action: "delete-controller", value: controller})
return nil
}
func (client *FakeKubeClient) GetService(name string) (Service, error) {
client.actions = append(client.actions, Action{action: "get-controller", value: name})
return Service{}, nil
}
func (client *FakeKubeClient) CreateService(controller Service) (Service, error) {
client.actions = append(client.actions, Action{action: "create-service", value: controller})
return Service{}, nil
}
func (client *FakeKubeClient) UpdateService(controller Service) (Service, error) {
client.actions = append(client.actions, Action{action: "update-service", value: controller})
return Service{}, nil
}
func (client *FakeKubeClient) DeleteService(controller string) error {
client.actions = append(client.actions, Action{action: "delete-service", value: controller})
return nil
}
func validateAction(expectedAction, actualAction Action, t *testing.T) {
if expectedAction != actualAction {
t.Errorf("Unexpected action: %#v, expected: %#v", actualAction, expectedAction)
}
}
func TestUpdateWithTasks(t *testing.T) {
client := FakeKubeClient{
tasks: TaskList{
Items: []Task{
Task{JSONBase: JSONBase{ID: "task-1"}},
Task{JSONBase: JSONBase{ID: "task-2"}},
},
},
}
Update("foo", &client, 0)
if len(client.actions) != 4 {
t.Errorf("Unexpected action list %#v", client.actions)
}
validateAction(Action{action: "get-controller", value: "foo"}, client.actions[0], t)
validateAction(Action{action: "list-tasks"}, client.actions[1], t)
validateAction(Action{action: "update-task", value: "task-1"}, client.actions[2], t)
validateAction(Action{action: "update-task", value: "task-2"}, client.actions[3], t)
}
func TestUpdateNoTasks(t *testing.T) {
client := FakeKubeClient{}
Update("foo", &client, 0)
if len(client.actions) != 2 {
t.Errorf("Unexpected action list %#v", client.actions)
}
validateAction(Action{action: "get-controller", value: "foo"}, client.actions[0], t)
validateAction(Action{action: "list-tasks"}, client.actions[1], t)
}
func TestDoRequest(t *testing.T) {
expectedBody := `{ "items": []}`
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: expectedBody,
}
testServer := httptest.NewTLSServer(&fakeHandler)
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
body, err := DoRequest(request, "user", "pass")
if request.Header["Authorization"] == nil {
t.Errorf("Request is missing authorization header: %#v", *request)
}
if err != nil {
t.Error("Unexpected error")
}
if body != expectedBody {
t.Errorf("Expected body: '%s', saw: '%s'", expectedBody, body)
}
fakeHandler.ValidateRequest(t, "/foo/bar", "GET", &fakeHandler.ResponseBody)
}
func TestRunController(t *testing.T) {
fakeClient := FakeKubeClient{}
name := "name"
image := "foo/bar"
replicas := 3
RunController(image, name, replicas, &fakeClient, "8080:80", -1)
if len(fakeClient.actions) != 1 || fakeClient.actions[0].action != "create-controller" {
t.Errorf("Unexpected actions: %#v", fakeClient.actions)
}
controller := fakeClient.actions[0].value.(ReplicationController)
if controller.ID != name ||
controller.DesiredState.Replicas != replicas ||
controller.DesiredState.TaskTemplate.DesiredState.Manifest.Containers[0].Image != image {
t.Errorf("Unexpected controller: %#v", controller)
}
}
func TestRunControllerWithService(t *testing.T) {
fakeClient := FakeKubeClient{}
name := "name"
image := "foo/bar"
replicas := 3
RunController(image, name, replicas, &fakeClient, "", 8000)
if len(fakeClient.actions) != 2 ||
fakeClient.actions[0].action != "create-controller" ||
fakeClient.actions[1].action != "create-service" {
t.Errorf("Unexpected actions: %#v", fakeClient.actions)
}
controller := fakeClient.actions[0].value.(ReplicationController)
if controller.ID != name ||
controller.DesiredState.Replicas != replicas ||
controller.DesiredState.TaskTemplate.DesiredState.Manifest.Containers[0].Image != image {
t.Errorf("Unexpected controller: %#v", controller)
}
}
func TestStopController(t *testing.T) {
fakeClient := FakeKubeClient{}
name := "name"
StopController(name, &fakeClient)
if len(fakeClient.actions) != 2 {
t.Errorf("Unexpected actions: %#v", fakeClient.actions)
}
if fakeClient.actions[0].action != "get-controller" ||
fakeClient.actions[0].value.(string) != name {
t.Errorf("Unexpected action: %#v", fakeClient.actions[0])
}
controller := fakeClient.actions[1].value.(ReplicationController)
if fakeClient.actions[1].action != "update-controller" ||
controller.DesiredState.Replicas != 0 {
t.Errorf("Unexpected action: %#v", fakeClient.actions[1])
}
}
func TestCloudCfgDeleteController(t *testing.T) {
fakeClient := FakeKubeClient{}
name := "name"
err := DeleteController(name, &fakeClient)
expectNoError(t, err)
if len(fakeClient.actions) != 2 {
t.Errorf("Unexpected actions: %#v", fakeClient.actions)
}
if fakeClient.actions[0].action != "get-controller" ||
fakeClient.actions[0].value.(string) != name {
t.Errorf("Unexpected action: %#v", fakeClient.actions[0])
}
if fakeClient.actions[1].action != "delete-controller" ||
fakeClient.actions[1].value.(string) != name {
t.Errorf("Unexpected action: %#v", fakeClient.actions[1])
}
}
func TestCloudCfgDeleteControllerWithReplicas(t *testing.T) {
fakeClient := FakeKubeClient{
ctrl: ReplicationController{
DesiredState: ReplicationControllerState{
Replicas: 2,
},
},
}
name := "name"
err := DeleteController(name, &fakeClient)
if len(fakeClient.actions) != 1 {
t.Errorf("Unexpected actions: %#v", fakeClient.actions)
}
if fakeClient.actions[0].action != "get-controller" ||
fakeClient.actions[0].value.(string) != name {
t.Errorf("Unexpected action: %#v", fakeClient.actions[0])
}
if err == nil {
t.Errorf("Unexpected non-error.")
}
}
func TestRequestWithBodyNoSuchFile(t *testing.T) {
request, err := RequestWithBody("non/existent/file.json", "http://www.google.com", "GET")
if request != nil {
t.Error("Unexpected non-nil result")
}
if err == nil {
t.Error("Unexpected non-error")
}
}
func TestRequestWithBody(t *testing.T) {
file, err := ioutil.TempFile("", "foo")
expectNoError(t, err)
data, err := json.Marshal(Task{JSONBase: JSONBase{ID: "foo"}})
expectNoError(t, err)
_, err = file.Write(data)
expectNoError(t, err)
request, err := RequestWithBody(file.Name(), "http://www.google.com", "GET")
if request == nil {
t.Error("Unexpected nil result")
}
if err != nil {
t.Errorf("Unexpected error: %#v")
}
dataOut, err := ioutil.ReadAll(request.Body)
expectNoError(t, err)
if string(data) != string(dataOut) {
t.Errorf("Mismatched data. Expected %s, got %s", data, dataOut)
}
}
func validatePort(t *testing.T, p Port, external int, internal int) {
if p.HostPort != external || p.ContainerPort != internal {
t.Errorf("Unexpected port: %#v != (%d, %d)", p, external, internal)
}
}
func TestMakePorts(t *testing.T) {
ports := makePorts("8080:80,8081:8081,443:444")
if len(ports) != 3 {
t.Errorf("Unexpected ports: %#v", ports)
}
validatePort(t, ports[0], 8080, 80)
validatePort(t, ports[1], 8081, 8081)
validatePort(t, ports[2], 443, 444)
}

598
pkg/kubelet/kubelet.go Normal file
View File

@@ -0,0 +1,598 @@
/*
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 is ...
package kubelet
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
"github.com/fsouza/go-dockerclient"
"gopkg.in/v1/yaml"
)
// State, sub object of the Docker JSON data
type State struct {
Running bool
}
// The structured representation of the JSON object returned by Docker inspect
type DockerContainerData struct {
state State
}
// Interface for testability
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
}
// The main kubelet implementation
type Kubelet struct {
Client registry.EtcdClient
DockerClient DockerInterface
FileCheckFrequency time.Duration
SyncFrequency time.Duration
HTTPCheckFrequency time.Duration
pullLock sync.Mutex
}
// Starts background goroutines. If file, manifest_url, or address are empty,
// they are not watched. Never returns.
func (sl *Kubelet) RunKubelet(file, manifest_url, etcd_servers, address string, port uint) {
fileChannel := make(chan api.ContainerManifest)
etcdChannel := make(chan []api.ContainerManifest)
httpChannel := make(chan api.ContainerManifest)
serverChannel := make(chan api.ContainerManifest)
go util.Forever(func() { sl.WatchFile(file, fileChannel) }, 20*time.Second)
if manifest_url != "" {
go util.Forever(func() { sl.WatchHTTP(manifest_url, httpChannel) }, 20*time.Second)
}
if etcd_servers != "" {
servers := []string{etcd_servers}
log.Printf("Creating etcd client pointing to %v", servers)
sl.Client = etcd.NewClient(servers)
go util.Forever(func() { sl.SyncAndSetupEtcdWatch(etcdChannel) }, 20*time.Second)
}
if address != "" {
log.Printf("Starting to listen on %s:%d", address, port)
handler := KubeletServer{
Kubelet: sl,
UpdateChannel: serverChannel,
}
s := &http.Server{
// TODO: This is broken if address is an ipv6 address.
Addr: fmt.Sprintf("%s:%d", address, port),
Handler: &handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
go util.Forever(func() { s.ListenAndServe() }, 0)
}
sl.RunSyncLoop(etcdChannel, fileChannel, serverChannel, httpChannel, sl)
}
// Interface implemented by Kubelet, for testability
type SyncHandler interface {
SyncManifests([]api.ContainerManifest) error
}
// Log an event to the etcd backend.
func (sl *Kubelet) LogEvent(event *api.Event) error {
if sl.Client == nil {
return fmt.Errorf("no etcd client connection.")
}
event.Timestamp = time.Now().Unix()
data, err := json.Marshal(event)
if err != nil {
return err
}
var response *etcd.Response
response, err = sl.Client.AddChild(fmt.Sprintf("/events/%s", event.Container.Name), string(data), 60*60*48 /* 2 days */)
// TODO(bburns) : examine response here.
if err != nil {
log.Printf("Error writing event: %s\n", err)
if response != nil {
log.Printf("Response was: %#v\n", *response)
}
}
return err
}
// Does this container exist on this host? Returns true if so, and the name under which the container is running.
// Returns an error if one occurs.
func (sl *Kubelet) ContainerExists(manifest *api.ContainerManifest, container *api.Container) (exists bool, foundName string, err error) {
containers, err := sl.ListContainers()
if err != nil {
return false, "", err
}
for _, name := range containers {
manifestId, containerName := dockerNameToManifestAndContainer(name)
if manifestId == manifest.Id && containerName == container.Name {
// TODO(bburns) : This leads to an extra list. Convert this to use the returned ID and a straight call
// to inspect
data, err := sl.GetContainerByName(name)
return data != nil, name, err
}
}
return false, "", nil
}
func (sl *Kubelet) GetContainerID(name string) (string, error) {
containerList, err := sl.DockerClient.ListContainers(docker.ListContainersOptions{})
if err != nil {
return "", err
}
for _, value := range containerList {
if strings.Contains(value.Names[0], name) {
return value.ID, nil
}
}
return "", fmt.Errorf("couldn't find name: %s", name)
}
// Get a container by name.
// returns the container data from Docker, or an error if one exists.
func (sl *Kubelet) GetContainerByName(name string) (*docker.Container, error) {
id, err := sl.GetContainerID(name)
if err != nil {
return nil, err
}
return sl.DockerClient.InspectContainer(id)
}
func (sl *Kubelet) ListContainers() ([]string, error) {
result := []string{}
containerList, err := sl.DockerClient.ListContainers(docker.ListContainersOptions{})
if err != nil {
return result, err
}
for _, value := range containerList {
result = append(result, value.Names[0])
}
return result, err
}
func (sl *Kubelet) pullImage(image string) error {
sl.pullLock.Lock()
defer sl.pullLock.Unlock()
cmd := exec.Command("docker", "pull", image)
err := cmd.Start()
if err != nil {
return err
}
return cmd.Wait()
}
// 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
}
// Creates a name which can be reversed to identify both manifest id and container name.
func manifestAndContainerToDockerName(manifest *api.ContainerManifest, container *api.Container) string {
// Note, manifest.Id could be blank.
return fmt.Sprintf("%s--%s--%x", 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 dockerNameToManifestAndContainer(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 {
containerName = unescapeDash(parts[0])
}
if len(parts) > 1 {
manifestId = unescapeDash(parts[1])
}
return
}
func (sl *Kubelet) RunContainer(manifest *api.ContainerManifest, container *api.Container) (name string, err error) {
err = sl.pullImage(container.Image)
if err != nil {
return "", err
}
name = manifestAndContainerToDockerName(manifest, container)
envVariables := []string{}
for _, value := range container.Env {
envVariables = append(envVariables, fmt.Sprintf("%s=%s", value.Name, value.Value))
}
volumes := map[string]struct{}{}
binds := []string{}
for _, volume := range container.VolumeMounts {
volumes[volume.MountPath] = struct{}{}
basePath := "/exports/" + volume.Name + ":" + volume.MountPath
if volume.ReadOnly {
basePath += ":ro"
}
binds = append(binds, basePath)
}
exposedPorts := map[docker.Port]struct{}{}
portBindings := map[docker.Port][]docker.PortBinding{}
for _, port := range container.Ports {
interiorPort := port.ContainerPort
exteriorPort := port.HostPort
// Some of this port stuff is under-documented voodoo.
// See http://stackoverflow.com/questions/20428302/binding-a-port-to-a-host-interface-using-the-rest-api
dockerPort := docker.Port(strconv.Itoa(interiorPort) + "/tcp")
exposedPorts[dockerPort] = struct{}{}
portBindings[dockerPort] = []docker.PortBinding{
docker.PortBinding{
HostPort: strconv.Itoa(exteriorPort),
},
}
}
var cmdList []string
if len(container.Command) > 0 {
cmdList = strings.Split(container.Command, " ")
}
opts := docker.CreateContainerOptions{
Name: name,
Config: &docker.Config{
Image: container.Image,
ExposedPorts: exposedPorts,
Env: envVariables,
Volumes: volumes,
WorkingDir: container.WorkingDir,
Cmd: cmdList,
},
}
dockerContainer, err := sl.DockerClient.CreateContainer(opts)
if err != nil {
return "", err
}
return name, sl.DockerClient.StartContainer(dockerContainer.ID, &docker.HostConfig{
PortBindings: portBindings,
Binds: binds,
})
}
func (sl *Kubelet) KillContainer(name string) error {
id, err := sl.GetContainerID(name)
if err != nil {
return err
}
err = sl.DockerClient.StopContainer(id, 10)
manifestId, containerName := dockerNameToManifestAndContainer(name)
sl.LogEvent(&api.Event{
Event: "STOP",
Manifest: &api.ContainerManifest{
Id: manifestId,
},
Container: &api.Container{
Name: containerName,
},
})
return err
}
// Watch a file for changes to the set of tasks that should run on this Kubelet
// This function loops forever and is intended to be run as a goroutine
func (sl *Kubelet) WatchFile(file string, changeChannel chan<- api.ContainerManifest) {
var lastData []byte
for {
time.Sleep(sl.FileCheckFrequency)
var manifest api.ContainerManifest
data, err := ioutil.ReadFile(file)
if err != nil {
log.Printf("Couldn't read file: %s : %v", file, err)
continue
}
if err = sl.ExtractYAMLData(data, &manifest); err != nil {
continue
}
if !bytes.Equal(lastData, data) {
lastData = data
// Ok, we have a valid configuration, send to channel for
// rejiggering.
changeChannel <- manifest
continue
}
}
}
// Watch an HTTP endpoint for changes to the set of tasks that should run on this Kubelet
// This function runs forever and is intended to be run as a goroutine
func (sl *Kubelet) WatchHTTP(url string, changeChannel chan<- api.ContainerManifest) {
var lastData []byte
client := &http.Client{}
for {
time.Sleep(sl.HTTPCheckFrequency)
var config api.ContainerManifest
data, err := sl.SyncHTTP(client, url, &config)
log.Printf("Containers: %#v", config)
if err != nil {
log.Printf("Error syncing HTTP: %#v", err)
continue
}
if !bytes.Equal(lastData, data) {
lastData = data
changeChannel <- config
continue
}
}
}
// SyncHTTP reads from url a yaml manifest and populates config. Returns the
// raw bytes, if something was read. Returns an error if something goes wrong.
// 'client' is used to execute the request, to allow caching of clients.
func (sl *Kubelet) SyncHTTP(client *http.Client, url string, config *api.ContainerManifest) ([]byte, error) {
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
if err = sl.ExtractYAMLData(body, &config); err != nil {
return body, err
}
return body, nil
}
// Take an etcd Response object, and turn it into a structured list of containers
// Return a list of containers, or an error if one occurs.
func (sl *Kubelet) ResponseToManifests(response *etcd.Response) ([]api.ContainerManifest, error) {
if response.Node == nil || len(response.Node.Value) == 0 {
return nil, fmt.Errorf("no nodes field: %#v", response)
}
var manifests []api.ContainerManifest
err := sl.ExtractYAMLData([]byte(response.Node.Value), &manifests)
return manifests, err
}
func (sl *Kubelet) getKubeletStateFromEtcd(key string, changeChannel chan<- []api.ContainerManifest) error {
response, err := sl.Client.Get(key+"/kubelet", true, false)
if err != nil {
log.Printf("Error on get on %s: %#v", key, err)
switch err.(type) {
case *etcd.EtcdError:
etcdError := err.(*etcd.EtcdError)
if etcdError.ErrorCode == 100 {
return nil
}
}
return err
}
manifests, err := sl.ResponseToManifests(response)
if err != nil {
log.Printf("Error parsing response (%#v): %s", response, err)
return err
}
log.Printf("Got initial state from etcd: %+v", manifests)
changeChannel <- manifests
return nil
}
// Sync with etcd, and set up an etcd watch for new configurations
// The channel to send new configurations across
// This function loops forever and is intended to be run in a go routine.
func (sl *Kubelet) SyncAndSetupEtcdWatch(changeChannel chan<- []api.ContainerManifest) {
hostname, err := exec.Command("hostname", "-f").Output()
if err != nil {
log.Printf("Couldn't determine hostname : %v", err)
return
}
key := "/registry/hosts/" + strings.TrimSpace(string(hostname))
// First fetch the initial configuration (watch only gives changes...)
for {
err = sl.getKubeletStateFromEtcd(key, changeChannel)
if err == nil {
// We got a successful response, etcd is up, set up the watch.
break
}
time.Sleep(30 * time.Second)
}
done := make(chan bool)
go util.Forever(func() { sl.TimeoutWatch(done) }, 0)
for {
// The etcd client will close the watch channel when it exits. So we need
// to create and service a new one every time.
watchChannel := make(chan *etcd.Response)
// We don't push this through Forever because if it dies, we just do it again in 30 secs.
// anyway.
go sl.WatchEtcd(watchChannel, changeChannel)
sl.getKubeletStateFromEtcd(key, changeChannel)
log.Printf("Setting up a watch for configuration changes in etcd for %s", key)
sl.Client.Watch(key, 0, true, watchChannel, done)
}
}
// Timeout the watch after 30 seconds
func (sl *Kubelet) TimeoutWatch(done chan bool) {
t := time.Tick(30 * time.Second)
for _ = range t {
done <- true
}
}
// Extract data from YAML file into a list of containers.
func (sl *Kubelet) ExtractYAMLData(buf []byte, output interface{}) error {
err := yaml.Unmarshal(buf, output)
if err != nil {
log.Printf("Couldn't unmarshal configuration: %v", err)
return err
}
return nil
}
// Watch etcd for changes, receives config objects from the etcd client watch.
// This function loops forever and is intended to be run as a goroutine.
func (sl *Kubelet) WatchEtcd(watchChannel <-chan *etcd.Response, changeChannel chan<- []api.ContainerManifest) {
defer util.HandleCrash()
for {
watchResponse := <-watchChannel
log.Printf("Got change: %#v", watchResponse)
// This means the channel has been closed.
if watchResponse == nil {
return
}
if watchResponse.Node == nil || len(watchResponse.Node.Value) == 0 {
log.Printf("No nodes field: %#v", watchResponse)
if watchResponse.Node != nil {
log.Printf("Node: %#v", watchResponse.Node)
}
}
log.Printf("Got data: %v", watchResponse.Node.Value)
var manifests []api.ContainerManifest
if err := sl.ExtractYAMLData([]byte(watchResponse.Node.Value), &manifests); err != nil {
continue
}
// Ok, we have a valid configuration, send to channel for
// rejiggering.
changeChannel <- manifests
}
}
// Sync the configured list of containers (desired state) with the host current state
func (sl *Kubelet) SyncManifests(config []api.ContainerManifest) error {
log.Printf("Desired:%#v", config)
var err error
desired := map[string]bool{}
for _, manifest := range config {
for _, element := range manifest.Containers {
var exists bool
exists, actualName, err := sl.ContainerExists(&manifest, &element)
if err != nil {
log.Printf("Error detecting container: %#v skipping.", err)
continue
}
if !exists {
log.Printf("%#v doesn't exist, creating", element)
actualName, err = sl.RunContainer(&manifest, &element)
// For some reason, list gives back names that start with '/'
actualName = "/" + actualName
if err != nil {
// TODO(bburns) : Perhaps blacklist a container after N failures?
log.Printf("Error creating container: %#v", err)
desired[actualName] = true
continue
}
} else {
log.Printf("%#v exists as %v", element.Name, actualName)
}
desired[actualName] = true
}
}
existingContainers, _ := sl.ListContainers()
log.Printf("Existing:\n%#v Desired: %#v", existingContainers, desired)
for _, container := range existingContainers {
if !desired[container] {
log.Printf("Killing: %s", container)
err = sl.KillContainer(container)
if err != nil {
log.Printf("Error killing container: %#v", err)
}
}
}
return err
}
// runSyncLoop is the main loop for processing changes. It watches for changes from
// four channels (file, etcd, server, and http) and creates a union of the two. For
// any new change seen, will run a sync against desired state and running state. If
// no changes are seen to the configuration, will synchronize the last known desired
// state every sync_frequency seconds.
// Never returns.
func (sl *Kubelet) RunSyncLoop(etcdChannel <-chan []api.ContainerManifest, fileChannel, serverChannel, httpChannel <-chan api.ContainerManifest, handler SyncHandler) {
var lastFile, lastEtcd, lastHttp, lastServer []api.ContainerManifest
for {
select {
case manifest := <-fileChannel:
log.Printf("Got new manifest from file... %v", manifest)
lastFile = []api.ContainerManifest{manifest}
case manifests := <-etcdChannel:
log.Printf("Got new configuration from etcd... %v", manifests)
lastEtcd = manifests
case manifest := <-httpChannel:
log.Printf("Got new manifest from external http... %v", manifest)
lastHttp = []api.ContainerManifest{manifest}
case manifest := <-serverChannel:
log.Printf("Got new manifest from our server... %v", manifest)
lastServer = []api.ContainerManifest{manifest}
case <-time.After(sl.SyncFrequency):
}
manifests := append([]api.ContainerManifest{}, lastFile...)
manifests = append(manifests, lastEtcd...)
manifests = append(manifests, lastHttp...)
manifests = append(manifests, lastServer...)
err := handler.SyncManifests(manifests)
if err != nil {
log.Printf("Couldn't sync containers : %#v", err)
}
}
}
func (sl *Kubelet) GetContainerInfo(name string) (string, error) {
info, err := sl.DockerClient.InspectContainer(name)
if err != nil {
return "{}", err
}
data, err := json.Marshal(info)
return string(data), err
}

View File

@@ -0,0 +1,80 @@
/*
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"
"io/ioutil"
"net/http"
"net/url"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"gopkg.in/v1/yaml"
)
type KubeletServer struct {
Kubelet *Kubelet
UpdateChannel chan api.ContainerManifest
}
func (s *KubeletServer) error(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Error: %#v", err)
}
func (s *KubeletServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
u, err := url.ParseRequestURI(req.RequestURI)
if err != nil {
s.error(w, err)
return
}
switch {
case u.Path == "/container":
defer req.Body.Close()
data, err := ioutil.ReadAll(req.Body)
if err != nil {
s.error(w, err)
return
}
var manifest api.ContainerManifest
err = yaml.Unmarshal(data, &manifest)
if err != nil {
s.error(w, err)
return
}
s.UpdateChannel <- manifest
case u.Path == "/containerInfo":
container := u.Query().Get("container")
if len(container) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "Missing container query arg.")
return
}
id, err := s.Kubelet.GetContainerID(container)
body, err := s.Kubelet.GetContainerInfo(id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Error: %#v", err)
return
}
w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, body)
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "Not found.")
}
}

562
pkg/kubelet/kubelet_test.go Normal file
View File

@@ -0,0 +1,562 @@
/*
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 (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
"github.com/fsouza/go-dockerclient"
)
// TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove.
func expectNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
// These are used for testing extract json (below)
type TestData struct {
Value string
Number int
}
type TestObject struct {
Name string
Data TestData
}
func verifyStringEquals(t *testing.T, actual, expected string) {
if actual != expected {
t.Errorf("Verification failed. Expected: %s, Found %s", expected, actual)
}
}
func verifyIntEquals(t *testing.T, actual, expected int) {
if actual != expected {
t.Errorf("Verification failed. Expected: %d, Found %d", expected, actual)
}
}
func verifyNoError(t *testing.T, e error) {
if e != nil {
t.Errorf("Expected no error, found %#v", e)
}
}
func verifyError(t *testing.T, e error) {
if e == nil {
t.Errorf("Expected error, found nil")
}
}
func TestExtractJSON(t *testing.T) {
obj := TestObject{}
kubelet := Kubelet{}
data := `{ "name": "foo", "data": { "value": "bar", "number": 10 } }`
kubelet.ExtractYAMLData([]byte(data), &obj)
verifyStringEquals(t, obj.Name, "foo")
verifyStringEquals(t, obj.Data.Value, "bar")
verifyIntEquals(t, obj.Data.Number, 10)
}
type FakeDockerClient struct {
containerList []docker.APIContainers
container *docker.Container
err error
called []string
}
func (f *FakeDockerClient) clearCalls() {
f.called = []string{}
}
func (f *FakeDockerClient) appendCall(call string) {
f.called = append(f.called, call)
}
func (f *FakeDockerClient) ListContainers(options docker.ListContainersOptions) ([]docker.APIContainers, error) {
f.appendCall("list")
return f.containerList, f.err
}
func (f *FakeDockerClient) InspectContainer(id string) (*docker.Container, error) {
f.appendCall("inspect")
return f.container, f.err
}
func (f *FakeDockerClient) CreateContainer(docker.CreateContainerOptions) (*docker.Container, error) {
f.appendCall("create")
return nil, nil
}
func (f *FakeDockerClient) StartContainer(id string, hostConfig *docker.HostConfig) error {
f.appendCall("start")
return nil
}
func (f *FakeDockerClient) StopContainer(id string, timeout uint) error {
f.appendCall("stop")
return nil
}
func verifyCalls(t *testing.T, fakeDocker FakeDockerClient, calls []string) {
verifyStringArrayEquals(t, fakeDocker.called, calls)
}
func verifyStringArrayEquals(t *testing.T, actual, expected []string) {
invalid := len(actual) != len(expected)
for ix, value := range actual {
if expected[ix] != value {
invalid = true
}
}
if invalid {
t.Errorf("Expected: %#v, Actual: %#v", expected, actual)
}
}
func verifyPackUnpack(t *testing.T, manifestId, containerName string) {
name := manifestAndContainerToDockerName(
&api.ContainerManifest{Id: manifestId},
&api.Container{Name: containerName},
)
returnedManifestId, returnedContainerName := dockerNameToManifestAndContainer(name)
if manifestId != returnedManifestId || containerName != returnedContainerName {
t.Errorf("For (%s, %s), unpacked (%s, %s)", manifestId, containerName, returnedManifestId, returnedContainerName)
}
}
func TestContainerManifestNaming(t *testing.T) {
verifyPackUnpack(t, "manifest1234", "container5678")
verifyPackUnpack(t, "manifest--", "container__")
verifyPackUnpack(t, "--manifest", "__container")
verifyPackUnpack(t, "m___anifest_", "container-_-")
verifyPackUnpack(t, "_m___anifest", "-_-container")
}
func TestContainerExists(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
manifest := api.ContainerManifest{
Id: "qux",
}
container := api.Container{
Name: "foo",
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo--qux--1234"},
},
docker.APIContainers{
Names: []string{"bar--qux--1234"},
},
}
fakeDocker.container = &docker.Container{
ID: "foobar",
}
exists, _, err := kubelet.ContainerExists(&manifest, &container)
verifyCalls(t, fakeDocker, []string{"list", "list", "inspect"})
if !exists {
t.Errorf("Failed to find container %#v", container)
}
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
func TestGetContainerID(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
ID: "1234",
},
docker.APIContainers{
Names: []string{"bar"},
ID: "4567",
},
}
id, err := kubelet.GetContainerID("foo")
verifyStringEquals(t, id, "1234")
verifyNoError(t, err)
verifyCalls(t, fakeDocker, []string{"list"})
fakeDocker.clearCalls()
id, err = kubelet.GetContainerID("bar")
verifyStringEquals(t, id, "4567")
verifyNoError(t, err)
verifyCalls(t, fakeDocker, []string{"list"})
fakeDocker.clearCalls()
id, err = kubelet.GetContainerID("NotFound")
verifyError(t, err)
verifyCalls(t, fakeDocker, []string{"list"})
}
func TestGetContainerByName(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
},
docker.APIContainers{
Names: []string{"bar"},
},
}
fakeDocker.container = &docker.Container{
ID: "foobar",
}
container, err := kubelet.GetContainerByName("foo")
verifyCalls(t, fakeDocker, []string{"list", "inspect"})
if container == nil {
t.Errorf("Unexpected nil container")
}
verifyStringEquals(t, container.ID, "foobar")
verifyNoError(t, err)
}
func TestListContainers(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
},
docker.APIContainers{
Names: []string{"bar"},
},
}
containers, err := kubelet.ListContainers()
verifyStringArrayEquals(t, containers, []string{"foo", "bar"})
verifyNoError(t, err)
verifyCalls(t, fakeDocker, []string{"list"})
}
func TestKillContainerWithError(t *testing.T) {
fakeDocker := FakeDockerClient{
err: fmt.Errorf("Sample Error"),
containerList: []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
},
docker.APIContainers{
Names: []string{"bar"},
},
},
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
err := kubelet.KillContainer("foo")
verifyError(t, err)
verifyCalls(t, fakeDocker, []string{"list"})
}
func TestKillContainer(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
},
docker.APIContainers{
Names: []string{"bar"},
},
}
fakeDocker.container = &docker.Container{
ID: "foobar",
}
err := kubelet.KillContainer("foo")
verifyNoError(t, err)
verifyCalls(t, fakeDocker, []string{"list", "stop"})
}
func TestSyncHTTP(t *testing.T) {
containers := api.ContainerManifest{
Containers: []api.Container{
api.Container{
Name: "foo",
Image: "dockerfile/foo",
},
api.Container{
Name: "bar",
Image: "dockerfile/bar",
},
},
}
data, _ := json.Marshal(containers)
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(data),
}
testServer := httptest.NewServer(&fakeHandler)
kubelet := Kubelet{}
var containersOut api.ContainerManifest
data, err := kubelet.SyncHTTP(&http.Client{}, testServer.URL, &containersOut)
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
if len(containers.Containers) != len(containersOut.Containers) {
t.Errorf("Container sizes don't match. Expected: %d Received %d, %#v", len(containers.Containers), len(containersOut.Containers), containersOut)
}
expectedData, _ := json.Marshal(containers)
actualData, _ := json.Marshal(containersOut)
if string(expectedData) != string(actualData) {
t.Errorf("Container data doesn't match. Expected: %s Received %s", string(expectedData), string(actualData))
}
}
func TestResponseToContainersNil(t *testing.T) {
kubelet := Kubelet{}
list, err := kubelet.ResponseToManifests(&etcd.Response{Node: nil})
if len(list) != 0 {
t.Errorf("Unexpected non-zero list: %#v", list)
}
if err == nil {
t.Error("Unexpected non-error")
}
}
func TestResponseToManifests(t *testing.T) {
kubelet := Kubelet{}
list, err := kubelet.ResponseToManifests(&etcd.Response{
Node: &etcd.Node{
Value: util.MakeJSONString([]api.ContainerManifest{
api.ContainerManifest{Id: "foo"},
api.ContainerManifest{Id: "bar"},
}),
},
})
if len(list) != 2 || list[0].Id != "foo" || list[1].Id != "bar" {
t.Errorf("Unexpected list: %#v", list)
}
expectNoError(t, err)
}
type channelReader struct {
list [][]api.ContainerManifest
wg sync.WaitGroup
}
func startReading(channel <-chan []api.ContainerManifest) *channelReader {
cr := &channelReader{}
cr.wg.Add(1)
go func() {
for {
containers, ok := <-channel
if !ok {
break
}
cr.list = append(cr.list, containers)
}
cr.wg.Done()
}()
return cr
}
func (cr *channelReader) GetList() [][]api.ContainerManifest {
cr.wg.Wait()
return cr.list
}
func TestGetKubeletStateFromEtcdNoData(t *testing.T) {
fakeClient := registry.MakeFakeEtcdClient(t)
kubelet := Kubelet{
Client: fakeClient,
}
channel := make(chan []api.ContainerManifest)
reader := startReading(channel)
fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{
R: &etcd.Response{},
E: nil,
}
err := kubelet.getKubeletStateFromEtcd("/registry/hosts/machine", channel)
if err == nil {
t.Error("Unexpected no err.")
}
close(channel)
list := reader.GetList()
if len(list) != 0 {
t.Errorf("Unexpected list: %#v", list)
}
}
func TestGetKubeletStateFromEtcd(t *testing.T) {
fakeClient := registry.MakeFakeEtcdClient(t)
kubelet := Kubelet{
Client: fakeClient,
}
channel := make(chan []api.ContainerManifest)
reader := startReading(channel)
fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Value: util.MakeJSONString([]api.Container{}),
},
},
E: nil,
}
err := kubelet.getKubeletStateFromEtcd("/registry/hosts/machine", channel)
expectNoError(t, err)
close(channel)
list := reader.GetList()
if len(list) != 1 {
t.Errorf("Unexpected list: %#v", list)
}
}
func TestGetKubeletStateFromEtcdNotFound(t *testing.T) {
fakeClient := registry.MakeFakeEtcdClient(t)
kubelet := Kubelet{
Client: fakeClient,
}
channel := make(chan []api.ContainerManifest)
reader := startReading(channel)
fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{
R: &etcd.Response{},
E: &etcd.EtcdError{
ErrorCode: 100,
},
}
err := kubelet.getKubeletStateFromEtcd("/registry/hosts/machine", channel)
expectNoError(t, err)
close(channel)
list := reader.GetList()
if len(list) != 0 {
t.Errorf("Unexpected list: %#v", list)
}
}
func TestGetKubeletStateFromEtcdError(t *testing.T) {
fakeClient := registry.MakeFakeEtcdClient(t)
kubelet := Kubelet{
Client: fakeClient,
}
channel := make(chan []api.ContainerManifest)
reader := startReading(channel)
fakeClient.Data["/registry/hosts/machine/kubelet"] = registry.EtcdResponseWithError{
R: &etcd.Response{},
E: &etcd.EtcdError{
ErrorCode: 200, // non not found error
},
}
err := kubelet.getKubeletStateFromEtcd("/registry/hosts/machine", channel)
if err == nil {
t.Error("Unexpected non-error")
}
close(channel)
list := reader.GetList()
if len(list) != 0 {
t.Errorf("Unexpected list: %#v", list)
}
}
func TestSyncManifestsDoesNothing(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
// format is <container-id>--<manifest-id>
Names: []string{"bar--foo"},
ID: "1234",
},
}
fakeDocker.container = &docker.Container{
ID: "1234",
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
err := kubelet.SyncManifests([]api.ContainerManifest{
api.ContainerManifest{
Id: "foo",
Containers: []api.Container{
api.Container{Name: "bar"},
},
},
})
expectNoError(t, err)
if len(fakeDocker.called) != 4 ||
fakeDocker.called[0] != "list" ||
fakeDocker.called[1] != "list" ||
fakeDocker.called[2] != "inspect" ||
fakeDocker.called[3] != "list" {
t.Errorf("Unexpected call sequence: %#v", fakeDocker.called)
}
}
func TestSyncManifestsDeletes(t *testing.T) {
fakeDocker := FakeDockerClient{
err: nil,
}
fakeDocker.containerList = []docker.APIContainers{
docker.APIContainers{
Names: []string{"foo"},
ID: "1234",
},
}
kubelet := Kubelet{
DockerClient: &fakeDocker,
}
err := kubelet.SyncManifests([]api.ContainerManifest{})
expectNoError(t, err)
if len(fakeDocker.called) != 3 ||
fakeDocker.called[0] != "list" ||
fakeDocker.called[1] != "list" ||
fakeDocker.called[2] != "stop" {
t.Errorf("Unexpected call sequence: %#v", fakeDocker.called)
}
}

320
pkg/proxy/config/config.go Normal file
View File

@@ -0,0 +1,320 @@
/*
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.
*/
// Config provides decoupling between various configuration sources (etcd, files,...) and
// the pieces that actually care about them (loadbalancer, proxy). Config takes 1 or more
// configuration sources and allows for incremental (add/remove) and full replace (set)
// changes from each of the sources, then creates a union of the configuration and provides
// a unified view for both service handlers as well as endpoint handlers. There is no attempt
// to resolve conflicts of any sort. Basic idea is that each configuration source gets a channel
// from the Config service and pushes updates to it via that channel. Config then keeps track of
// incremental & replace changes and distributes them to listeners as appropriate.
package config
import (
"log"
"sync"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type Operation int
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
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
type EndpointsUpdate struct {
Endpoints []api.Endpoints
Op Operation
}
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(services []api.Service)
}
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
// service comes up, or when containers come up or down for an existing service.
OnUpdate(endpoints []api.Endpoints)
}
type ServiceConfig struct {
// Configuration sources and their lock.
configSourceLock sync.RWMutex
serviceConfigSources map[string]chan ServiceUpdate
endpointsConfigSources map[string]chan EndpointsUpdate
// Handlers for changes to services and endpoints and their lock.
handlerLock sync.RWMutex
serviceHandlers []ServiceConfigHandler
endpointHandlers []EndpointsConfigHandler
// Last known configuration for union of the sources and the locks. Map goes
// from each source to array of services/endpoints that have been configured
// through that channel.
configLock sync.RWMutex
serviceConfig map[string]map[string]api.Service
endpointConfig map[string]map[string]api.Endpoints
// Channel that service configuration source listeners use to signal of new
// configurations.
// Value written is the source of the change.
serviceNotifyChannel chan string
// Channel that endpoint configuration source listeners use to signal of new
// configurations.
// Value written is the source of the change.
endpointsNotifyChannel chan string
}
func NewServiceConfig() ServiceConfig {
config := ServiceConfig{
serviceConfigSources: make(map[string]chan ServiceUpdate),
endpointsConfigSources: make(map[string]chan EndpointsUpdate),
serviceHandlers: make([]ServiceConfigHandler, 10),
endpointHandlers: make([]EndpointsConfigHandler, 10),
serviceConfig: make(map[string]map[string]api.Service),
endpointConfig: make(map[string]map[string]api.Endpoints),
serviceNotifyChannel: make(chan string),
endpointsNotifyChannel: make(chan string),
}
go config.Run()
return config
}
func (impl *ServiceConfig) Run() {
log.Printf("Starting the config Run loop")
for {
select {
case source := <-impl.serviceNotifyChannel:
log.Printf("Got new service configuration from source %s", source)
impl.NotifyServiceUpdate()
case source := <-impl.endpointsNotifyChannel:
log.Printf("Got new endpoint configuration from source %s", source)
impl.NotifyEndpointsUpdate()
case <-time.After(1 * time.Second):
}
}
}
func (impl *ServiceConfig) ServiceChannelListener(source string, listenChannel chan ServiceUpdate) {
// Represents the current services configuration for this channel.
serviceMap := make(map[string]api.Service)
for {
select {
case update := <-listenChannel:
switch update.Op {
case ADD:
log.Printf("Adding new service from source %s : %v", source, update.Services)
for _, value := range update.Services {
serviceMap[value.ID] = value
}
case REMOVE:
log.Printf("Removing a service %v", update)
for _, value := range update.Services {
delete(serviceMap, value.ID)
}
case SET:
log.Printf("Setting services %v", update)
// Clear the old map entries by just creating a new map
serviceMap = make(map[string]api.Service)
for _, value := range update.Services {
serviceMap[value.ID] = value
}
default:
log.Printf("Received invalid update type: %v", update)
continue
}
impl.configLock.Lock()
impl.serviceConfig[source] = serviceMap
impl.configLock.Unlock()
impl.serviceNotifyChannel <- source
}
}
}
func (impl *ServiceConfig) EndpointsChannelListener(source string, listenChannel chan EndpointsUpdate) {
endpointMap := make(map[string]api.Endpoints)
for {
select {
case update := <-listenChannel:
switch update.Op {
case ADD:
log.Printf("Adding a new endpoint %v", update)
for _, value := range update.Endpoints {
endpointMap[value.Name] = value
}
case REMOVE:
log.Printf("Removing an endpoint %v", update)
for _, value := range update.Endpoints {
delete(endpointMap, value.Name)
}
case SET:
log.Printf("Setting services %v", update)
// Clear the old map entries by just creating a new map
endpointMap = make(map[string]api.Endpoints)
for _, value := range update.Endpoints {
endpointMap[value.Name] = value
}
default:
log.Printf("Received invalid update type: %v", update)
continue
}
impl.configLock.Lock()
impl.endpointConfig[source] = endpointMap
impl.configLock.Unlock()
impl.endpointsNotifyChannel <- source
}
}
}
// GetServiceConfigurationChannel returns a channel where a configuration source
// can send updates of new service 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
// union.
func (impl *ServiceConfig) GetServiceConfigurationChannel(source string) chan ServiceUpdate {
if len(source) == 0 {
panic("GetServiceConfigurationChannel given an empty service name")
}
impl.configSourceLock.Lock()
defer impl.configSourceLock.Unlock()
channel, exists := impl.serviceConfigSources[source]
if exists {
return channel
}
newChannel := make(chan ServiceUpdate)
impl.serviceConfigSources[source] = newChannel
go impl.ServiceChannelListener(source, newChannel)
return newChannel
}
// GetEndpointConfigurationChannel 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
// union.
func (impl *ServiceConfig) GetEndpointsConfigurationChannel(source string) chan EndpointsUpdate {
if len(source) == 0 {
panic("GetEndpointConfigurationChannel given an empty service name")
}
impl.configSourceLock.Lock()
defer impl.configSourceLock.Unlock()
channel, exists := impl.endpointsConfigSources[source]
if exists {
return channel
}
newChannel := make(chan EndpointsUpdate)
impl.endpointsConfigSources[source] = newChannel
go impl.EndpointsChannelListener(source, newChannel)
return newChannel
}
// Register ServiceConfigHandler to receive updates of changes to services.
func (impl *ServiceConfig) RegisterServiceHandler(handler ServiceConfigHandler) {
impl.handlerLock.Lock()
defer impl.handlerLock.Unlock()
for i, h := range impl.serviceHandlers {
if h == nil {
impl.serviceHandlers[i] = handler
return
}
}
// TODO(vaikas): Grow the array here instead of panic.
// In practice we are expecting there to be 1 handler anyways,
// so not a big deal for now
panic("Only up to 10 service handlers supported for now")
}
// Register ServiceConfigHandler to receive updates of changes to services.
func (impl *ServiceConfig) RegisterEndpointsHandler(handler EndpointsConfigHandler) {
impl.handlerLock.Lock()
defer impl.handlerLock.Unlock()
for i, h := range impl.endpointHandlers {
if h == nil {
impl.endpointHandlers[i] = handler
return
}
}
// TODO(vaikas): Grow the array here instead of panic.
// In practice we are expecting there to be 1 handler anyways,
// so not a big deal for now
panic("Only up to 10 endpoint handlers supported for now")
}
func (impl *ServiceConfig) NotifyServiceUpdate() {
services := make([]api.Service, 0)
impl.configLock.RLock()
for _, sourceServices := range impl.serviceConfig {
for _, value := range sourceServices {
services = append(services, value)
}
}
impl.configLock.RUnlock()
log.Printf("Unified configuration %+v", services)
impl.handlerLock.RLock()
handlers := impl.serviceHandlers
impl.handlerLock.RUnlock()
for _, handler := range handlers {
if handler != nil {
handler.OnUpdate(services)
}
}
}
func (impl *ServiceConfig) NotifyEndpointsUpdate() {
endpoints := make([]api.Endpoints, 0)
impl.configLock.RLock()
for _, sourceEndpoints := range impl.endpointConfig {
for _, value := range sourceEndpoints {
endpoints = append(endpoints, value)
}
}
impl.configLock.RUnlock()
log.Printf("Unified configuration %+v", endpoints)
impl.handlerLock.RLock()
handlers := impl.endpointHandlers
impl.handlerLock.RUnlock()
for _, handler := range handlers {
if handler != nil {
handler.OnUpdate(endpoints)
}
}
}

View File

@@ -0,0 +1,240 @@
/*
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 config
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
const TomcatPort int = 8080
const TomcatName = "tomcat"
var TomcatEndpoints = map[string]string{"c0": "1.1.1.1:18080", "c1": "2.2.2.2:18081"}
const MysqlPort int = 3306
const MysqlName = "mysql"
var MysqlEndpoints = map[string]string{"c0": "1.1.1.1:13306", "c3": "2.2.2.2:13306"}
type ServiceHandlerMock struct {
services []api.Service
}
func NewServiceHandlerMock() ServiceHandlerMock {
return ServiceHandlerMock{services: make([]api.Service, 0)}
}
func (impl ServiceHandlerMock) OnUpdate(services []api.Service) {
impl.services = services
}
func (impl ServiceHandlerMock) ValidateServices(t *testing.T, expectedServices []api.Service) {
if reflect.DeepEqual(impl.services, expectedServices) {
t.Errorf("Services don't match %+v expected: %+v", impl.services, expectedServices)
}
}
type EndpointsHandlerMock struct {
endpoints []api.Endpoints
}
func NewEndpointsHandlerMock() EndpointsHandlerMock {
return EndpointsHandlerMock{endpoints: make([]api.Endpoints, 0)}
}
func (impl EndpointsHandlerMock) OnUpdate(endpoints []api.Endpoints) {
impl.endpoints = endpoints
}
func (impl EndpointsHandlerMock) ValidateEndpoints(t *testing.T, expectedEndpoints []api.Endpoints) {
if reflect.DeepEqual(impl.endpoints, expectedEndpoints) {
t.Errorf("Endpoints don't match %+v", impl.endpoints, expectedEndpoints)
}
}
func CreateServiceUpdate(op Operation, services ...api.Service) ServiceUpdate {
ret := ServiceUpdate{Op: op}
ret.Services = make([]api.Service, len(services))
for i, value := range services {
ret.Services[i] = value
}
return ret
}
func CreateEndpointsUpdate(op Operation, endpoints ...api.Endpoints) EndpointsUpdate {
ret := EndpointsUpdate{Op: op}
ret.Endpoints = make([]api.Endpoints, len(endpoints))
for i, value := range endpoints {
ret.Endpoints[i] = value
}
return ret
}
func TestServiceConfigurationChannels(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetServiceConfigurationChannel("one")
if channelOne != config.GetServiceConfigurationChannel("one") {
t.Error("Didn't get the same service configuration channel back with the same name")
}
channelTwo := config.GetServiceConfigurationChannel("two")
if channelOne == channelTwo {
t.Error("Got back the same service configuration channel for different names")
}
}
func TestEndpointConfigurationChannels(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetEndpointsConfigurationChannel("one")
if channelOne != config.GetEndpointsConfigurationChannel("one") {
t.Error("Didn't get the same endpoint configuration channel back with the same name")
}
channelTwo := config.GetEndpointsConfigurationChannel("two")
if channelOne == channelTwo {
t.Error("Got back the same endpoint configuration channel for different names")
}
}
func TestNewServiceAddedAndNotified(t *testing.T) {
config := NewServiceConfig()
channel := config.GetServiceConfigurationChannel("one")
handler := NewServiceHandlerMock()
config.RegisterServiceHandler(&handler)
serviceUpdate := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "foo"}, Port: 10})
channel <- serviceUpdate
handler.ValidateServices(t, serviceUpdate.Services)
}
func TestServiceAddedRemovedSetAndNotified(t *testing.T) {
config := NewServiceConfig()
channel := config.GetServiceConfigurationChannel("one")
handler := NewServiceHandlerMock()
config.RegisterServiceHandler(&handler)
serviceUpdate := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "foo"}, Port: 10})
channel <- serviceUpdate
handler.ValidateServices(t, serviceUpdate.Services)
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "bar"}, Port: 20})
channel <- serviceUpdate2
services := []api.Service{serviceUpdate.Services[0], serviceUpdate2.Services[0]}
handler.ValidateServices(t, services)
serviceUpdate3 := CreateServiceUpdate(REMOVE, api.Service{JSONBase: api.JSONBase{ID: "foo"}})
channel <- serviceUpdate3
services = []api.Service{serviceUpdate2.Services[0]}
handler.ValidateServices(t, services)
serviceUpdate4 := CreateServiceUpdate(SET, api.Service{JSONBase: api.JSONBase{ID: "foobar"}, Port: 99})
channel <- serviceUpdate4
services = []api.Service{serviceUpdate4.Services[0]}
handler.ValidateServices(t, services)
}
func TestNewMultipleSourcesServicesAddedAndNotified(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetServiceConfigurationChannel("one")
channelTwo := config.GetServiceConfigurationChannel("two")
if channelOne == channelTwo {
t.Error("Same channel handed back for one and two")
}
handler := NewServiceHandlerMock()
config.RegisterServiceHandler(handler)
serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "foo"}, Port: 10})
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "bar"}, Port: 20})
channelOne <- serviceUpdate1
channelTwo <- serviceUpdate2
services := []api.Service{serviceUpdate1.Services[0], serviceUpdate2.Services[0]}
handler.ValidateServices(t, services)
}
func TestNewMultipleSourcesServicesMultipleHandlersAddedAndNotified(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetServiceConfigurationChannel("one")
channelTwo := config.GetServiceConfigurationChannel("two")
handler := NewServiceHandlerMock()
handler2 := NewServiceHandlerMock()
config.RegisterServiceHandler(handler)
config.RegisterServiceHandler(handler2)
serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "foo"}, Port: 10})
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{JSONBase: api.JSONBase{ID: "bar"}, Port: 20})
channelOne <- serviceUpdate1
channelTwo <- serviceUpdate2
services := []api.Service{serviceUpdate1.Services[0], serviceUpdate2.Services[0]}
handler.ValidateServices(t, services)
handler2.ValidateServices(t, services)
}
func TestNewMultipleSourcesEndpointsMultipleHandlersAddedAndNotified(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetEndpointsConfigurationChannel("one")
channelTwo := config.GetEndpointsConfigurationChannel("two")
handler := NewEndpointsHandlerMock()
handler2 := NewEndpointsHandlerMock()
config.RegisterEndpointsHandler(handler)
config.RegisterEndpointsHandler(handler2)
endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1", "endpoint2"}})
endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "bar", Endpoints: []string{"endpoint3", "endpoint4"}})
channelOne <- endpointsUpdate1
channelTwo <- endpointsUpdate2
endpoints := []api.Endpoints{endpointsUpdate1.Endpoints[0], endpointsUpdate2.Endpoints[0]}
handler.ValidateEndpoints(t, endpoints)
handler2.ValidateEndpoints(t, endpoints)
}
func TestNewMultipleSourcesEndpointsMultipleHandlersAddRemoveSetAndNotified(t *testing.T) {
config := NewServiceConfig()
channelOne := config.GetEndpointsConfigurationChannel("one")
channelTwo := config.GetEndpointsConfigurationChannel("two")
handler := NewEndpointsHandlerMock()
handler2 := NewEndpointsHandlerMock()
config.RegisterEndpointsHandler(handler)
config.RegisterEndpointsHandler(handler2)
endpointsUpdate1 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1", "endpoint2"}})
endpointsUpdate2 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "bar", Endpoints: []string{"endpoint3", "endpoint4"}})
channelOne <- endpointsUpdate1
channelTwo <- endpointsUpdate2
endpoints := []api.Endpoints{endpointsUpdate1.Endpoints[0], endpointsUpdate2.Endpoints[0]}
handler.ValidateEndpoints(t, endpoints)
handler2.ValidateEndpoints(t, endpoints)
// Add one more
endpointsUpdate3 := CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foobar", Endpoints: []string{"endpoint5", "endpoint6"}})
channelTwo <- endpointsUpdate3
endpoints = []api.Endpoints{endpointsUpdate1.Endpoints[0], endpointsUpdate2.Endpoints[0], endpointsUpdate3.Endpoints[0]}
handler.ValidateEndpoints(t, endpoints)
handler2.ValidateEndpoints(t, endpoints)
// Update the "foo" service with new endpoints
endpointsUpdate1 = CreateEndpointsUpdate(ADD, api.Endpoints{Name: "foo", Endpoints: []string{"endpoint77"}})
channelOne <- endpointsUpdate1
endpoints = []api.Endpoints{endpointsUpdate1.Endpoints[0], endpointsUpdate2.Endpoints[0], endpointsUpdate3.Endpoints[0]}
handler.ValidateEndpoints(t, endpoints)
handler2.ValidateEndpoints(t, endpoints)
// Remove "bar" service
endpointsUpdate2 = CreateEndpointsUpdate(REMOVE, api.Endpoints{Name: "bar"})
channelTwo <- endpointsUpdate2
endpoints = []api.Endpoints{endpointsUpdate1.Endpoints[0], endpointsUpdate3.Endpoints[0]}
handler.ValidateEndpoints(t, endpoints)
handler2.ValidateEndpoints(t, endpoints)
}

227
pkg/proxy/config/etcd.go Normal file
View File

@@ -0,0 +1,227 @@
/*
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.
*/
// Watches etcd and gets the full configuration on preset intervals.
// 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
//
// The port that proxy needs to listen in for each service is a value in:
// registry/services/<service>
//
// The endpoints for each of the services found is a json string
// representing that service at:
// /registry/services/<service>/endpoint
// and the format is:
// '[ { "machine": <host>, "name": <name", "port": <port> },
// { "machine": <host2>, "name": <name2", "port": <port2> }
// ]',
//
package config
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/coreos/go-etcd/etcd"
)
const RegistryRoot = "registry/services"
type ConfigSourceEtcd struct {
client *etcd.Client
serviceChannel chan ServiceUpdate
endpointsChannel chan EndpointsUpdate
}
func NewConfigSourceEtcd(client *etcd.Client, serviceChannel chan ServiceUpdate, endpointsChannel chan EndpointsUpdate) ConfigSourceEtcd {
config := ConfigSourceEtcd{
client: client,
serviceChannel: serviceChannel,
endpointsChannel: endpointsChannel,
}
go config.Run()
return config
}
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()
if err == nil {
break
}
log.Printf("Failed to get any services: %v", err)
time.Sleep(2 * time.Second)
}
if len(services) > 0 {
serviceUpdate := ServiceUpdate{Op: SET, Services: services}
impl.serviceChannel <- serviceUpdate
}
if len(endpoints) > 0 {
endpointsUpdate := EndpointsUpdate{Op: SET, Endpoints: endpoints}
impl.endpointsChannel <- endpointsUpdate
}
// Ok, so we got something back from etcd. Let's set up a watch for new services, and
// their endpoints
go impl.WatchForChanges()
for {
services, endpoints, err = impl.GetServices()
if err != nil {
log.Printf("ConfigSourceEtcd: Failed to get services: %v", err)
} else {
if len(services) > 0 {
serviceUpdate := ServiceUpdate{Op: SET, Services: services}
impl.serviceChannel <- serviceUpdate
}
if len(endpoints) > 0 {
endpointsUpdate := EndpointsUpdate{Op: SET, Endpoints: endpoints}
impl.endpointsChannel <- endpointsUpdate
}
}
time.Sleep(30 * time.Second)
}
}
// 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)
if err != nil {
log.Printf("Failed to get the key %s: %v", RegistryRoot, err)
return make([]api.Service, 0), make([]api.Endpoints, 0), err
}
if response.Node.Dir == true {
retServices := make([]api.Service, len(response.Node.Nodes))
retEndpoints := make([]api.Endpoints, len(response.Node.Nodes))
// Ok, so we have directories, this list should be the list
// of services. Find the local port to listen on and remote endpoints
// and create a Service entry for it.
for i, node := range response.Node.Nodes {
var svc api.Service
err = json.Unmarshal([]byte(node.Value), &svc)
if err != nil {
log.Printf("Failed to load Service: %s (%#v)", node.Value, err)
continue
}
retServices[i] = svc
endpoints, err := impl.GetEndpoints(svc.ID)
if err != nil {
log.Printf("Couldn't get endpoints for %s : %v skipping", svc.ID, err)
}
log.Printf("Got service: %s on localport %d mapping to: %s", svc.ID, svc.Port, endpoints)
retEndpoints[i] = endpoints
}
return retServices, retEndpoints, err
}
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)
response, err := impl.client.Get(key, true, false)
if err != nil {
log.Printf("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)
}
// EtcdResponseToServiceAndLocalport 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)
}
var svc api.Service
err := json.Unmarshal([]byte(response.Node.Value), &svc)
if err != nil {
return nil, err
}
return &svc, err
}
func ParseEndpoints(jsonString string) (api.Endpoints, error) {
var e api.Endpoints
err := json.Unmarshal([]byte(jsonString), &e)
return e, err
}
func (impl ConfigSourceEtcd) WatchForChanges() {
log.Print("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)
}
}
func (impl ConfigSourceEtcd) ProcessChange(response *etcd.Response) {
log.Printf("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)
} else if response.Action == "set" {
service, err := EtcdResponseToService(response)
if err != nil {
log.Printf("Failed to parse %s Port: %s", response, err)
return
}
log.Printf("New service added/updated: %#v", service)
serviceUpdate := ServiceUpdate{Op: ADD, Services: []api.Service{*service}}
impl.serviceChannel <- serviceUpdate
return
}
if response.Action == "delete" {
parts := strings.Split(response.Node.Key[1:], "/")
if len(parts) == 4 {
log.Printf("Deleting service: %s", parts[3])
serviceUpdate := ServiceUpdate{Op: REMOVE, Services: []api.Service{api.Service{JSONBase: api.JSONBase{ID: parts[3]}}}}
impl.serviceChannel <- serviceUpdate
return
} else {
log.Printf("Unknown service delete: %#v", parts)
}
}
}
func (impl ConfigSourceEtcd) ProcessEndpointResponse(response *etcd.Response) {
log.Printf("Processing a change in endpoint configuration... %s", *response)
var endpoints api.Endpoints
err := json.Unmarshal([]byte(response.Node.Value), &endpoints)
if err != nil {
log.Printf("Failed to parse service out of etcd key: %v : %+v", response.Node.Value, err)
return
}
endpointsUpdate := EndpointsUpdate{Op: ADD, Endpoints: []api.Endpoints{endpoints}}
impl.endpointsChannel <- endpointsUpdate
}

View File

@@ -0,0 +1,56 @@
/*
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 config
import (
"encoding/json"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
const TomcatContainerEtcdKey = "/registry/services/tomcat/endpoints/tomcat-3bd5af34"
const TomcatService = "tomcat"
const TomcatContainerId = "tomcat-3bd5af34"
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)
}
if err != nil && !expectError {
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)
}
}
func TestParseJsonEndpoints(t *testing.T) {
ValidateJsonParsing(t, "", api.Endpoints{}, true)
endpoints := api.Endpoints{
Name: "foo",
Endpoints: []string{"foo", "bar", "baz"},
}
data, err := json.Marshal(endpoints)
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)
}

111
pkg/proxy/config/file.go Normal file
View File

@@ -0,0 +1,111 @@
/*
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.
*/
// Reads the configuration from the file. Example file for two services [nodejs & mysql]
//{"Services": [
// {
// "Name":"nodejs",
// "Port":10000,
// "Endpoints":["10.240.180.168:8000", "10.240.254.199:8000", "10.240.62.150:8000"]
// },
// {
// "Name":"mysql",
// "Port":10001,
// "Endpoints":["10.240.180.168:9000", "10.240.254.199:9000", "10.240.62.150:9000"]
// }
//]
//}
package config
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"reflect"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// TODO: kill this struct.
type ServiceJSON struct {
Name string
Port int
Endpoints []string
}
type ConfigFile struct {
Services []ServiceJSON
}
type ConfigSourceFile struct {
serviceChannel chan ServiceUpdate
endpointsChannel chan EndpointsUpdate
filename string
}
func NewConfigSourceFile(filename string, serviceChannel chan ServiceUpdate, endpointsChannel chan EndpointsUpdate) ConfigSourceFile {
config := ConfigSourceFile{
filename: filename,
serviceChannel: serviceChannel,
endpointsChannel: endpointsChannel,
}
go config.Run()
return config
}
func (impl ConfigSourceFile) Run() {
log.Printf("Watching file %s", impl.filename)
var lastData []byte
var lastServices []api.Service
var lastEndpoints []api.Endpoints
for {
data, err := ioutil.ReadFile(impl.filename)
if err != nil {
log.Printf("Couldn't read file: %s : %v", impl.filename, err)
} else {
var config ConfigFile
err = json.Unmarshal(data, &config)
if err != nil {
log.Printf("Couldn't unmarshal configuration from file : %s %v", data, err)
} else {
if !bytes.Equal(lastData, data) {
lastData = data
// Ok, we have a valid configuration, send to channel for
// rejiggering.
newServices := make([]api.Service, len(config.Services))
newEndpoints := make([]api.Endpoints, len(config.Services))
for i, service := range config.Services {
newServices[i] = api.Service{JSONBase: api.JSONBase{ID: service.Name}, Port: service.Port}
newEndpoints[i] = api.Endpoints{Name: service.Name, Endpoints: service.Endpoints}
}
if !reflect.DeepEqual(lastServices, newServices) {
serviceUpdate := ServiceUpdate{Op: SET, Services: newServices}
impl.serviceChannel <- serviceUpdate
lastServices = newServices
}
if !reflect.DeepEqual(lastEndpoints, newEndpoints) {
endpointsUpdate := EndpointsUpdate{Op: SET, Endpoints: newEndpoints}
impl.endpointsChannel <- endpointsUpdate
lastEndpoints = newEndpoints
}
}
}
}
time.Sleep(5 * time.Second)
}
}

29
pkg/proxy/loadbalancer.go Normal file
View File

@@ -0,0 +1,29 @@
/*
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.
*/
// Loadbalancer interface. Implementations use loadbalancer_<strategy> naming.
package proxy
import (
"net"
)
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
// well as the source making the connection.
LoadBalance(service string, srcAddr net.Addr) (string, error)
}

117
pkg/proxy/proxier.go Normal file
View File

@@ -0,0 +1,117 @@
/*
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.
*/
// Simple proxy for tcp connections between a localhost:lport and services that provide
// the actual implementations.
package proxy
import (
"fmt"
"io"
"log"
"net"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type Proxier struct {
loadBalancer LoadBalancer
serviceMap map[string]int
}
func NewProxier(loadBalancer LoadBalancer) *Proxier {
return &Proxier{loadBalancer: loadBalancer, serviceMap: make(map[string]int)}
}
func CopyBytes(in, out *net.TCPConn) {
log.Printf("Copying from %v <-> %v <-> %v <-> %v",
in.RemoteAddr(), in.LocalAddr(), out.LocalAddr(), out.RemoteAddr())
_, err := io.Copy(in, out)
if err != nil && err != io.EOF {
log.Printf("I/O error: %v", err)
}
in.CloseRead()
out.CloseWrite()
}
// Create a bidirectional byte shuffler. Copies bytes to/from each connection.
func ProxyConnection(in, out *net.TCPConn) {
log.Printf("Creating proxy between %v <-> %v <-> %v <-> %v",
in.RemoteAddr(), in.LocalAddr(), out.LocalAddr(), out.RemoteAddr())
go CopyBytes(in, out)
go CopyBytes(out, in)
}
func (proxier Proxier) AcceptHandler(service string, listener net.Listener) {
for {
inConn, err := listener.Accept()
if err != nil {
log.Printf("Accept failed: %v", err)
continue
}
log.Printf("Accepted connection from: %v to %v", inConn.RemoteAddr(), inConn.LocalAddr())
// Figure out where this request should go.
endpoint, err := proxier.loadBalancer.LoadBalance(service, inConn.RemoteAddr())
if err != nil {
log.Printf("Couldn't find an endpoint for %s %v", service, err)
inConn.Close()
continue
}
log.Printf("Mapped service %s to endpoint %s", service, endpoint)
outConn, err := net.DialTimeout("tcp", endpoint, time.Duration(5)*time.Second)
// We basically need to take everything from inConn and send to outConn
// and anything coming from outConn needs to be sent to inConn.
if err != nil {
log.Printf("Dial failed: %v", err)
inConn.Close()
continue
}
go 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 {
// Make sure we can start listening on the port before saying all's well.
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
log.Printf("Listening for %s on %d", service, port)
// If that succeeds, start the accepting loop.
go proxier.AcceptHandler(service, ln)
return nil
}
func (proxier Proxier) OnUpdate(services []api.Service) {
log.Printf("Received update notice: %+v", services)
for _, service := range services {
port, exists := proxier.serviceMap[service.ID]
if !exists || port != service.Port {
log.Printf("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 {
log.Printf("Failed to start listening for %s on %d", service.ID, service.Port)
}
}
}
}

73
pkg/proxy/proxier_test.go Normal file
View File

@@ -0,0 +1,73 @@
/*
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 proxy
import (
"fmt"
"io"
"net"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// a simple echoServer that only accept one connection
func echoServer(addr string) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to start echo service: %v", err)
}
defer l.Close()
conn, err := l.Accept()
if err != nil {
return fmt.Errorf("failed to accept new conn to echo service: %v", err)
}
io.Copy(conn, conn)
conn.Close()
return nil
}
func TestProxy(t *testing.T) {
go func() {
if err := echoServer("127.0.0.1:2222"); err != nil {
t.Fatal(err)
}
}()
lb := NewLoadBalancerRR()
lb.OnUpdate([]api.Endpoints{{"echo", []string{"127.0.0.1:2222"}}})
p := NewProxier(lb)
if err := p.AddService("echo", 2223); err != nil {
t.Fatalf("error adding new service: %v", err)
}
conn, err := net.Dial("tcp", "127.0.0.1:2223")
if err != nil {
t.Fatalf("error connecting to proxy: %v", err)
}
magic := "aaaaa"
if _, err := conn.Write([]byte(magic)); err != nil {
t.Fatalf("error writing to proxy: %v", err)
}
buf := make([]byte, 5)
if _, err := conn.Read(buf); err != nil {
t.Fatalf("error reading from proxy: %v", err)
}
if string(buf) != magic {
t.Fatalf("bad echo from proxy: got: %q, expected %q", string(buf), magic)
}
}

103
pkg/proxy/roundrobbin.go Normal file
View File

@@ -0,0 +1,103 @@
/*
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.
*/
// RoundRobin Loadbalancer
package proxy
import (
"errors"
"log"
"net"
"reflect"
"strconv"
"strings"
"sync"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type LoadBalancerRR struct {
lock sync.RWMutex
endpointsMap map[string][]string
rrIndex map[string]int
}
func NewLoadBalancerRR() *LoadBalancerRR {
return &LoadBalancerRR{endpointsMap: make(map[string][]string), rrIndex: make(map[string]int)}
}
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 {
return "", errors.New("no service entry for:" + service)
}
if len(endpoints) == 0 {
return "", errors.New("no endpoints for: " + service)
}
endpoint := endpoints[index]
impl.rrIndex[service] = (index + 1) % len(endpoints)
return endpoint, nil
}
func (impl LoadBalancerRR) IsValid(spec string) bool {
index := strings.Index(spec, ":")
if index == -1 {
return false
}
value, err := strconv.Atoi(spec[index+1:])
if err != nil {
return false
}
return value > 0
}
func (impl LoadBalancerRR) FilterValidEndpoints(endpoints []string) []string {
var result []string
for _, spec := range endpoints {
if impl.IsValid(spec) {
result = append(result, spec)
}
}
return result
}
func (impl LoadBalancerRR) OnUpdate(endpoints []api.Endpoints) {
tmp := make(map[string]bool)
impl.lock.Lock()
defer impl.lock.Unlock()
// First update / add all new endpoints for services.
for _, value := range endpoints {
existingEndpoints, exists := impl.endpointsMap[value.Name]
if !exists || !reflect.DeepEqual(value.Endpoints, existingEndpoints) {
log.Printf("LoadBalancerRR: Setting endpoints for %s to %+v", value.Name, value.Endpoints)
impl.endpointsMap[value.Name] = impl.FilterValidEndpoints(value.Endpoints)
// Start RR from the beginning if added or updated.
impl.rrIndex[value.Name] = 0
}
tmp[value.Name] = true
}
// Then remove any endpoints no longer relevant
for key, value := range impl.endpointsMap {
_, exists := tmp[key]
if !exists {
log.Printf("LoadBalancerRR: Removing endpoints for %s -> %+v", key, value)
delete(impl.endpointsMap, key)
}
}
}

View File

@@ -0,0 +1,178 @@
/*
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 proxy
import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func TestLoadBalanceValidateWorks(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
if loadBalancer.IsValid("") {
t.Errorf("Didn't fail for empty string")
}
if loadBalancer.IsValid("foobar") {
t.Errorf("Didn't fail with no port")
}
if loadBalancer.IsValid("foobar:-1") {
t.Errorf("Didn't fail with a negative port")
}
if !loadBalancer.IsValid("foobar:8080") {
t.Errorf("Failed a valid config.")
}
}
func TestLoadBalanceFilterWorks(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoints := []string{"foobar:1", "foobar:2", "foobar:-1", "foobar:3", "foobar:-2"}
filtered := loadBalancer.FilterValidEndpoints(endpoints)
if len(filtered) != 3 {
t.Errorf("Failed to filter to the correct size")
}
if filtered[0] != "foobar:1" {
t.Errorf("Index zero is not foobar:1")
}
if filtered[1] != "foobar:2" {
t.Errorf("Index one is not foobar:2")
}
if filtered[2] != "foobar:3" {
t.Errorf("Index two is not foobar:3")
}
}
func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoints := make([]api.Endpoints, 0)
loadBalancer.OnUpdate(endpoints)
endpoint, err := loadBalancer.LoadBalance("foo", nil)
if err == nil {
t.Errorf("Didn't fail with non-existent service")
}
if len(endpoint) != 0 {
t.Errorf("Got an endpoint")
}
}
func expectEndpoint(t *testing.T, loadBalancer *LoadBalancerRR, service string, expected string) {
endpoint, err := loadBalancer.LoadBalance(service, nil)
if err != nil {
t.Errorf("Didn't find a service for %s, expected %s, failed with: %v", service, expected, err)
}
if endpoint != expected {
t.Errorf("Didn't get expected endpoint for service %s, expected %s, got: %s", service, expected, endpoint)
}
}
func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoint, err := loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint1:40"}}
loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, "foo", "endpoint1:40")
expectEndpoint(t, loadBalancer, "foo", "endpoint1:40")
expectEndpoint(t, loadBalancer, "foo", "endpoint1:40")
expectEndpoint(t, loadBalancer, "foo", "endpoint1:40")
}
func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoint, err := loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}}
loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
expectEndpoint(t, loadBalancer, "foo", "endpoint:2")
expectEndpoint(t, loadBalancer, "foo", "endpoint:3")
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
}
func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoint, err := loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}}
loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
expectEndpoint(t, loadBalancer, "foo", "endpoint:2")
expectEndpoint(t, loadBalancer, "foo", "endpoint:3")
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
expectEndpoint(t, loadBalancer, "foo", "endpoint:2")
// Then update the configuration with one fewer endpoints, make sure
// we start in the beginning again
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:8", "endpoint:9"}}
loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, "foo", "endpoint:8")
expectEndpoint(t, loadBalancer, "foo", "endpoint:9")
expectEndpoint(t, loadBalancer, "foo", "endpoint:8")
expectEndpoint(t, loadBalancer, "foo", "endpoint:9")
// Clear endpoints
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{}}
loadBalancer.OnUpdate(endpoints)
endpoint, err = loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
}
func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) {
loadBalancer := NewLoadBalancerRR()
endpoint, err := loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
endpoints := make([]api.Endpoints, 2)
endpoints[0] = api.Endpoints{Name: "foo", Endpoints: []string{"endpoint:1", "endpoint:2", "endpoint:3"}}
endpoints[1] = api.Endpoints{Name: "bar", Endpoints: []string{"endpoint:4", "endpoint:5"}}
loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
expectEndpoint(t, loadBalancer, "foo", "endpoint:2")
expectEndpoint(t, loadBalancer, "foo", "endpoint:3")
expectEndpoint(t, loadBalancer, "foo", "endpoint:1")
expectEndpoint(t, loadBalancer, "foo", "endpoint:2")
expectEndpoint(t, loadBalancer, "bar", "endpoint:4")
expectEndpoint(t, loadBalancer, "bar", "endpoint:5")
expectEndpoint(t, loadBalancer, "bar", "endpoint:4")
expectEndpoint(t, loadBalancer, "bar", "endpoint:5")
expectEndpoint(t, loadBalancer, "bar", "endpoint:4")
// Then update the configuration by removing foo
loadBalancer.OnUpdate(endpoints[1:])
endpoint, err = loadBalancer.LoadBalance("foo", nil)
if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service")
}
// but bar is still there, and we continue RR from where we left off.
expectEndpoint(t, loadBalancer, "bar", "endpoint:5")
expectEndpoint(t, loadBalancer, "bar", "endpoint:4")
expectEndpoint(t, loadBalancer, "bar", "endpoint:5")
expectEndpoint(t, loadBalancer, "bar", "endpoint:4")
}

View File

@@ -0,0 +1,68 @@
/*
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 (
"encoding/json"
"net/url"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
)
// Implementation of RESTStorage for the api server.
type ControllerRegistryStorage struct {
registry ControllerRegistry
}
func MakeControllerRegistryStorage(registry ControllerRegistry) apiserver.RESTStorage {
return &ControllerRegistryStorage{
registry: registry,
}
}
func (storage *ControllerRegistryStorage) List(*url.URL) (interface{}, error) {
var result ReplicationControllerList
controllers, err := storage.registry.ListControllers()
if err == nil {
result = ReplicationControllerList{
Items: controllers,
}
}
return result, err
}
func (storage *ControllerRegistryStorage) Get(id string) (interface{}, error) {
return storage.registry.GetController(id)
}
func (storage *ControllerRegistryStorage) Delete(id string) error {
return storage.registry.DeleteController(id)
}
func (storage *ControllerRegistryStorage) Extract(body string) (interface{}, error) {
result := ReplicationController{}
err := json.Unmarshal([]byte(body), &result)
return result, err
}
func (storage *ControllerRegistryStorage) Create(controller interface{}) error {
return storage.registry.CreateController(controller.(ReplicationController))
}
func (storage *ControllerRegistryStorage) Update(controller interface{}) error {
return storage.registry.UpdateController(controller.(ReplicationController))
}

View File

@@ -0,0 +1,187 @@
/*
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 (
"encoding/json"
"fmt"
"io/ioutil"
"reflect"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type MockControllerRegistry struct {
err error
controllers []ReplicationController
}
func (registry *MockControllerRegistry) ListControllers() ([]ReplicationController, error) {
return registry.controllers, registry.err
}
func (registry *MockControllerRegistry) GetController(ID string) (*ReplicationController, error) {
return &ReplicationController{}, registry.err
}
func (registry *MockControllerRegistry) CreateController(controller ReplicationController) error {
return registry.err
}
func (registry *MockControllerRegistry) UpdateController(controller ReplicationController) error {
return registry.err
}
func (registry *MockControllerRegistry) DeleteController(ID string) error {
return registry.err
}
func TestListControllersError(t *testing.T) {
mockRegistry := MockControllerRegistry{
err: fmt.Errorf("Test Error"),
}
storage := ControllerRegistryStorage{
registry: &mockRegistry,
}
controllersObj, err := storage.List(nil)
controllers := controllersObj.(ReplicationControllerList)
if err != mockRegistry.err {
t.Errorf("Expected %#v, Got %#v", mockRegistry.err, err)
}
if len(controllers.Items) != 0 {
t.Errorf("Unexpected non-zero task list: %#v", controllers)
}
}
func TestListEmptyControllerList(t *testing.T) {
mockRegistry := MockControllerRegistry{}
storage := ControllerRegistryStorage{
registry: &mockRegistry,
}
controllers, err := storage.List(nil)
expectNoError(t, err)
if len(controllers.(ReplicationControllerList).Items) != 0 {
t.Errorf("Unexpected non-zero task list: %#v", controllers)
}
}
func TestListControllerList(t *testing.T) {
mockRegistry := MockControllerRegistry{
controllers: []ReplicationController{
ReplicationController{
JSONBase: JSONBase{
ID: "foo",
},
},
ReplicationController{
JSONBase: JSONBase{
ID: "bar",
},
},
},
}
storage := ControllerRegistryStorage{
registry: &mockRegistry,
}
controllersObj, err := storage.List(nil)
controllers := controllersObj.(ReplicationControllerList)
expectNoError(t, err)
if len(controllers.Items) != 2 {
t.Errorf("Unexpected controller list: %#v", controllers)
}
if controllers.Items[0].ID != "foo" {
t.Errorf("Unexpected controller: %#v", controllers.Items[0])
}
if controllers.Items[1].ID != "bar" {
t.Errorf("Unexpected controller: %#v", controllers.Items[1])
}
}
func TestExtractControllerJson(t *testing.T) {
mockRegistry := MockControllerRegistry{}
storage := ControllerRegistryStorage{
registry: &mockRegistry,
}
controller := ReplicationController{
JSONBase: JSONBase{
ID: "foo",
},
}
body, err := json.Marshal(controller)
expectNoError(t, err)
controllerOut, err := storage.Extract(string(body))
expectNoError(t, err)
jsonOut, err := json.Marshal(controllerOut)
expectNoError(t, err)
if string(body) != string(jsonOut) {
t.Errorf("Expected %#v, found %#v", controller, controllerOut)
}
}
func TestControllerParsing(t *testing.T) {
expectedController := ReplicationController{
JSONBase: JSONBase{
ID: "nginxController",
},
DesiredState: ReplicationControllerState{
Replicas: 2,
ReplicasInSet: map[string]string{
"name": "nginx",
},
TaskTemplate: TaskTemplate{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Image: "dockerfile/nginx",
Ports: []Port{
Port{
ContainerPort: 80,
HostPort: 8080,
},
},
},
},
},
},
Labels: map[string]string{
"name": "nginx",
},
},
},
Labels: map[string]string{
"name": "nginx",
},
}
file, err := ioutil.TempFile("", "controller")
fileName := file.Name()
expectNoError(t, err)
data, err := json.Marshal(expectedController)
expectNoError(t, err)
_, err = file.Write(data)
expectNoError(t, err)
err = file.Close()
expectNoError(t, err)
data, err = ioutil.ReadFile(fileName)
expectNoError(t, err)
var controller ReplicationController
err = json.Unmarshal(data, &controller)
expectNoError(t, err)
if !reflect.DeepEqual(controller, expectedController) {
t.Errorf("Parsing failed: %s %#v %#v", string(data), controller, expectedController)
}
}

65
pkg/registry/endpoints.go Normal file
View File

@@ -0,0 +1,65 @@
/*
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"
"log"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func MakeEndpointController(serviceRegistry ServiceRegistry, taskRegistry TaskRegistry) *EndpointController {
return &EndpointController{
serviceRegistry: serviceRegistry,
taskRegistry: taskRegistry,
}
}
type EndpointController struct {
serviceRegistry ServiceRegistry
taskRegistry TaskRegistry
}
func (e *EndpointController) SyncServiceEndpoints() error {
services, err := e.serviceRegistry.ListServices()
if err != nil {
return err
}
var resultErr error
for _, service := range services.Items {
tasks, err := e.taskRegistry.ListTasks(&service.Labels)
if err != nil {
log.Printf("Error syncing service: %#v, skipping.", service)
resultErr = err
continue
}
endpoints := make([]string, len(tasks))
for ix, task := range tasks {
// TODO: Use port names in the service object, don't just use port #0
endpoints[ix] = fmt.Sprintf("%s:%d", task.CurrentState.Host, task.DesiredState.Manifest.Containers[0].Ports[0].HostPort)
}
err = e.serviceRegistry.UpdateEndpoints(Endpoints{
Name: service.ID,
Endpoints: endpoints,
})
if err != nil {
log.Printf("Error updating endpoints: %#v", err)
continue
}
}
return resultErr
}

View File

@@ -0,0 +1,108 @@
/*
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"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func TestSyncEndpointsEmpty(t *testing.T) {
serviceRegistry := MockServiceRegistry{}
taskRegistry := MockTaskRegistry{}
endpoints := MakeEndpointController(&serviceRegistry, &taskRegistry)
err := endpoints.SyncServiceEndpoints()
expectNoError(t, err)
}
func TestSyncEndpointsError(t *testing.T) {
serviceRegistry := MockServiceRegistry{
err: fmt.Errorf("Test Error"),
}
taskRegistry := MockTaskRegistry{}
endpoints := MakeEndpointController(&serviceRegistry, &taskRegistry)
err := endpoints.SyncServiceEndpoints()
if err != serviceRegistry.err {
t.Errorf("Errors don't match: %#v %#v", err, serviceRegistry.err)
}
}
func TestSyncEndpointsItems(t *testing.T) {
serviceRegistry := MockServiceRegistry{
list: ServiceList{
Items: []Service{
Service{
Labels: map[string]string{
"foo": "bar",
},
},
},
},
}
taskRegistry := MockTaskRegistry{
tasks: []Task{
Task{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Ports: []Port{
Port{
HostPort: 8080,
},
},
},
},
},
},
},
},
}
endpoints := MakeEndpointController(&serviceRegistry, &taskRegistry)
err := endpoints.SyncServiceEndpoints()
expectNoError(t, err)
if len(serviceRegistry.endpoints.Endpoints) != 1 {
t.Errorf("Unexpected endpoints update: %#v", serviceRegistry.endpoints)
}
}
func TestSyncEndpointsTaskError(t *testing.T) {
serviceRegistry := MockServiceRegistry{
list: ServiceList{
Items: []Service{
Service{
Labels: map[string]string{
"foo": "bar",
},
},
},
},
}
taskRegistry := MockTaskRegistry{
err: fmt.Errorf("test error."),
}
endpoints := MakeEndpointController(&serviceRegistry, &taskRegistry)
err := endpoints.SyncServiceEndpoints()
if err == nil {
t.Error("Unexpected non-error")
}
}

View File

@@ -0,0 +1,392 @@
/*
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 (
"encoding/json"
"fmt"
"log"
"github.com/coreos/go-etcd/etcd"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// TODO: Need to add a reconciler loop that makes sure that things in tasks are reflected into
// kubelet (and vice versa)
// EtcdClient is an injectable interface for testing.
type EtcdClient interface {
AddChild(key, data string, ttl uint64) (*etcd.Response, error)
Get(key string, sort, recursive bool) (*etcd.Response, error)
Set(key, value string, ttl uint64) (*etcd.Response, error)
Create(key, value string, ttl uint64) (*etcd.Response, error)
Delete(key string, recursive bool) (*etcd.Response, error)
// I'd like to use directional channels here (e.g. <-chan) but this interface mimics
// the etcd client interface which doesn't, and it doesn't seem worth it to wrap the api.
Watch(prefix string, waitIndex uint64, recursive bool, receiver chan *etcd.Response, stop chan bool) (*etcd.Response, error)
}
// EtcdRegistry is an implementation of both ControllerRegistry and TaskRegistry which is backed with etcd.
type EtcdRegistry struct {
etcdClient EtcdClient
machines []string
manifestFactory ManifestFactory
}
// MakeEtcdRegistry creates an etcd registry.
// 'client' is the connection to etcd
// 'machines' is the list of machines
// 'scheduler' is the scheduling algorithm to use.
func MakeEtcdRegistry(client EtcdClient, machines []string) *EtcdRegistry {
registry := &EtcdRegistry{
etcdClient: client,
machines: machines,
}
registry.manifestFactory = &BasicManifestFactory{
serviceRegistry: registry,
}
return registry
}
func makeTaskKey(machine, taskID string) string {
return "/registry/hosts/" + machine + "/tasks/" + taskID
}
func (registry *EtcdRegistry) ListTasks(query *map[string]string) ([]Task, error) {
tasks := []Task{}
for _, machine := range registry.machines {
machineTasks, err := registry.listTasksForMachine(machine)
if err != nil {
return tasks, err
}
for _, task := range machineTasks {
if LabelsMatch(task, query) {
tasks = append(tasks, task)
}
}
}
return tasks, nil
}
func (registry *EtcdRegistry) listEtcdNode(key string) ([]*etcd.Node, error) {
result, err := registry.etcdClient.Get(key, false, true)
if err != nil {
nodes := make([]*etcd.Node, 0)
if isEtcdNotFound(err) {
return nodes, nil
} else {
return nodes, err
}
}
return result.Node.Nodes, nil
}
func (registry *EtcdRegistry) listTasksForMachine(machine string) ([]Task, error) {
tasks := []Task{}
key := "/registry/hosts/" + machine + "/tasks"
nodes, err := registry.listEtcdNode(key)
for _, node := range nodes {
task := Task{}
err = json.Unmarshal([]byte(node.Value), &task)
if err != nil {
return tasks, err
}
task.CurrentState.Host = machine
tasks = append(tasks, task)
}
return tasks, err
}
func (registry *EtcdRegistry) GetTask(taskID string) (*Task, error) {
task, _, err := registry.findTask(taskID)
return &task, err
}
func makeContainerKey(machine string) string {
return "/registry/hosts/" + machine + "/kubelet"
}
func (registry *EtcdRegistry) loadManifests(machine string) ([]ContainerManifest, error) {
var manifests []ContainerManifest
response, err := registry.etcdClient.Get(makeContainerKey(machine), false, false)
if err != nil {
if isEtcdNotFound(err) {
err = nil
manifests = []ContainerManifest{}
}
} else {
err = json.Unmarshal([]byte(response.Node.Value), &manifests)
}
return manifests, err
}
func (registry *EtcdRegistry) updateManifests(machine string, manifests []ContainerManifest) error {
containerData, err := json.Marshal(manifests)
if err != nil {
return err
}
_, err = registry.etcdClient.Set(makeContainerKey(machine), string(containerData), 0)
return err
}
func (registry *EtcdRegistry) CreateTask(machineIn string, task Task) error {
taskOut, machine, err := registry.findTask(task.ID)
if err == nil {
return fmt.Errorf("A task named %s already exists on %s (%#v)", task.ID, machine, taskOut)
}
return registry.runTask(task, machineIn)
}
func (registry *EtcdRegistry) runTask(task Task, machine string) error {
manifests, err := registry.loadManifests(machine)
if err != nil {
return err
}
key := makeTaskKey(machine, task.ID)
data, err := json.Marshal(task)
if err != nil {
return err
}
_, err = registry.etcdClient.Create(key, string(data), 0)
manifest, err := registry.manifestFactory.MakeManifest(machine, task)
if err != nil {
return err
}
manifests = append(manifests, manifest)
return registry.updateManifests(machine, manifests)
}
func (registry *EtcdRegistry) UpdateTask(task Task) error {
return fmt.Errorf("Unimplemented!")
}
func (registry *EtcdRegistry) DeleteTask(taskID string) error {
_, machine, err := registry.findTask(taskID)
if err != nil {
return err
}
return registry.deleteTaskFromMachine(machine, taskID)
}
func (registry *EtcdRegistry) deleteTaskFromMachine(machine, taskID string) error {
manifests, err := registry.loadManifests(machine)
if err != nil {
return err
}
newManifests := make([]ContainerManifest, 0)
found := false
for _, manifest := range manifests {
if manifest.Id != taskID {
newManifests = append(newManifests, manifest)
} else {
found = true
}
}
if !found {
// This really shouldn't happen, it indicates something is broken, and likely
// there is a lost task somewhere.
// However it is "deleted" so log it and move on
log.Printf("Couldn't find: %s in %#v", taskID, manifests)
}
if err = registry.updateManifests(machine, newManifests); err != nil {
return err
}
key := makeTaskKey(machine, taskID)
_, err = registry.etcdClient.Delete(key, true)
return err
}
func (registry *EtcdRegistry) getTaskForMachine(machine, taskID string) (Task, error) {
key := makeTaskKey(machine, taskID)
result, err := registry.etcdClient.Get(key, false, false)
if err != nil {
if isEtcdNotFound(err) {
return Task{}, fmt.Errorf("Not found (%#v).", err)
} else {
return Task{}, err
}
}
if result.Node == nil || len(result.Node.Value) == 0 {
return Task{}, fmt.Errorf("no nodes field: %#v", result)
}
task := Task{}
err = json.Unmarshal([]byte(result.Node.Value), &task)
task.CurrentState.Host = machine
return task, err
}
func (registry *EtcdRegistry) findTask(taskID string) (Task, string, error) {
for _, machine := range registry.machines {
task, err := registry.getTaskForMachine(machine, taskID)
if err == nil {
return task, machine, nil
}
}
return Task{}, "", fmt.Errorf("Task not found %s", taskID)
}
func isEtcdNotFound(err error) bool {
if err == nil {
return false
}
switch err.(type) {
case *etcd.EtcdError:
etcdError := err.(*etcd.EtcdError)
if etcdError == nil {
return false
}
if etcdError.ErrorCode == 100 {
return true
}
}
return false
}
func (registry *EtcdRegistry) ListControllers() ([]ReplicationController, error) {
var controllers []ReplicationController
key := "/registry/controllers"
nodes, err := registry.listEtcdNode(key)
for _, node := range nodes {
var controller ReplicationController
err = json.Unmarshal([]byte(node.Value), &controller)
if err != nil {
return controllers, err
}
controllers = append(controllers, controller)
}
return controllers, nil
}
func makeControllerKey(id string) string {
return "/registry/controllers/" + id
}
func (registry *EtcdRegistry) GetController(controllerID string) (*ReplicationController, error) {
var controller ReplicationController
key := makeControllerKey(controllerID)
result, err := registry.etcdClient.Get(key, false, false)
if err != nil {
if isEtcdNotFound(err) {
return nil, fmt.Errorf("Controller %s not found", controllerID)
} else {
return nil, err
}
}
if result.Node == nil || len(result.Node.Value) == 0 {
return nil, fmt.Errorf("no nodes field: %#v", result)
}
err = json.Unmarshal([]byte(result.Node.Value), &controller)
return &controller, err
}
func (registry *EtcdRegistry) CreateController(controller ReplicationController) error {
// TODO : check for existence here and error.
return registry.UpdateController(controller)
}
func (registry *EtcdRegistry) UpdateController(controller ReplicationController) error {
controllerData, err := json.Marshal(controller)
if err != nil {
return err
}
key := makeControllerKey(controller.ID)
_, err = registry.etcdClient.Set(key, string(controllerData), 0)
return err
}
func (registry *EtcdRegistry) DeleteController(controllerID string) error {
key := makeControllerKey(controllerID)
_, err := registry.etcdClient.Delete(key, false)
return err
}
func makeServiceKey(name string) string {
return "/registry/services/specs/" + name
}
func (registry *EtcdRegistry) ListServices() (ServiceList, error) {
nodes, err := registry.listEtcdNode("/registry/services/specs")
if err != nil {
return ServiceList{}, err
}
var services []Service
for _, node := range nodes {
var svc Service
err := json.Unmarshal([]byte(node.Value), &svc)
if err != nil {
return ServiceList{}, err
}
services = append(services, svc)
}
return ServiceList{Items: services}, nil
}
func (registry *EtcdRegistry) CreateService(svc Service) error {
key := makeServiceKey(svc.ID)
data, err := json.Marshal(svc)
if err != nil {
return err
}
_, err = registry.etcdClient.Set(key, string(data), 0)
return err
}
func (registry *EtcdRegistry) GetService(name string) (*Service, error) {
key := makeServiceKey(name)
response, err := registry.etcdClient.Get(key, false, false)
if err != nil {
if isEtcdNotFound(err) {
return nil, fmt.Errorf("Service %s was not found.", name)
} else {
return nil, err
}
}
var svc Service
err = json.Unmarshal([]byte(response.Node.Value), &svc)
if err != nil {
return nil, err
}
return &svc, err
}
func (registry *EtcdRegistry) DeleteService(name string) error {
key := makeServiceKey(name)
_, err := registry.etcdClient.Delete(key, true)
if err != nil {
return err
}
key = "/registry/services/endpoints/" + name
_, err = registry.etcdClient.Delete(key, true)
return err
}
func (registry *EtcdRegistry) UpdateService(svc Service) error {
return registry.CreateService(svc)
}
func (registry *EtcdRegistry) UpdateEndpoints(e Endpoints) error {
data, err := json.Marshal(e)
if err != nil {
return err
}
_, err = registry.etcdClient.Set("/registry/services/endpoints/"+e.Name, string(data), 0)
return err
}

View File

@@ -0,0 +1,623 @@
/*
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 (
"encoding/json"
"reflect"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
)
func TestEtcdGetTask(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Set("/registry/hosts/machine/tasks/foo", util.MakeJSONString(Task{JSONBase: JSONBase{ID: "foo"}}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
task, err := registry.GetTask("foo")
expectNoError(t, err)
if task.ID != "foo" {
t.Errorf("Unexpected task: %#v", task)
}
}
func TestEtcdGetTaskNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{
ErrorCode: 100,
},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
_, err := registry.GetTask("foo")
if err == nil {
t.Errorf("Unexpected non-error.")
}
}
func TestEtcdCreateTask(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]ContainerManifest{}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateTask("machine", Task{
JSONBase: JSONBase{
ID: "foo",
},
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Name: "foo",
},
},
},
},
})
expectNoError(t, err)
resp, err := fakeClient.Get("/registry/hosts/machine/tasks/foo", false, false)
expectNoError(t, err)
var task Task
err = json.Unmarshal([]byte(resp.Node.Value), &task)
expectNoError(t, err)
if task.ID != "foo" {
t.Errorf("Unexpected task: %#v %s", task, resp.Node.Value)
}
var manifests []ContainerManifest
resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false)
expectNoError(t, err)
err = json.Unmarshal([]byte(resp.Node.Value), &manifests)
if len(manifests) != 1 || manifests[0].Id != "foo" {
t.Errorf("Unexpected manifest list: %#v", manifests)
}
}
func TestEtcdCreateTaskAlreadyExisting(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Value: util.MakeJSONString(Task{JSONBase: JSONBase{ID: "foo"}}),
},
},
E: nil,
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateTask("machine", Task{
JSONBase: JSONBase{
ID: "foo",
},
})
if err == nil {
t.Error("Unexpected non-error")
}
}
func TestEtcdCreateTaskWithContainersError(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
fakeClient.Data["/registry/hosts/machine/kubelet"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 200},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateTask("machine", Task{
JSONBase: JSONBase{
ID: "foo",
},
})
if err == nil {
t.Error("Unexpected non-error")
}
_, err = fakeClient.Get("/registry/hosts/machine/tasks/foo", false, false)
if err == nil {
t.Error("Unexpected non-error")
}
if err != nil && err.(*etcd.EtcdError).ErrorCode != 100 {
t.Errorf("Unexpected error: %#v", err)
}
}
func TestEtcdCreateTaskWithContainersNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
fakeClient.Data["/registry/hosts/machine/kubelet"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateTask("machine", Task{
JSONBase: JSONBase{
ID: "foo",
},
DesiredState: TaskState{
Manifest: ContainerManifest{
Id: "foo",
Containers: []Container{
Container{
Name: "foo",
},
},
},
},
})
expectNoError(t, err)
resp, err := fakeClient.Get("/registry/hosts/machine/tasks/foo", false, false)
expectNoError(t, err)
var task Task
err = json.Unmarshal([]byte(resp.Node.Value), &task)
expectNoError(t, err)
if task.ID != "foo" {
t.Errorf("Unexpected task: %#v %s", task, resp.Node.Value)
}
var manifests []ContainerManifest
resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false)
expectNoError(t, err)
err = json.Unmarshal([]byte(resp.Node.Value), &manifests)
if len(manifests) != 1 || manifests[0].Id != "foo" {
t.Errorf("Unexpected manifest list: %#v", manifests)
}
}
func TestEtcdCreateTaskWithExistingContainers(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/hosts/machine/tasks/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]ContainerManifest{
ContainerManifest{
Id: "bar",
},
}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateTask("machine", Task{
JSONBase: JSONBase{
ID: "foo",
},
DesiredState: TaskState{
Manifest: ContainerManifest{
Id: "foo",
Containers: []Container{
Container{
Name: "foo",
},
},
},
},
})
expectNoError(t, err)
resp, err := fakeClient.Get("/registry/hosts/machine/tasks/foo", false, false)
expectNoError(t, err)
var task Task
err = json.Unmarshal([]byte(resp.Node.Value), &task)
expectNoError(t, err)
if task.ID != "foo" {
t.Errorf("Unexpected task: %#v %s", task, resp.Node.Value)
}
var manifests []ContainerManifest
resp, err = fakeClient.Get("/registry/hosts/machine/kubelet", false, false)
expectNoError(t, err)
err = json.Unmarshal([]byte(resp.Node.Value), &manifests)
if len(manifests) != 2 || manifests[1].Id != "foo" {
t.Errorf("Unexpected manifest list: %#v", manifests)
}
}
func TestEtcdDeleteTask(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/hosts/machine/tasks/foo"
fakeClient.Set(key, util.MakeJSONString(Task{JSONBase: JSONBase{ID: "foo"}}), 0)
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]ContainerManifest{
ContainerManifest{
Id: "foo",
},
}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.DeleteTask("foo")
expectNoError(t, err)
if len(fakeClient.deletedKeys) != 1 {
t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys)
}
if fakeClient.deletedKeys[0] != key {
t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key)
}
response, _ := fakeClient.Get("/registry/hosts/machine/kubelet", false, false)
if response.Node.Value != "[]" {
t.Errorf("Unexpected container set: %s, expected empty", response.Node.Value)
}
}
func TestEtcdDeleteTaskMultipleContainers(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/hosts/machine/tasks/foo"
fakeClient.Set(key, util.MakeJSONString(Task{JSONBase: JSONBase{ID: "foo"}}), 0)
fakeClient.Set("/registry/hosts/machine/kubelet", util.MakeJSONString([]ContainerManifest{
ContainerManifest{Id: "foo"},
ContainerManifest{Id: "bar"},
}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.DeleteTask("foo")
expectNoError(t, err)
if len(fakeClient.deletedKeys) != 1 {
t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys)
}
if fakeClient.deletedKeys[0] != key {
t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key)
}
response, _ := fakeClient.Get("/registry/hosts/machine/kubelet", false, false)
var manifests []ContainerManifest
json.Unmarshal([]byte(response.Node.Value), &manifests)
if len(manifests) != 1 {
t.Errorf("Unexpected manifest set: %#v, expected empty", manifests)
}
if manifests[0].Id != "bar" {
t.Errorf("Deleted wrong manifest: %#v", manifests)
}
}
func TestEtcdEmptyListTasks(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/hosts/machine/tasks"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Nodes: []*etcd.Node{},
},
},
E: nil,
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
tasks, err := registry.ListTasks(nil)
expectNoError(t, err)
if len(tasks) != 0 {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestEtcdListTasksNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/hosts/machine/tasks"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{},
E: &etcd.EtcdError{ErrorCode: 100},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
tasks, err := registry.ListTasks(nil)
expectNoError(t, err)
if len(tasks) != 0 {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestEtcdListTasks(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/hosts/machine/tasks"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Nodes: []*etcd.Node{
&etcd.Node{
Value: util.MakeJSONString(Task{JSONBase: JSONBase{ID: "foo"}}),
},
&etcd.Node{
Value: util.MakeJSONString(Task{JSONBase: JSONBase{ID: "bar"}}),
},
},
},
},
E: nil,
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
tasks, err := registry.ListTasks(nil)
expectNoError(t, err)
if len(tasks) != 2 || tasks[0].ID != "foo" || tasks[1].ID != "bar" {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestEtcdListControllersNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/controllers"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{},
E: &etcd.EtcdError{ErrorCode: 100},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
controllers, err := registry.ListControllers()
expectNoError(t, err)
if len(controllers) != 0 {
t.Errorf("Unexpected controller list: %#v", controllers)
}
}
func TestEtcdListServicesNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/services/specs"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{},
E: &etcd.EtcdError{ErrorCode: 100},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
services, err := registry.ListServices()
expectNoError(t, err)
if len(services.Items) != 0 {
t.Errorf("Unexpected controller list: %#v", services)
}
}
func TestEtcdListControllers(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/controllers"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Nodes: []*etcd.Node{
&etcd.Node{
Value: util.MakeJSONString(ReplicationController{JSONBase: JSONBase{ID: "foo"}}),
},
&etcd.Node{
Value: util.MakeJSONString(ReplicationController{JSONBase: JSONBase{ID: "bar"}}),
},
},
},
},
E: nil,
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
controllers, err := registry.ListControllers()
expectNoError(t, err)
if len(controllers) != 2 || controllers[0].ID != "foo" || controllers[1].ID != "bar" {
t.Errorf("Unexpected controller list: %#v", controllers)
}
}
func TestEtcdGetController(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Set("/registry/controllers/foo", util.MakeJSONString(ReplicationController{JSONBase: JSONBase{ID: "foo"}}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
ctrl, err := registry.GetController("foo")
expectNoError(t, err)
if ctrl.ID != "foo" {
t.Errorf("Unexpected controller: %#v", ctrl)
}
}
func TestEtcdGetControllerNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/controllers/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{
ErrorCode: 100,
},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
ctrl, err := registry.GetController("foo")
if ctrl != nil {
t.Errorf("Unexpected non-nil controller: %#v", ctrl)
}
if err == nil {
t.Error("Unexpected non-error.")
}
}
func TestEtcdDeleteController(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.DeleteController("foo")
expectNoError(t, err)
if len(fakeClient.deletedKeys) != 1 {
t.Errorf("Expected 1 delete, found %#v", fakeClient.deletedKeys)
}
key := "/registry/controllers/foo"
if fakeClient.deletedKeys[0] != key {
t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key)
}
}
func TestEtcdCreateController(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateController(ReplicationController{
JSONBase: JSONBase{
ID: "foo",
},
})
expectNoError(t, err)
resp, err := fakeClient.Get("/registry/controllers/foo", false, false)
expectNoError(t, err)
var ctrl ReplicationController
err = json.Unmarshal([]byte(resp.Node.Value), &ctrl)
expectNoError(t, err)
if ctrl.ID != "foo" {
t.Errorf("Unexpected task: %#v %s", ctrl, resp.Node.Value)
}
}
func TestEtcdUpdateController(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Set("/registry/controllers/foo", util.MakeJSONString(ReplicationController{JSONBase: JSONBase{ID: "foo"}}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.UpdateController(ReplicationController{
JSONBase: JSONBase{ID: "foo"},
DesiredState: ReplicationControllerState{
Replicas: 2,
},
})
expectNoError(t, err)
ctrl, err := registry.GetController("foo")
if ctrl.DesiredState.Replicas != 2 {
t.Errorf("Unexpected controller: %#v", ctrl)
}
}
func TestEtcdListServices(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
key := "/registry/services/specs"
fakeClient.Data[key] = EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Nodes: []*etcd.Node{
&etcd.Node{
Value: util.MakeJSONString(Service{JSONBase: JSONBase{ID: "foo"}}),
},
&etcd.Node{
Value: util.MakeJSONString(Service{JSONBase: JSONBase{ID: "bar"}}),
},
},
},
},
E: nil,
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
services, err := registry.ListServices()
expectNoError(t, err)
if len(services.Items) != 2 || services.Items[0].ID != "foo" || services.Items[1].ID != "bar" {
t.Errorf("Unexpected task list: %#v", services)
}
}
func TestEtcdCreateService(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/services/specs/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{ErrorCode: 100},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.CreateService(Service{
JSONBase: JSONBase{ID: "foo"},
})
expectNoError(t, err)
resp, err := fakeClient.Get("/registry/services/specs/foo", false, false)
expectNoError(t, err)
var service Service
err = json.Unmarshal([]byte(resp.Node.Value), &service)
expectNoError(t, err)
if service.ID != "foo" {
t.Errorf("Unexpected service: %#v %s", service, resp.Node.Value)
}
}
func TestEtcdGetService(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Set("/registry/services/specs/foo", util.MakeJSONString(Service{JSONBase: JSONBase{ID: "foo"}}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
service, err := registry.GetService("foo")
expectNoError(t, err)
if service.ID != "foo" {
t.Errorf("Unexpected task: %#v", service)
}
}
func TestEtcdGetServiceNotFound(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Data["/registry/services/specs/foo"] = EtcdResponseWithError{
R: &etcd.Response{
Node: nil,
},
E: &etcd.EtcdError{
ErrorCode: 100,
},
}
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
_, err := registry.GetService("foo")
if err == nil {
t.Errorf("Unexpected non-error.")
}
}
func TestEtcdDeleteService(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.DeleteService("foo")
expectNoError(t, err)
if len(fakeClient.deletedKeys) != 2 {
t.Errorf("Expected 2 delete, found %#v", fakeClient.deletedKeys)
}
key := "/registry/services/specs/foo"
if fakeClient.deletedKeys[0] != key {
t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[0], key)
}
key = "/registry/services/endpoints/foo"
if fakeClient.deletedKeys[1] != key {
t.Errorf("Unexpected key: %s, expected %s", fakeClient.deletedKeys[1], key)
}
}
func TestEtcdUpdateService(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
fakeClient.Set("/registry/services/specs/foo", util.MakeJSONString(Service{JSONBase: JSONBase{ID: "foo"}}), 0)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
err := registry.UpdateService(Service{
JSONBase: JSONBase{ID: "foo"},
Labels: map[string]string{
"baz": "bar",
},
})
expectNoError(t, err)
svc, err := registry.GetService("foo")
if svc.Labels["baz"] != "bar" {
t.Errorf("Unexpected service: %#v", svc)
}
}
func TestEtcdUpdateEndpoints(t *testing.T) {
fakeClient := MakeFakeEtcdClient(t)
registry := MakeTestEtcdRegistry(fakeClient, []string{"machine"})
endpoints := Endpoints{
Name: "foo",
Endpoints: []string{"baz", "bar"},
}
err := registry.UpdateEndpoints(endpoints)
expectNoError(t, err)
response, err := fakeClient.Get("/registry/services/endpoints/foo", false, false)
expectNoError(t, err)
var endpointsOut Endpoints
err = json.Unmarshal([]byte(response.Node.Value), &endpointsOut)
if !reflect.DeepEqual(endpoints, endpointsOut) {
t.Errorf("Unexpected endpoints: %#v, expected %#v", endpointsOut, endpoints)
}
}

View 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"
"testing"
"github.com/coreos/go-etcd/etcd"
)
type EtcdResponseWithError struct {
R *etcd.Response
E error
}
type FakeEtcdClient struct {
Data map[string]EtcdResponseWithError
deletedKeys []string
err error
t *testing.T
}
func MakeFakeEtcdClient(t *testing.T) *FakeEtcdClient {
return &FakeEtcdClient{
t: t,
Data: map[string]EtcdResponseWithError{},
}
}
func (f *FakeEtcdClient) AddChild(key, data string, ttl uint64) (*etcd.Response, error) {
return f.Set(key, data, ttl)
}
func (f *FakeEtcdClient) Get(key string, sort, recursive bool) (*etcd.Response, error) {
result := f.Data[key]
if result.R == nil {
f.t.Errorf("Unexpected get for %s", key)
return &etcd.Response{}, &etcd.EtcdError{ErrorCode: 100}
}
return result.R, result.E
}
func (f *FakeEtcdClient) Set(key, value string, ttl uint64) (*etcd.Response, error) {
result := EtcdResponseWithError{
R: &etcd.Response{
Node: &etcd.Node{
Value: value,
},
},
}
f.Data[key] = result
return result.R, f.err
}
func (f *FakeEtcdClient) Create(key, value string, ttl uint64) (*etcd.Response, error) {
return f.Set(key, value, ttl)
}
func (f *FakeEtcdClient) Delete(key string, recursive bool) (*etcd.Response, error) {
f.deletedKeys = append(f.deletedKeys, key)
return &etcd.Response{}, f.err
}
func (f *FakeEtcdClient) Watch(prefix string, waitIndex uint64, recursive bool, receiver chan *etcd.Response, stop chan bool) (*etcd.Response, error) {
return nil, fmt.Errorf("Unimplemented")
}
func MakeTestEtcdRegistry(client EtcdClient, machines []string) *EtcdRegistry {
registry := MakeEtcdRegistry(client, machines)
registry.manifestFactory = &BasicManifestFactory{
serviceRegistry: &MockServiceRegistry{},
}
return registry
}

View File

@@ -0,0 +1,44 @@
/*
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 (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// TaskRegistry is an interface implemented by things that know how to store Task objects
type TaskRegistry interface {
// ListTasks obtains a list of tasks that match query.
// Query may be nil in which case all tasks are returned.
ListTasks(query *map[string]string) ([]api.Task, error)
// Get a specific task
GetTask(taskId string) (*api.Task, error)
// Create a task based on a specification, schedule it onto a specific machine.
CreateTask(machine string, task api.Task) error
// Update an existing task
UpdateTask(task api.Task) error
// Delete an existing task
DeleteTask(taskId string) error
}
// ControllerRegistry is an interface for things that know how to store Controllers
type ControllerRegistry interface {
ListControllers() ([]api.ReplicationController, error)
GetController(controllerId string) (*api.ReplicationController, error)
CreateController(controller api.ReplicationController) error
UpdateController(controller api.ReplicationController) error
DeleteController(controllerId string) error
}

View File

@@ -0,0 +1,41 @@
/*
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 (
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type ManifestFactory interface {
// Make a container object for a given task, given the machine that the task is running on.
MakeManifest(machine string, task Task) (ContainerManifest, error)
}
type BasicManifestFactory struct {
serviceRegistry ServiceRegistry
}
func (b *BasicManifestFactory) MakeManifest(machine string, task Task) (ContainerManifest, error) {
envVars, err := GetServiceEnvironmentVariables(b.serviceRegistry, machine)
if err != nil {
return ContainerManifest{}, err
}
for ix, container := range task.DesiredState.Manifest.Containers {
task.DesiredState.Manifest.Id = task.ID
task.DesiredState.Manifest.Containers[ix].Env = append(container.Env, envVars...)
}
return task.DesiredState.Manifest, nil
}

View File

@@ -0,0 +1,133 @@
/*
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 (
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func TestMakeManifestNoServices(t *testing.T) {
registry := MockServiceRegistry{}
factory := &BasicManifestFactory{
serviceRegistry: &registry,
}
manifest, err := factory.MakeManifest("machine", Task{
JSONBase: JSONBase{ID: "foobar"},
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Name: "foo",
},
},
},
},
})
expectNoError(t, err)
container := manifest.Containers[0]
if len(container.Env) != 1 ||
container.Env[0].Name != "SERVICE_HOST" ||
container.Env[0].Value != "machine" {
t.Errorf("Expected one env vars, got: %#v", manifest)
}
if manifest.Id != "foobar" {
t.Errorf("Failed to assign id to manifest: %#v")
}
}
func TestMakeManifestServices(t *testing.T) {
registry := MockServiceRegistry{
list: ServiceList{
Items: []Service{
Service{
JSONBase: JSONBase{ID: "test"},
Port: 8080,
},
},
},
}
factory := &BasicManifestFactory{
serviceRegistry: &registry,
}
manifest, err := factory.MakeManifest("machine", Task{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Name: "foo",
},
},
},
},
})
expectNoError(t, err)
container := manifest.Containers[0]
if len(container.Env) != 2 ||
container.Env[0].Name != "TEST_SERVICE_PORT" ||
container.Env[0].Value != "8080" ||
container.Env[1].Name != "SERVICE_HOST" ||
container.Env[1].Value != "machine" {
t.Errorf("Expected 2 env vars, got: %#v", manifest)
}
}
func TestMakeManifestServicesExistingEnvVar(t *testing.T) {
registry := MockServiceRegistry{
list: ServiceList{
Items: []Service{
Service{
JSONBase: JSONBase{ID: "test"},
Port: 8080,
},
},
},
}
factory := &BasicManifestFactory{
serviceRegistry: &registry,
}
manifest, err := factory.MakeManifest("machine", Task{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Env: []EnvVar{
EnvVar{
Name: "foo",
Value: "bar",
},
},
},
},
},
},
})
expectNoError(t, err)
container := manifest.Containers[0]
if len(container.Env) != 3 ||
container.Env[0].Name != "foo" ||
container.Env[0].Value != "bar" ||
container.Env[1].Name != "TEST_SERVICE_PORT" ||
container.Env[1].Value != "8080" ||
container.Env[2].Name != "SERVICE_HOST" ||
container.Env[2].Value != "machine" {
t.Errorf("Expected no env vars, got: %#v", manifest)
}
}

View File

@@ -0,0 +1,137 @@
/*
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 (
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// An implementation of TaskRegistry and ControllerRegistry that is backed by memory
// Mainly used for testing.
type MemoryRegistry struct {
taskData map[string]Task
controllerData map[string]ReplicationController
serviceData map[string]Service
}
func MakeMemoryRegistry() *MemoryRegistry {
return &MemoryRegistry{
taskData: map[string]Task{},
controllerData: map[string]ReplicationController{},
serviceData: map[string]Service{},
}
}
func (registry *MemoryRegistry) ListTasks(labelQuery *map[string]string) ([]Task, error) {
result := []Task{}
for _, value := range registry.taskData {
if LabelsMatch(value, labelQuery) {
result = append(result, value)
}
}
return result, nil
}
func (registry *MemoryRegistry) GetTask(taskID string) (*Task, error) {
task, found := registry.taskData[taskID]
if found {
return &task, nil
} else {
return nil, nil
}
}
func (registry *MemoryRegistry) CreateTask(machine string, task Task) error {
registry.taskData[task.ID] = task
return nil
}
func (registry *MemoryRegistry) DeleteTask(taskID string) error {
delete(registry.taskData, taskID)
return nil
}
func (registry *MemoryRegistry) UpdateTask(task Task) error {
registry.taskData[task.ID] = task
return nil
}
func (registry *MemoryRegistry) ListControllers() ([]ReplicationController, error) {
result := []ReplicationController{}
for _, value := range registry.controllerData {
result = append(result, value)
}
return result, nil
}
func (registry *MemoryRegistry) GetController(controllerID string) (*ReplicationController, error) {
controller, found := registry.controllerData[controllerID]
if found {
return &controller, nil
} else {
return nil, nil
}
}
func (registry *MemoryRegistry) CreateController(controller ReplicationController) error {
registry.controllerData[controller.ID] = controller
return nil
}
func (registry *MemoryRegistry) DeleteController(controllerId string) error {
delete(registry.controllerData, controllerId)
return nil
}
func (registry *MemoryRegistry) UpdateController(controller ReplicationController) error {
registry.controllerData[controller.ID] = controller
return nil
}
func (registry *MemoryRegistry) ListServices() (ServiceList, error) {
var list []Service
for _, value := range registry.serviceData {
list = append(list, value)
}
return ServiceList{Items: list}, nil
}
func (registry *MemoryRegistry) CreateService(svc Service) error {
registry.serviceData[svc.ID] = svc
return nil
}
func (registry *MemoryRegistry) GetService(name string) (*Service, error) {
svc, found := registry.serviceData[name]
if found {
return &svc, nil
} else {
return nil, nil
}
}
func (registry *MemoryRegistry) DeleteService(name string) error {
delete(registry.serviceData, name)
return nil
}
func (registry *MemoryRegistry) UpdateService(svc Service) error {
return registry.CreateService(svc)
}
func (registry *MemoryRegistry) UpdateEndpoints(e Endpoints) error {
return nil
}

View File

@@ -0,0 +1,146 @@
/*
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 (
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func TestListTasksEmpty(t *testing.T) {
registry := MakeMemoryRegistry()
tasks, err := registry.ListTasks(nil)
expectNoError(t, err)
if len(tasks) != 0 {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestMemoryListTasks(t *testing.T) {
registry := MakeMemoryRegistry()
registry.CreateTask("machine", Task{JSONBase: JSONBase{ID: "foo"}})
tasks, err := registry.ListTasks(nil)
expectNoError(t, err)
if len(tasks) != 1 || tasks[0].ID != "foo" {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestMemorySetGetTasks(t *testing.T) {
registry := MakeMemoryRegistry()
expectedTask := Task{JSONBase: JSONBase{ID: "foo"}}
registry.CreateTask("machine", expectedTask)
task, err := registry.GetTask("foo")
expectNoError(t, err)
if expectedTask.ID != task.ID {
t.Errorf("Unexpected task, expected %#v, actual %#v", expectedTask, task)
}
}
func TestMemorySetUpdateGetTasks(t *testing.T) {
registry := MakeMemoryRegistry()
oldTask := Task{JSONBase: JSONBase{ID: "foo"}}
expectedTask := Task{
JSONBase: JSONBase{
ID: "foo",
},
DesiredState: TaskState{
Host: "foo.com",
},
}
registry.CreateTask("machine", oldTask)
registry.UpdateTask(expectedTask)
task, err := registry.GetTask("foo")
expectNoError(t, err)
if expectedTask.ID != task.ID || task.DesiredState.Host != expectedTask.DesiredState.Host {
t.Errorf("Unexpected task, expected %#v, actual %#v", expectedTask, task)
}
}
func TestMemorySetDeleteGetTasks(t *testing.T) {
registry := MakeMemoryRegistry()
expectedTask := Task{JSONBase: JSONBase{ID: "foo"}}
registry.CreateTask("machine", expectedTask)
registry.DeleteTask("foo")
task, err := registry.GetTask("foo")
expectNoError(t, err)
if task != nil {
t.Errorf("Unexpected task: %#v", task)
}
}
func TestListControllersEmpty(t *testing.T) {
registry := MakeMemoryRegistry()
tasks, err := registry.ListControllers()
expectNoError(t, err)
if len(tasks) != 0 {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestMemoryListControllers(t *testing.T) {
registry := MakeMemoryRegistry()
registry.CreateController(ReplicationController{JSONBase: JSONBase{ID: "foo"}})
tasks, err := registry.ListControllers()
expectNoError(t, err)
if len(tasks) != 1 || tasks[0].ID != "foo" {
t.Errorf("Unexpected task list: %#v", tasks)
}
}
func TestMemorySetGetControllers(t *testing.T) {
registry := MakeMemoryRegistry()
expectedController := ReplicationController{JSONBase: JSONBase{ID: "foo"}}
registry.CreateController(expectedController)
task, err := registry.GetController("foo")
expectNoError(t, err)
if expectedController.ID != task.ID {
t.Errorf("Unexpected task, expected %#v, actual %#v", expectedController, task)
}
}
func TestMemorySetUpdateGetControllers(t *testing.T) {
registry := MakeMemoryRegistry()
oldController := ReplicationController{JSONBase: JSONBase{ID: "foo"}}
expectedController := ReplicationController{
JSONBase: JSONBase{
ID: "foo",
},
DesiredState: ReplicationControllerState{
Replicas: 2,
},
}
registry.CreateController(oldController)
registry.UpdateController(expectedController)
task, err := registry.GetController("foo")
expectNoError(t, err)
if expectedController.ID != task.ID || task.DesiredState.Replicas != expectedController.DesiredState.Replicas {
t.Errorf("Unexpected task, expected %#v, actual %#v", expectedController, task)
}
}
func TestMemorySetDeleteGetControllers(t *testing.T) {
registry := MakeMemoryRegistry()
expectedController := ReplicationController{JSONBase: JSONBase{ID: "foo"}}
registry.CreateController(expectedController)
registry.DeleteController("foo")
task, err := registry.GetController("foo")
expectNoError(t, err)
if task != nil {
t.Errorf("Unexpected task: %#v", task)
}
}

View 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 registry
import (
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type MockServiceRegistry struct {
list ServiceList
err error
endpoints Endpoints
}
func (m *MockServiceRegistry) ListServices() (ServiceList, error) {
return m.list, m.err
}
func (m *MockServiceRegistry) CreateService(svc Service) error {
return m.err
}
func (m *MockServiceRegistry) GetService(name string) (*Service, error) {
return nil, m.err
}
func (m *MockServiceRegistry) DeleteService(name string) error {
return m.err
}
func (m *MockServiceRegistry) UpdateService(svc Service) error {
return m.err
}
func (m *MockServiceRegistry) UpdateEndpoints(e Endpoints) error {
m.endpoints = e
return m.err
}

View File

@@ -0,0 +1,186 @@
/*
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 (
"encoding/json"
"fmt"
"log"
"math/rand"
"strings"
"sync"
"time"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
)
// ReplicationManager is responsible for synchronizing ReplicationController objects stored in etcd
// with actual running tasks.
// TODO: Remove the etcd dependency and re-factor in terms of a generic watch interface
type ReplicationManager struct {
etcdClient *etcd.Client
kubeClient client.ClientInterface
taskControl TaskControlInterface
updateLock sync.Mutex
}
// An interface that knows how to add or delete tasks
// created as an interface to allow testing.
type TaskControlInterface interface {
createReplica(controllerSpec ReplicationController)
deleteTask(taskID string) error
}
type RealTaskControl struct {
kubeClient client.ClientInterface
}
func (r RealTaskControl) createReplica(controllerSpec ReplicationController) {
labels := controllerSpec.DesiredState.TaskTemplate.Labels
if labels != nil {
labels["replicationController"] = controllerSpec.ID
}
task := Task{
JSONBase: JSONBase{
ID: fmt.Sprintf("%x", rand.Int()),
},
DesiredState: controllerSpec.DesiredState.TaskTemplate.DesiredState,
Labels: controllerSpec.DesiredState.TaskTemplate.Labels,
}
_, err := r.kubeClient.CreateTask(task)
if err != nil {
log.Printf("%#v\n", err)
}
}
func (r RealTaskControl) deleteTask(taskID string) error {
return r.kubeClient.DeleteTask(taskID)
}
func MakeReplicationManager(etcdClient *etcd.Client, kubeClient client.ClientInterface) *ReplicationManager {
return &ReplicationManager{
kubeClient: kubeClient,
etcdClient: etcdClient,
taskControl: RealTaskControl{
kubeClient: kubeClient,
},
}
}
func (rm *ReplicationManager) WatchControllers() {
watchChannel := make(chan *etcd.Response)
go util.Forever(func() { rm.etcdClient.Watch("/registry/controllers", 0, true, watchChannel, nil) }, 0)
for {
watchResponse := <-watchChannel
if watchResponse == nil {
time.Sleep(time.Second * 10)
continue
}
log.Printf("Got watch: %#v", watchResponse)
controller, err := rm.handleWatchResponse(watchResponse)
if err != nil {
log.Printf("Error handling data: %#v, %#v", err, watchResponse)
continue
}
rm.syncReplicationController(*controller)
}
}
func (rm *ReplicationManager) handleWatchResponse(response *etcd.Response) (*ReplicationController, error) {
if response.Action == "set" {
if response.Node != nil {
var controllerSpec ReplicationController
err := json.Unmarshal([]byte(response.Node.Value), &controllerSpec)
if err != nil {
return nil, err
}
return &controllerSpec, nil
} else {
return nil, fmt.Errorf("Response node is null %#v", response)
}
}
return nil, nil
}
func (rm *ReplicationManager) filterActiveTasks(tasks []Task) []Task {
var result []Task
for _, value := range tasks {
if strings.Index(value.CurrentState.Status, "Exit") == -1 {
result = append(result, value)
}
}
return result
}
func (rm *ReplicationManager) syncReplicationController(controllerSpec ReplicationController) error {
rm.updateLock.Lock()
taskList, err := rm.kubeClient.ListTasks(controllerSpec.DesiredState.ReplicasInSet)
if err != nil {
return err
}
filteredList := rm.filterActiveTasks(taskList.Items)
diff := len(filteredList) - controllerSpec.DesiredState.Replicas
log.Printf("%#v", filteredList)
if diff < 0 {
diff *= -1
log.Printf("Too few replicas, creating %d\n", diff)
for i := 0; i < diff; i++ {
rm.taskControl.createReplica(controllerSpec)
}
} else if diff > 0 {
log.Print("Too many replicas, deleting")
for i := 0; i < diff; i++ {
rm.taskControl.deleteTask(filteredList[i].ID)
}
}
rm.updateLock.Unlock()
return nil
}
func (rm *ReplicationManager) Synchronize() {
for {
response, err := rm.etcdClient.Get("/registry/controllers", false, false)
if err != nil {
log.Printf("Synchronization error %#v", err)
}
// TODO(bburns): There is a race here, if we get a version of the controllers, and then it is
// updated, its possible that the watch will pick up the change first, and then we will execute
// using the old version of the controller.
// Probably the correct thing to do is to use the version number in etcd to detect when
// we are stale.
// Punting on this for now, but this could lead to some nasty bugs, so we should really fix it
// sooner rather than later.
if response != nil && response.Node != nil && response.Node.Nodes != nil {
for _, value := range response.Node.Nodes {
var controllerSpec ReplicationController
err := json.Unmarshal([]byte(value.Value), &controllerSpec)
if err != nil {
log.Printf("Unexpected error: %#v", err)
continue
}
log.Printf("Synchronizing %s\n", controllerSpec.ID)
err = rm.syncReplicationController(controllerSpec)
if err != nil {
log.Printf("Error synchronizing: %#v", err)
}
}
}
time.Sleep(10 * time.Second)
}
}

View File

@@ -0,0 +1,311 @@
/*
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 (
"encoding/json"
"fmt"
"net/http/httptest"
"reflect"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
)
// TODO: Move this to a common place, it's needed in multiple tests.
var apiPath = "/api/v1beta1"
func makeUrl(suffix string) string {
return apiPath + suffix
}
type FakeTaskControl struct {
controllerSpec []ReplicationController
deleteTaskID []string
}
func (f *FakeTaskControl) createReplica(spec ReplicationController) {
f.controllerSpec = append(f.controllerSpec, spec)
}
func (f *FakeTaskControl) deleteTask(taskID string) error {
f.deleteTaskID = append(f.deleteTaskID, taskID)
return nil
}
func makeReplicationController(replicas int) ReplicationController {
return ReplicationController{
DesiredState: ReplicationControllerState{
Replicas: replicas,
TaskTemplate: TaskTemplate{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Image: "foo/bar",
},
},
},
},
Labels: map[string]string{
"name": "foo",
"type": "production",
},
},
},
}
}
func makeTaskList(count int) TaskList {
tasks := []Task{}
for i := 0; i < count; i++ {
tasks = append(tasks, Task{
JSONBase: JSONBase{
ID: fmt.Sprintf("task%d", i),
},
})
}
return TaskList{
Items: tasks,
}
}
func validateSyncReplication(t *testing.T, fakeTaskControl *FakeTaskControl, expectedCreates, expectedDeletes int) {
if len(fakeTaskControl.controllerSpec) != expectedCreates {
t.Errorf("Unexpected number of creates. Expected %d, saw %d\n", expectedCreates, len(fakeTaskControl.controllerSpec))
}
if len(fakeTaskControl.deleteTaskID) != expectedDeletes {
t.Errorf("Unexpected number of deletes. Expected %d, saw %d\n", expectedDeletes, len(fakeTaskControl.deleteTaskID))
}
}
func TestSyncReplicationControllerDoesNothing(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
controllerSpec := makeReplicationController(2)
manager.syncReplicationController(controllerSpec)
validateSyncReplication(t, &fakeTaskControl, 0, 0)
}
func TestSyncReplicationControllerDeletes(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
controllerSpec := makeReplicationController(1)
manager.syncReplicationController(controllerSpec)
validateSyncReplication(t, &fakeTaskControl, 0, 1)
}
func TestSyncReplicationControllerCreates(t *testing.T) {
body := "{ \"items\": [] }"
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
controllerSpec := makeReplicationController(2)
manager.syncReplicationController(controllerSpec)
validateSyncReplication(t, &fakeTaskControl, 2, 0)
}
func TestCreateReplica(t *testing.T) {
body := "{}"
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
taskControl := RealTaskControl{
kubeClient: client,
}
controllerSpec := ReplicationController{
DesiredState: ReplicationControllerState{
TaskTemplate: TaskTemplate{
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Image: "foo/bar",
},
},
},
},
Labels: map[string]string{
"name": "foo",
"type": "production",
},
},
},
}
taskControl.createReplica(controllerSpec)
//expectedTask := Task{
// Labels: controllerSpec.DesiredState.TaskTemplate.Labels,
// DesiredState: controllerSpec.DesiredState.TaskTemplate.DesiredState,
//}
// TODO: fix this so that it validates the body.
fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "POST", nil)
}
func TestHandleWatchResponseNotSet(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
_, err := manager.handleWatchResponse(&etcd.Response{
Action: "delete",
})
expectNoError(t, err)
}
func TestHandleWatchResponseNoNode(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
_, err := manager.handleWatchResponse(&etcd.Response{
Action: "set",
})
if err == nil {
t.Error("Unexpected non-error")
}
}
func TestHandleWatchResponseBadData(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
_, err := manager.handleWatchResponse(&etcd.Response{
Action: "set",
Node: &etcd.Node{
Value: "foobar",
},
})
if err == nil {
t.Error("Unexpected non-error")
}
}
func TestHandleWatchResponse(t *testing.T) {
body, _ := json.Marshal(makeTaskList(2))
fakeHandler := util.FakeHandler{
StatusCode: 200,
ResponseBody: string(body),
}
testServer := httptest.NewTLSServer(&fakeHandler)
client := Client{
Host: testServer.URL,
}
fakeTaskControl := FakeTaskControl{}
manager := MakeReplicationManager(nil, &client)
manager.taskControl = &fakeTaskControl
controller := makeReplicationController(2)
data, err := json.Marshal(controller)
expectNoError(t, err)
controllerOut, err := manager.handleWatchResponse(&etcd.Response{
Action: "set",
Node: &etcd.Node{
Value: string(data),
},
})
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
if !reflect.DeepEqual(controller, *controllerOut) {
t.Errorf("Unexpected mismatch. Expected %#v, Saw: %#v", controller, controllerOut)
}
}

115
pkg/registry/scheduler.go Normal file
View File

@@ -0,0 +1,115 @@
/*
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"
"math/rand"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
// Scheduler is an interface implemented by things that know how to schedule tasks onto machines.
type Scheduler interface {
Schedule(Task) (string, error)
}
// RandomScheduler choses machines uniformly at random.
type RandomScheduler struct {
machines []string
random rand.Rand
}
func MakeRandomScheduler(machines []string, random rand.Rand) Scheduler {
return &RandomScheduler{
machines: machines,
random: random,
}
}
func (s *RandomScheduler) Schedule(task Task) (string, error) {
return s.machines[s.random.Int()%len(s.machines)], nil
}
// RoundRobinScheduler chooses machines in order.
type RoundRobinScheduler struct {
machines []string
currentIndex int
}
func MakeRoundRobinScheduler(machines []string) Scheduler {
return &RoundRobinScheduler{
machines: machines,
currentIndex: 0,
}
}
func (s *RoundRobinScheduler) Schedule(task Task) (string, error) {
result := s.machines[s.currentIndex]
s.currentIndex = (s.currentIndex + 1) % len(s.machines)
return result, nil
}
type FirstFitScheduler struct {
machines []string
registry TaskRegistry
}
func MakeFirstFitScheduler(machines []string, registry TaskRegistry) Scheduler {
return &FirstFitScheduler{
machines: machines,
registry: registry,
}
}
func (s *FirstFitScheduler) containsPort(task Task, port Port) bool {
for _, container := range task.DesiredState.Manifest.Containers {
for _, taskPort := range container.Ports {
if taskPort.HostPort == port.HostPort {
return true
}
}
}
return false
}
func (s *FirstFitScheduler) Schedule(task Task) (string, error) {
machineToTasks := map[string][]Task{}
tasks, err := s.registry.ListTasks(nil)
if err != nil {
return "", err
}
for _, scheduledTask := range tasks {
host := scheduledTask.CurrentState.Host
machineToTasks[host] = append(machineToTasks[host], scheduledTask)
}
for _, machine := range s.machines {
taskFits := true
for _, scheduledTask := range machineToTasks[machine] {
for _, container := range task.DesiredState.Manifest.Containers {
for _, port := range container.Ports {
if s.containsPort(scheduledTask, port) {
taskFits = false
}
}
}
}
if taskFits {
return machine, nil
}
}
return "", fmt.Errorf("Failed to find fit for %#v", task)
}

View File

@@ -0,0 +1,110 @@
/*
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 (
"math/rand"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
func expectSchedule(scheduler Scheduler, task Task, expected string, t *testing.T) {
actual, err := scheduler.Schedule(task)
expectNoError(t, err)
if actual != expected {
t.Errorf("Unexpected scheduling value: %d, expected %d", actual, expected)
}
}
func TestRoundRobinScheduler(t *testing.T) {
scheduler := MakeRoundRobinScheduler([]string{"m1", "m2", "m3", "m4"})
expectSchedule(scheduler, Task{}, "m1", t)
expectSchedule(scheduler, Task{}, "m2", t)
expectSchedule(scheduler, Task{}, "m3", t)
expectSchedule(scheduler, Task{}, "m4", t)
}
func TestRandomScheduler(t *testing.T) {
random := rand.New(rand.NewSource(0))
scheduler := MakeRandomScheduler([]string{"m1", "m2", "m3", "m4"}, *random)
_, err := scheduler.Schedule(Task{})
expectNoError(t, err)
}
func TestFirstFitSchedulerNothingScheduled(t *testing.T) {
mockRegistry := MockTaskRegistry{}
scheduler := MakeFirstFitScheduler([]string{"m1", "m2", "m3"}, &mockRegistry)
expectSchedule(scheduler, Task{}, "m1", t)
}
func makeTask(host string, hostPorts ...int) Task {
networkPorts := []Port{}
for _, port := range hostPorts {
networkPorts = append(networkPorts, Port{HostPort: port})
}
return Task{
CurrentState: TaskState{
Host: host,
},
DesiredState: TaskState{
Manifest: ContainerManifest{
Containers: []Container{
Container{
Ports: networkPorts,
},
},
},
},
}
}
func TestFirstFitSchedulerFirstScheduled(t *testing.T) {
mockRegistry := MockTaskRegistry{
tasks: []Task{
makeTask("m1", 8080),
},
}
scheduler := MakeFirstFitScheduler([]string{"m1", "m2", "m3"}, &mockRegistry)
expectSchedule(scheduler, makeTask("", 8080), "m2", t)
}
func TestFirstFitSchedulerFirstScheduledComplicated(t *testing.T) {
mockRegistry := MockTaskRegistry{
tasks: []Task{
makeTask("m1", 80, 8080),
makeTask("m2", 8081, 8082, 8083),
makeTask("m3", 80, 443, 8085),
},
}
scheduler := MakeFirstFitScheduler([]string{"m1", "m2", "m3"}, &mockRegistry)
expectSchedule(scheduler, makeTask("", 8080, 8081), "m3", t)
}
func TestFirstFitSchedulerFirstScheduledImpossible(t *testing.T) {
mockRegistry := MockTaskRegistry{
tasks: []Task{
makeTask("m1", 8080),
makeTask("m2", 8081),
makeTask("m3", 8080),
},
}
scheduler := MakeFirstFitScheduler([]string{"m1", "m2", "m3"}, &mockRegistry)
_, err := scheduler.Schedule(makeTask("", 8080, 8081))
if err == nil {
t.Error("Unexpected non-error.")
}
}

View 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 (
"encoding/json"
"net/url"
"strconv"
"strings"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
)
type ServiceRegistry interface {
ListServices() (ServiceList, error)
CreateService(svc Service) error
GetService(name string) (*Service, error)
DeleteService(name string) error
UpdateService(svc Service) error
UpdateEndpoints(e Endpoints) error
}
type ServiceRegistryStorage struct {
registry ServiceRegistry
}
func MakeServiceRegistryStorage(registry ServiceRegistry) apiserver.RESTStorage {
return &ServiceRegistryStorage{registry: registry}
}
// GetServiceEnvironmentVariables populates a list of environment variables that are use
// in the container environment to get access to services.
func GetServiceEnvironmentVariables(registry ServiceRegistry, machine string) ([]EnvVar, error) {
var result []EnvVar
services, err := registry.ListServices()
if err != nil {
return result, err
}
for _, service := range services.Items {
name := strings.ToUpper(service.ID) + "_SERVICE_PORT"
value := strconv.Itoa(service.Port)
result = append(result, EnvVar{Name: name, Value: value})
}
result = append(result, EnvVar{Name: "SERVICE_HOST", Value: machine})
return result, nil
}
func (sr *ServiceRegistryStorage) List(*url.URL) (interface{}, error) {
return sr.registry.ListServices()
}
func (sr *ServiceRegistryStorage) Get(id string) (interface{}, error) {
return sr.registry.GetService(id)
}
func (sr *ServiceRegistryStorage) Delete(id string) error {
return sr.registry.DeleteService(id)
}
func (sr *ServiceRegistryStorage) Extract(body string) (interface{}, error) {
var svc Service
err := json.Unmarshal([]byte(body), &svc)
return svc, err
}
func (sr *ServiceRegistryStorage) Create(obj interface{}) error {
return sr.registry.CreateService(obj.(Service))
}
func (sr *ServiceRegistryStorage) Update(obj interface{}) error {
return sr.registry.UpdateService(obj.(Service))
}

View File

@@ -0,0 +1,119 @@
/*
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 (
"encoding/json"
"fmt"
"net/url"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
// TaskRegistryStorage implements the RESTStorage interface in terms of a TaskRegistry
type TaskRegistryStorage struct {
registry TaskRegistry
containerInfo client.ContainerInfo
scheduler Scheduler
}
func MakeTaskRegistryStorage(registry TaskRegistry, containerInfo client.ContainerInfo, scheduler Scheduler) apiserver.RESTStorage {
return &TaskRegistryStorage{
registry: registry,
containerInfo: containerInfo,
scheduler: scheduler,
}
}
// LabelMatch tests to see if a Task's labels map contains 'key' mapping to 'value'
func LabelMatch(task Task, queryKey, queryValue string) bool {
for key, value := range task.Labels {
if queryKey == key && queryValue == value {
return true
}
}
return false
}
// LabelMatch tests to see if a Task's labels map contains all key/value pairs in 'labelQuery'
func LabelsMatch(task Task, labelQuery *map[string]string) bool {
if labelQuery == nil {
return true
}
for key, value := range *labelQuery {
if !LabelMatch(task, key, value) {
return false
}
}
return true
}
func (storage *TaskRegistryStorage) List(url *url.URL) (interface{}, error) {
var result TaskList
var query *map[string]string
if url != nil {
queryMap := client.DecodeLabelQuery(url.Query().Get("labels"))
query = &queryMap
}
tasks, err := storage.registry.ListTasks(query)
if err == nil {
result = TaskList{
Items: tasks,
}
}
return result, err
}
func (storage *TaskRegistryStorage) Get(id string) (interface{}, error) {
task, err := storage.registry.GetTask(id)
if err != nil {
return task, err
}
info, err := storage.containerInfo.GetContainerInfo(task.CurrentState.Host, id)
if err != nil {
return task, err
}
task.CurrentState.Info = info
return task, err
}
func (storage *TaskRegistryStorage) Delete(id string) error {
return storage.registry.DeleteTask(id)
}
func (storage *TaskRegistryStorage) Extract(body string) (interface{}, error) {
task := Task{}
err := json.Unmarshal([]byte(body), &task)
return task, err
}
func (storage *TaskRegistryStorage) Create(task interface{}) error {
taskObj := task.(Task)
if len(taskObj.ID) == 0 {
return fmt.Errorf("ID is unspecified: %#v", task)
}
machine, err := storage.scheduler.Schedule(taskObj)
if err != nil {
return err
}
return storage.registry.CreateTask(machine, taskObj)
}
func (storage *TaskRegistryStorage) Update(task interface{}) error {
return storage.registry.UpdateTask(task.(Task))
}

View File

@@ -0,0 +1,204 @@
/*
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 (
"encoding/json"
"fmt"
"testing"
. "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
)
type MockTaskRegistry struct {
err error
tasks []Task
}
func expectNoError(t *testing.T, err error) {
if err != nil {
t.Errorf("Unexpected error: %#v", err)
}
}
func (registry *MockTaskRegistry) ListTasks(*map[string]string) ([]Task, error) {
return registry.tasks, registry.err
}
func (registry *MockTaskRegistry) GetTask(taskId string) (*Task, error) {
return &Task{}, registry.err
}
func (registry *MockTaskRegistry) CreateTask(machine string, task Task) error {
return registry.err
}
func (registry *MockTaskRegistry) UpdateTask(task Task) error {
return registry.err
}
func (registry *MockTaskRegistry) DeleteTask(taskId string) error {
return registry.err
}
func TestListTasksError(t *testing.T) {
mockRegistry := MockTaskRegistry{
err: fmt.Errorf("Test Error"),
}
storage := TaskRegistryStorage{
registry: &mockRegistry,
}
tasks, err := storage.List(nil)
if err != mockRegistry.err {
t.Errorf("Expected %#v, Got %#v", mockRegistry.err, err)
}
if len(tasks.(TaskList).Items) != 0 {
t.Errorf("Unexpected non-zero task list: %#v", tasks)
}
}
func TestListEmptyTaskList(t *testing.T) {
mockRegistry := MockTaskRegistry{}
storage := TaskRegistryStorage{
registry: &mockRegistry,
}
tasks, err := storage.List(nil)
expectNoError(t, err)
if len(tasks.(TaskList).Items) != 0 {
t.Errorf("Unexpected non-zero task list: %#v", tasks)
}
}
func TestListTaskList(t *testing.T) {
mockRegistry := MockTaskRegistry{
tasks: []Task{
Task{
JSONBase: JSONBase{
ID: "foo",
},
},
Task{
JSONBase: JSONBase{
ID: "bar",
},
},
},
}
storage := TaskRegistryStorage{
registry: &mockRegistry,
}
tasksObj, err := storage.List(nil)
tasks := tasksObj.(TaskList)
expectNoError(t, err)
if len(tasks.Items) != 2 {
t.Errorf("Unexpected task list: %#v", tasks)
}
if tasks.Items[0].ID != "foo" {
t.Errorf("Unexpected task: %#v", tasks.Items[0])
}
if tasks.Items[1].ID != "bar" {
t.Errorf("Unexpected task: %#v", tasks.Items[1])
}
}
func TestExtractJson(t *testing.T) {
mockRegistry := MockTaskRegistry{}
storage := TaskRegistryStorage{
registry: &mockRegistry,
}
task := Task{
JSONBase: JSONBase{
ID: "foo",
},
}
body, err := json.Marshal(task)
expectNoError(t, err)
taskOut, err := storage.Extract(string(body))
expectNoError(t, err)
jsonOut, err := json.Marshal(taskOut)
expectNoError(t, err)
if string(body) != string(jsonOut) {
t.Errorf("Expected %#v, found %#v", task, taskOut)
}
}
func expectLabelMatch(t *testing.T, task Task, key, value string) {
if !LabelMatch(task, key, value) {
t.Errorf("Unexpected match failure: %#v %s %s", task, key, value)
}
}
func expectNoLabelMatch(t *testing.T, task Task, key, value string) {
if LabelMatch(task, key, value) {
t.Errorf("Unexpected match success: %#v %s %s", task, key, value)
}
}
func expectLabelsMatch(t *testing.T, task Task, query *map[string]string) {
if !LabelsMatch(task, query) {
t.Errorf("Unexpected match failure: %#v %#v", task, *query)
}
}
func expectNoLabelsMatch(t *testing.T, task Task, query *map[string]string) {
if LabelsMatch(task, query) {
t.Errorf("Unexpected match success: %#v %#v", task, *query)
}
}
func TestLabelMatch(t *testing.T) {
task := Task{
Labels: map[string]string{
"foo": "bar",
"baz": "blah",
},
}
expectLabelMatch(t, task, "foo", "bar")
expectLabelMatch(t, task, "baz", "blah")
expectNoLabelMatch(t, task, "foo", "blah")
expectNoLabelMatch(t, task, "baz", "bar")
}
func TestLabelsMatch(t *testing.T) {
task := Task{
Labels: map[string]string{
"foo": "bar",
"baz": "blah",
},
}
expectLabelsMatch(t, task, &map[string]string{})
expectLabelsMatch(t, task, &map[string]string{
"foo": "bar",
})
expectLabelsMatch(t, task, &map[string]string{
"baz": "blah",
})
expectLabelsMatch(t, task, &map[string]string{
"foo": "bar",
"baz": "blah",
})
expectNoLabelsMatch(t, task, &map[string]string{
"foo": "blah",
})
expectNoLabelsMatch(t, task, &map[string]string{
"baz": "bar",
})
expectNoLabelsMatch(t, task, &map[string]string{
"foo": "bar",
"foobar": "bar",
"baz": "blah",
})
}

56
pkg/util/fake_handler.go Normal file
View File

@@ -0,0 +1,56 @@
/*
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 util
import (
"io/ioutil"
"log"
"net/http"
"testing"
)
// FakeHandler is to assist in testing HTTP requests.
type FakeHandler struct {
RequestReceived *http.Request
StatusCode int
ResponseBody string
}
func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
f.RequestReceived = request
response.WriteHeader(f.StatusCode)
response.Write([]byte(f.ResponseBody))
bodyReceived, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Received read error: %#v", err)
}
f.ResponseBody = string(bodyReceived)
}
func (f FakeHandler) ValidateRequest(t *testing.T, expectedPath, expectedMethod string, body *string) {
if f.RequestReceived.URL.Path != expectedPath {
t.Errorf("Unexpected request path: %s", f.RequestReceived.URL.Path)
}
if f.RequestReceived.Method != expectedMethod {
t.Errorf("Unexpected method: %s", f.RequestReceived.Method)
}
if body != nil {
if *body != f.ResponseBody {
t.Errorf("Received body:\n%s\n Doesn't match expected body:\n%s", f.ResponseBody, *body)
}
}
}

37
pkg/util/stringlist.go Normal file
View File

@@ -0,0 +1,37 @@
/*
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 util
import (
"fmt"
"strings"
)
type StringList []string
func (sl *StringList) String() string {
return fmt.Sprint(*sl)
}
func (sl *StringList) Set(value string) error {
for _, s := range strings.Split(value, ",") {
if len(s) == 0 {
return fmt.Errorf("value should not be an empty string")
}
*sl = append(*sl, s)
}
return nil
}

View File

@@ -0,0 +1,41 @@
/*
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 util
import (
"reflect"
"testing"
)
func TestStringListSet(t *testing.T) {
var sl StringList
sl.Set("foo,bar")
sl.Set("hop")
expected := []string{"foo", "bar", "hop"}
if reflect.DeepEqual(expected, []string(sl)) == false {
t.Errorf("expected: %v, got: %v:", expected, sl)
}
}
func TestStringListSetErr(t *testing.T) {
var sl StringList
if err := sl.Set(""); err == nil {
t.Errorf("expected error for empty string")
}
if err := sl.Set(","); err == nil {
t.Errorf("expected error for list of empty strings")
}
}

47
pkg/util/util.go Normal file
View File

@@ -0,0 +1,47 @@
/*
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 util
import (
"encoding/json"
"log"
"time"
)
// Simply catches a crash and logs an error. Meant to be called via defer.
func HandleCrash() {
r := recover()
if r != nil {
log.Printf("Recovered from panic: %#v", r)
}
}
// Loops forever running f every d. Catches any panics, and keeps going.
func Forever(f func(), period time.Duration) {
for {
func() {
defer HandleCrash()
f()
}()
time.Sleep(period)
}
}
// Returns o marshalled as a JSON string, ignoring any errors.
func MakeJSONString(o interface{}) string {
data, _ := json.Marshal(o)
return string(data)
}