Commit 2c4b3a56 authored by Joe Beda's avatar Joe Beda

First commit

parents
# OSX leaves these everywhere on SMB shares
._*
# Eclipse files
.classpath
.project
.settings/**
# This is where the result of the go build goes
/target/**
/target
# This is where we stage releases
/release/**
# Emacs save files
*~
This diff is collapsed. Click to expand it.
# Kubernetes
Kubernetes is an open source reference implementation of container cluster management.
## Getting started on Google Compute Engine
### Prerequisites
1. You need a Google Cloud Platform account with billing enabled. Visit http://cloud.google.com/console for more details
2. You must have Go installed: [www.golang.org](http://www.golang.org)
3. Ensure that your `gcloud` components are up-to-date by running `gcloud components update`.
4. Get the Kubernetes source: `git clone https://github.com/GoogleCloudPlatform/kubernetes.git`
### Setup
```
cd kubernetes
./src/scripts/dev-build-and-up.sh
```
### Running a container (simple version)
```
cd kubernetes
./src/scripts/build-go.sh
./src/scripts/cloudcfg.sh -p 8080:80 run dockerfile/nginx 2 myNginx
```
This will spin up two containers running Nginx mapping port 80 to 8080.
To stop the container:
```
./src/scripts/cloudcfg.sh stop myNginx
```
To delete the container:
```
./src/scripts/cloudcfg.sh rm myNginx
```
### Running a container (more complete version)
```
cd kubernetes
./src/scripts/cloudcfg.sh -c examples/task.json create /tasks
```
Where task.json contains something like:
```
{
"ID": "nginx",
"desiredState": {
"image": "dockerfile/nginx",
"networkPorts": [{
"containerPort": 80,
"hostPort": 8080
}]
},
"labels": {
"name": "foo"
}
}
```
Look in the ```examples/``` for more examples
### Tearing down the cluster
```
cd kubernetes
./src/scripts/kube-down.sh
```
## Development
### Hooks
```
# Before committing any changes, please link/copy these hooks into your .git
# directory. This will keep you from accidentally committing non-gofmt'd
# go code.
cd kubernetes
ln -s "../../hooks/prepare-commit-msg" .git/hooks/prepare-commit-msg
ln -s "../../hooks/commit-msg" .git/hooks/commit-msg
```
### Unit tests
```
cd kubernetes
./src/scripts/test-go.sh
```
### Coverage
```
cd kubernetes
go tool cover -html=target/c.out
```
### Integration tests
```
# You need an etcd somewhere in your path.
# To get from head:
go get github.com/coreos/etcd
go install github.com/coreos/etcd
sudo ln -s "$GOPATH/bin/etcd" /usr/bin/etcd
# Or just use the packaged one:
sudo ln -s "$REPO_ROOT/target/bin/etcd" /usr/bin/etcd
```
```
cd kubernetes
./src/scripts/integration-test.sh
```
### Keeping your development fork in sync
One time after cloning your forked repo:
```
git remote add upstream https://github.com/GoogleCloudPlatform/kubernetes.git
```
Then each time you want to sync to upstream:
```
git fetch upstream
git rebase upstream/master
```
### Regenerating the documentation
Install [nodejs](http://nodejs.org/download/), [npm](https://www.npmjs.org/), and
[raml2html](https://github.com/kevinrenskers/raml2html), then run:
```
cd kubernetes/api
raml2html kubernetes.raml > kubernetes.html
```
{
"$schema": "http://json-schema.org/draft-03/schema",
"type": "object",
"required": false,
"description": "A replicationController resource. A replicationController helps to create and manage a set of tasks. It acts as a factory to create new tasks based on a template. It ensures that there are a specific number of tasks running. If fewer tasks are running than `replicas` then the needed tasks are generated using `taskTemplate`. If more tasks are running than `replicas`, then excess tasks are deleted.",
"properties": {
"kind": {
"type": "string",
"required": false
},
"id": {
"type": "string",
"required": false
},
"creationTimestamp": {
"type": "string",
"required": false
},
"selfLink": {
"type": "string",
"required": false
},
"desiredState": {
"type": "object",
"required": false,
"description": "The desired configuration of the replicationController",
"properties": {
"replicas": {
"type": "number",
"required": false,
"description": "Number of tasks desired in the set"
},
"replicasInSet": {
"type": "object",
"required": false,
"description": "Required labels used to identify tasks in the set"
},
"taskTemplate": {
"type": "object",
"required": false,
"description": "Template from which to create new tasks, as necessary. Identical to task schema."
}
}
},
"labels": {
"type": "object",
"required": false
}
}
}
{
"$schema": "http://json-schema.org/draft-03/schema",
"type": "object",
"required": false,
"description": "A service resource.",
"properties": {
"kind": {
"type": "string",
"required": false
},
"id": {
"type": "string",
"required": false
},
"creationTimestamp": {
"type": "string",
"required": false
},
"selfLink": {
"type": "string",
"required": false
},
"name": {
"type": "string",
"required": false
},
"port": {
"type": "number",
"required": false
},
"labels": {
"type": "object",
"required": false
}
}
}
{
"$schema": "http://json-schema.org/draft-03/schema",
"type": "object",
"required": false,
"description": "Task resource. A task corresponds to a colocated group of [Docker containers](http://docker.io).",
"properties": {
"kind": {
"type": "string",
"required": false
},
"id": {
"type": "string",
"required": false
},
"creationTimestamp": {
"type": "string",
"required": false
},
"selfLink": {
"type": "string",
"required": false
},
"desiredState": {
"type": "object",
"required": false,
"description": "The desired configuration of the task",
"properties": {
"manifest": {
"type": "object",
"required": false,
"description": "Manifest describing group of [Docker containers](http://docker.io); compatible with format used by [Google Cloud Platform's container-vm images](https://developers.google.com/compute/docs/containers)"
},
"status": {
"type": "string",
"required": false,
"description": ""
},
"host": {
"type": "string",
"required": false,
"description": ""
},
"hostIP": {
"type": "string",
"required": false,
"description": ""
},
"info": {
"type": "object",
"required": false,
"description": ""
}
}
},
"currentState": {
"type": "object",
"required": false,
"description": "The current configuration and status of the task. Fields in common with desiredState have the same meaning.",
"properties": {
"manifest": {
"type": "object",
"required": false
},
"status": {
"type": "string",
"required": false
},
"host": {
"type": "string",
"required": false
},
"hostIP": {
"type": "string",
"required": false
},
"info": {
"type": "object",
"required": false
}
}
},
"labels": {
"type": "object",
"required": false
}
}
}
{
"items": [
{
"id": "testRun",
"desiredState": {
"replicas": 2,
"replicasInSet": {
"name": "testRun"
},
"taskTemplate": {
"desiredState": {
"image": "dockerfile/nginx",
"networkPorts": [
{
"hostPort": 8080,
"containerPort": 80
}
]
},
"labels": {
"name": "testRun"
}
}
},
"labels": {
"name": "testRun"
}
}
]
}
\ No newline at end of file
{
"id": "nginxController",
"desiredState": {
"replicas": 2,
"replicasInSet": {"name": "nginx"},
"taskTemplate": {
"desiredState": {
"manifest": {
"containers": [{
"image": "dockerfile/nginx",
"ports": [{"containerPort": 80, "hostPort": 8080}]
}]
}
},
"labels": {"name": "nginx"}
}},
"labels": {"name": "nginx"}
}
{
"items": [
{
"id": "example1",
"port": 8000,
"labels": {
"name": "nginx"
}
},
{
"id": "example2",
"port": 8080,
"labels": {
"env": "prod",
"name": "jetty"
}
}
]
}
{
"id": "example2",
"port": 8000,
"labels": {
"name": "nginx"
}
}
{
"items": [
{
"id": "my-task-1",
"labels": {
"name": "testRun",
"replicationController": "testRun"
},
"desiredState": {
"manifest": {
"containers": [{
"image": "dockerfile/nginx",
"ports": [{
"hostPort": 8080,
"containerPort": 80
}]
}
}
},
"currentState": {
"host": "host-1"
}
},
{
"id": "my-task-2",
"labels": {
"name": "testRun",
"replicationController": "testRun"
},
"desiredState": {
"manifest": {
"containers": [{
"image": "dockerfile/nginx",
"ports": [{
"hostPort": 8080,
"containerPort": 80
}]
}
}
},
"currentState": {
"host": "host-2"
}
}
]
}
\ No newline at end of file
{
"id": "php",
"desiredState": {
"manifest": {
"containers": [{
"image": "dockerfile/nginx",
"ports": [{
"containerPort": 80,
"hostPort": 8080
}]
}]
}
},
"labels": {
"name": "foo"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
#%RAML 0.8
baseUri: http://server/api/{version}
title: Kubernetes
version: v1beta1
mediaType: application/json
documentation:
- title: Overview
content: |
The Kubernetes API currently manages 3 main resources: `tasks`,
`replicationControllers`, and `services`. Tasks correspond to
colocated groups of [Docker containers](http://docker.io) with
shared volumes, as supported by [Google Cloud Platform's
container-vm
images](https://developers.google.com/compute/docs/containers).
Singleton tasks can be created directly via the `/tasks`
endpoint. Sets of tasks may created, maintained, and scaled using
replicationControllers. Services create load-balanced targets
for sets of tasks.
- title: Resource identifiers
content: |
Each resource has a string `id` and list of key-value
`labels`. The `id` is generated by the system and is guaranteed
to be unique in space and time across all resources. `labels`
is a map of string (key) to string (value). Each resource may
have at most one label with a particular key. Individual labels
are used to specify identifying metadata that can be used to
define sets of resources by specifying required labels. Examples
of typical task label keys include `stage`, `service`, `name`,
`tier`, `partition`, and `track`, but you are free to develop
your own conventions.
- title: Creation semantics
content: |
Creation is currently not idempotent. We plan to add a
modification token to each resource. A unique value for the token
should be provided by the user during creation. If the user
specifies a duplicate token at creation time, the system should
return an error with a pointer to the exiting resource with that
token. In this way a user can deterministically recover from a
dropped connection during a resource creation request.
- title: Update semantics
content: |
Custom verbs are minimized and are used only for 'edge triggered'
actions such as a reboot. Resource descriptions are generally set
up with `desiredState` for the user provided parameters and
`currentState` for the actual system state. While consistent
terminology is used across these two stanzas they do not match
member for member.
When a new version of a resource is PUT the `desiredState` is
updated and available immediately. Over time the system will work
to bring the `currentState` into line with the `desiredState`. The
system will drive toward the most recent `desiredState` regardless
of previous versions of that stanza. In other words, if a value
is changed from 2 to 5 in one PUT and then back down to 3 in
another PUT the system isn't required to 'touch base' at 5 before
making 3 the `currentState`.
When doing an update, we assume that the entire `desiredState`
stanza is specified. If a field is omitted it is assumed that the
user is looking to delete that field. It is viable for a user to
GET the resource, modify what they like in the `desiredState` or
labels stanzas and then PUT it back. If the `currentState` is
included in the PUT it will be silently ignored.
While currently unspecified, it is intended that concurrent
modification should be accomplished with optimistic locking of
resources. We plan to add a modification token to each resource. If
this is included with the PUT operation the system will verify
that there haven't been other successful mutations to the
resource during a read/modify/write cycle. The correct client
action at this point is to GET the resource again, apply the
changes afresh and try submitting again.
Note that updates currently only work for replicationControllers
and services, but not for tasks. Label updates have not yet been
implemented, either.
/tasks:
get:
description: List all tasks on this cluster
responses:
200:
body:
application/json:
example: !include examples/task-list.json
post:
description: Create a new task. currentState is ignored if present.
body:
json/application:
schema: !include doc/task-schema.json
example: !include examples/task.json
/{taskId}:
get:
description: Get a specific task
responses:
200:
body:
application/json:
example: !include examples/task.json
put:
description: Update a task
body:
json/application:
schema: !include doc/task-schema.json
example: !include examples/task.json
delete:
description: Delete a specific task
responses:
200:
body:
application/json:
example: |
{
"success": true
}
/replicationControllers:
get:
description: List all replicationControllers on this cluster
responses:
200:
body:
application/json:
example: !include examples/controller-list.json
post:
description: Create a new controller. currentState is ignored if present.
body:
json/application:
schema: !include doc/controller-schema.json
example: !include examples/controller.json
/{controllerId}:
get:
description: Get a specific controller
responses:
200:
body:
application/json:
example: !include examples/controller.json
put:
description: Update a controller
body:
json/application:
schema: !include doc/controller-schema.json
example: !include examples/controller.json
delete:
description: Delete a specific controller
responses:
200:
body:
application/json:
example: |
{
"success": true
}
/services:
get:
description: List all services on this cluster
responses:
200:
body:
application/json:
example: !include examples/service-list.json
post:
description: Create a new service
body:
json/application:
schema: !include doc/service-schema.json
example: !include examples/service.json
/{serviceId}:
get:
description: Get a specific service
responses:
200:
body:
application/json:
example: !include examples/service.json
put:
description: Update a service
body:
json/application:
schema: !include doc/service-schema.json
example: !include examples/service.json
delete:
description: Delete a specific service
responses:
200:
body:
application/json:
example: |
{
"success": true
}
/*
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.
*/
// apiserver is the main api server and master for the cluster.
// it is responsible for serving the cluster management API.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"time"
"github.com/coreos/go-etcd/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
var (
port = flag.Uint("port", 8080, "The port to listen on. Default 8080.")
address = flag.String("address", "127.0.0.1", "The address on the local server to listen to. Default 127.0.0.1")
apiPrefix = flag.String("api_prefix", "/api/v1beta1", "The prefix for API requests on the server. Default '/api/v1beta1'")
etcdServerList, machineList util.StringList
)
func init() {
flag.Var(&etcdServerList, "etcd_servers", "Servers for the etcd (http://ip:port), comma separated")
flag.Var(&machineList, "machines", "List of machines to schedule onto, comma separated.")
}
func main() {
flag.Parse()
if len(machineList) == 0 {
log.Fatal("No machines specified!")
}
var (
taskRegistry registry.TaskRegistry
controllerRegistry registry.ControllerRegistry
serviceRegistry registry.ServiceRegistry
)
if len(etcdServerList) > 0 {
log.Printf("Creating etcd client pointing to %v", etcdServerList)
etcdClient := etcd.NewClient(etcdServerList)
taskRegistry = registry.MakeEtcdRegistry(etcdClient, machineList)
controllerRegistry = registry.MakeEtcdRegistry(etcdClient, machineList)
serviceRegistry = registry.MakeEtcdRegistry(etcdClient, machineList)
} else {
taskRegistry = registry.MakeMemoryRegistry()
controllerRegistry = registry.MakeMemoryRegistry()
serviceRegistry = registry.MakeMemoryRegistry()
}
containerInfo := &kube_client.HTTPContainerInfo{
Client: http.DefaultClient,
Port: 10250,
}
storage := map[string]apiserver.RESTStorage{
"tasks": registry.MakeTaskRegistryStorage(taskRegistry, containerInfo, registry.MakeFirstFitScheduler(machineList, taskRegistry)),
"replicationControllers": registry.MakeControllerRegistryStorage(controllerRegistry),
"services": registry.MakeServiceRegistryStorage(serviceRegistry),
}
endpoints := registry.MakeEndpointController(serviceRegistry, taskRegistry)
go util.Forever(func() { endpoints.SyncServiceEndpoints() }, time.Second*10)
s := &http.Server{
Addr: fmt.Sprintf("%s:%d", *address, *port),
Handler: apiserver.New(storage, *apiPrefix),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudcfg"
)
const APP_VERSION = "0.1"
// The flag package provides a default help printer via -h switch
var versionFlag *bool = flag.Bool("v", false, "Print the version number.")
var httpServer *string = flag.String("h", "", "The host to connect to.")
var config *string = flag.String("c", "", "Path to the config file.")
var labelQuery *string = flag.String("l", "", "Label query to use for listing")
var updatePeriod *time.Duration = flag.Duration("u", 60*time.Second, "Update interarrival in seconds")
var portSpec *string = flag.String("p", "", "The port spec, comma-separated list of <external>:<internal>,...")
var servicePort *int = flag.Int("s", -1, "If positive, create and run a corresponding service on this port, only used with 'run'")
var authConfig *string = flag.String("auth", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user")
func usage() {
log.Fatal("Usage: cloudcfg -h <host> [-c config/file.json] [-p <hostPort>:<containerPort>,..., <hostPort-n>:<containerPort-n> <method> <path>")
}
// CloudCfg command line tool.
func main() {
flag.Parse() // Scan the arguments list
if *versionFlag {
fmt.Println("Version:", APP_VERSION)
os.Exit(0)
}
if len(flag.Args()) < 2 {
usage()
}
method := flag.Arg(0)
url := *httpServer + "/api/v1beta1" + flag.Arg(1)
var request *http.Request
var err error
auth, err := cloudcfg.LoadAuthInfo(*authConfig)
if err != nil {
log.Fatalf("Error loading auth: %#v", err)
}
if method == "get" || method == "list" {
if len(*labelQuery) > 0 && method == "list" {
url = url + "?labels=" + *labelQuery
}
request, err = http.NewRequest("GET", url, nil)
} else if method == "delete" {
request, err = http.NewRequest("DELETE", url, nil)
} else if method == "create" {
request, err = cloudcfg.RequestWithBody(*config, url, "POST")
} else if method == "update" {
request, err = cloudcfg.RequestWithBody(*config, url, "PUT")
} else if method == "rollingupdate" {
client := &kube_client.Client{
Host: *httpServer,
Auth: &auth,
}
cloudcfg.Update(flag.Arg(1), client, *updatePeriod)
} else if method == "run" {
args := flag.Args()
if len(args) < 4 {
log.Fatal("usage: cloudcfg -h <host> run <image> <replicas> <name>")
}
image := args[1]
replicas, err := strconv.Atoi(args[2])
name := args[3]
if err != nil {
log.Fatalf("Error parsing replicas: %#v", err)
}
err = cloudcfg.RunController(image, name, replicas, kube_client.Client{Host: *httpServer, Auth: &auth}, *portSpec, *servicePort)
if err != nil {
log.Fatalf("Error: %#v", err)
}
return
} else if method == "stop" {
err = cloudcfg.StopController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: &auth})
if err != nil {
log.Fatalf("Error: %#v", err)
}
return
} else if method == "rm" {
err = cloudcfg.DeleteController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: &auth})
if err != nil {
log.Fatalf("Error: %#v", err)
}
return
} else {
log.Fatalf("Unknown command: %s", method)
}
if err != nil {
log.Fatalf("Error: %#v", err)
}
var body string
body, err = cloudcfg.DoRequest(request, auth.User, auth.Password)
if err != nil {
log.Fatalf("Error: %#v", err)
}
fmt.Println(body)
}
/*
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.
*/
// The controller manager is responsible for monitoring replication controllers, and creating corresponding
// tasks to achieve the desired state. It listens for new controllers in etcd, and it sends requests to the
// master to create/delete tasks.
//
// TODO: Refactor the etcd watch code so that it is a pluggable interface.
package main
import (
"flag"
"log"
"os"
"time"
kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/coreos/go-etcd/etcd"
)
var (
etcd_servers = flag.String("etcd_servers", "", "Servers for the etcd (http://ip:port).")
master = flag.String("master", "", "The address of the Kubernetes API server")
)
func main() {
flag.Parse()
if len(*etcd_servers) == 0 || len(*master) == 0 {
log.Fatal("usage: controller-manager -etcd_servers <servers> -master <master>")
}
// Set up logger for etcd client
etcd.SetLogger(log.New(os.Stderr, "etcd ", log.LstdFlags))
controllerManager := registry.MakeReplicationManager(etcd.NewClient([]string{*etcd_servers}),
kube_client.Client{
Host: "http://" + *master,
})
go util.Forever(func() { controllerManager.Synchronize() }, 20*time.Second)
go util.Forever(func() { controllerManager.WatchControllers() }, 20*time.Second)
select {}
}
/*
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 basic integration test for the service.
// Assumes that there is a pre-existing etcd server running on localhost.
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http/httptest"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry"
"github.com/coreos/go-etcd/etcd"
)
func main() {
// Setup
servers := []string{"http://localhost:4001"}
log.Printf("Creating etcd client pointing to %v", servers)
etcdClient := etcd.NewClient(servers)
machineList := []string{"machine"}
reg := registry.MakeEtcdRegistry(etcdClient, machineList)
apiserver := apiserver.New(map[string]apiserver.RESTStorage{
"tasks": registry.MakeTaskRegistryStorage(reg, &kube_client.FakeContainerInfo{}, registry.MakeRoundRobinScheduler(machineList)),
"replicationControllers": registry.MakeControllerRegistryStorage(reg),
}, "/api/v1beta1")
server := httptest.NewServer(apiserver)
controllerManager := registry.MakeReplicationManager(etcd.NewClient(servers),
kube_client.Client{
Host: server.URL,
})
go controllerManager.Synchronize()
go controllerManager.WatchControllers()
// Ok. we're good to go.
log.Printf("API Server started on %s", server.URL)
// Wait for the synchronization threads to come up.
time.Sleep(time.Second * 10)
kubeClient := kube_client.Client{
Host: server.URL,
}
data, err := ioutil.ReadFile("api/examples/controller.json")
if err != nil {
log.Fatalf("Unexpected error: %#v", err)
}
var controllerRequest api.ReplicationController
if err = json.Unmarshal(data, &controllerRequest); err != nil {
log.Fatalf("Unexpected error: %#v", err)
}
if _, err = kubeClient.CreateReplicationController(controllerRequest); err != nil {
log.Fatalf("Unexpected error: %#v", err)
}
// Give the controllers some time to actually create the tasks
time.Sleep(time.Second * 10)
// Validate that they're truly up.
tasks, err := kubeClient.ListTasks(nil)
if err != nil || len(tasks.Items) != 2 {
log.Fatal("FAILED")
}
log.Printf("OK")
}
/*
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.
*/
// The kubelet binary is responsible for maintaining a set of containers on a particular host VM.
// It sync's data from both configuration file as well as from a quorum of etcd servers.
// It then queries Docker to see what is currently running. It synchronizes the configuration data,
// with the running set of containers by starting or stopping Docker containers.
package main
import (
"flag"
"log"
"math/rand"
"os"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
"github.com/coreos/go-etcd/etcd"
"github.com/fsouza/go-dockerclient"
)
var (
file = flag.String("config", "", "Path to the config file")
etcd_servers = flag.String("etcd_servers", "", "Url of etcd servers in the cluster")
syncFrequency = flag.Duration("sync_frequency", 10*time.Second, "Max seconds between synchronizing running containers and config")
fileCheckFrequency = flag.Duration("file_check_frequency", 20*time.Second, "Seconds between checking file for new data")
httpCheckFrequency = flag.Duration("http_check_frequency", 20*time.Second, "Seconds between checking http for new data")
manifest_url = flag.String("manifest_url", "", "URL for accessing the container manifest")
address = flag.String("address", "127.0.0.1", "The address for the info server to serve on")
port = flag.Uint("port", 10250, "The port for the info server to serve on")
)
const dockerBinary = "/usr/bin/docker"
func main() {
flag.Parse()
rand.Seed(time.Now().UTC().UnixNano())
// Set up logger for etcd client
etcd.SetLogger(log.New(os.Stderr, "etcd ", log.LstdFlags))
endpoint := "unix:///var/run/docker.sock"
dockerClient, err := docker.NewClient(endpoint)
if err != nil {
log.Fatal("Couldn't connnect to docker.")
}
my_kubelet := kubelet.Kubelet{
DockerClient: dockerClient,
FileCheckFrequency: *fileCheckFrequency,
SyncFrequency: *syncFrequency,
HTTPCheckFrequency: *httpCheckFrequency,
}
my_kubelet.RunKubelet(*file, *manifest_url, *etcd_servers, *address, *port)
}
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"log"
"os"
"github.com/GoogleCloudPlatform/kubernetes/pkg/proxy"
"github.com/GoogleCloudPlatform/kubernetes/pkg/proxy/config"
"github.com/coreos/go-etcd/etcd"
)
var (
config_file = flag.String("configfile", "/tmp/proxy_config", "Configuration file for the proxy")
etcd_servers = flag.String("etcd_servers", "http://10.240.10.57:4001", "Servers for the etcd cluster (http://ip:port).")
)
func main() {
flag.Parse()
// Set up logger for etcd client
etcd.SetLogger(log.New(os.Stderr, "etcd ", log.LstdFlags))
log.Printf("Using configuration file %s and etcd_servers %s", *config_file, *etcd_servers)
proxyConfig := config.NewServiceConfig()
// Create a configuration source that handles configuration from etcd.
etcdClient := etcd.NewClient([]string{*etcd_servers})
config.NewConfigSourceEtcd(etcdClient,
proxyConfig.GetServiceConfigurationChannel("etcd"),
proxyConfig.GetEndpointsConfigurationChannel("etcd"))
// And create a configuration source that reads from a local file
config.NewConfigSourceFile(*config_file,
proxyConfig.GetServiceConfigurationChannel("file"),
proxyConfig.GetEndpointsConfigurationChannel("file"))
loadBalancer := proxy.NewLoadBalancerRR()
proxier := proxy.NewProxier(loadBalancer)
// Wire proxier to handle changes to services
proxyConfig.RegisterServiceHandler(proxier)
// And wire loadBalancer to handle changes to endpoints to services
proxyConfig.RegisterEndpointsHandler(loadBalancer)
// Just loop forever for now...
select {}
}
{
"id": "frontendController",
"desiredState": {
"replicas": 3,
"replicasInSet": {"name": "frontend"},
"taskTemplate": {
"desiredState": {
"manifest": {
"containers": [{
"image": "brendanburns/php-redis",
"ports": [{"containerPort": 80, "hostPort": 8080}]
}]
}
},
"labels": {"name": "frontend"}
}},
"labels": {"name": "frontend"}
}
## GuestBook example
This example shows how to build a simple multi-tier web application using Kubernetes and Docker.
The example combines a web frontend, a redis master for storage and a replicated set of redis slaves.
### Step Zero: Prerequisites
This example assumes that you have forked the repository and turned up a Kubernetes cluster.
### Step One: Turn up the redis master.
Create a file named redis-master.json, this file is describes a single task, which runs a redis key-value server in a container.
```javascript
{
"id": "redis-master-2",
"desiredState": {
"manifest": {
"containers": [{
"name": "master",
"image": "dockerfile/redis",
"ports": [{
"containerPort": 6379,
"hostPort": 6379
}]
}]
}
},
"labels": {
"name": "redis-master"
}
}
```
Once you have that task file, you can create the redis task in your Kubernetes cluster using the cloudcfg cli:
```shell
./src/scripts/cloudcfg.sh -c redis-master.json create /tasks
```
Once that's up you can list the tasks in the cluster, to verify that the master is running:
```shell
./src/scripts/cloudcfg.sh list /tasks
```
You should see a single redis master task. It will also display the machine that the task is running on. If you ssh to that machine, you can run
```shell
sudo docker ps
```
And see the actual task. (note that initial ```docker pull``` may take a few minutes, depending on network conditions.
### Step Two: Turn up the master service.
A Kubernetes 'service' is named load balancer that proxies traffic to one or more containers. The services in a Kubernetes cluster are discoverable inside other containers via environment variables. Services find the containers to load balance based on task labels. The task that you created in Step One has the label "name=redis-master", so the corresponding service is defined by that label. Create a file named redis-master-service.json that contains:
```javascript
{
"id": "redismaster",
"port": 10000,
"labels": {
"name": "redis-master"
}
}
```
Once you have that service description, you can create the service with the cloudcfg cli:
```shell
./src/scripts/cloudcfg.sh -c redis-master-service create /services
```
Once created, the service proxy on each minion is configured to set up a proxy on the specified port (in this case port 10000).
### Step Three: Turn up the replicated slave service.
Although the redis master is a single task, the redis read slaves are a 'replicated' task, in Kubernetes, a replication controller is responsible for managing multiple instances of a replicated task. Create a file named redis-slave-controller.json that contains:
```javascript
{
"id": "redisSlaveController",
"desiredState": {
"replicas": 2,
"replicasInSet": {"name": "redis-slave"},
"taskTemplate": {
"desiredState": {
"manifest": {
"containers": [{
"image": "brendanburns/redis-slave",
"ports": [{"containerPort": 6379, "hostPort": 6380}]
}]
}
},
"labels": {"name": "redis-slave"}
}},
"labels": {"name": "redis-slave"}
}
```
Then you can create the service by running:
```shell
./src/scripts/cloudcfg.sh -c redis-slave-controller.json create /replicationControllers
```
The redis slave configures itself by looking for the Kubernetes service environment variables in the container environment. In particular, the redis slave is started with the following command:
```shell
redis-server --slaveof $SERVICE_HOST $REDISMASTER_SERVICE_PORT
```
Once that's up you can list the tasks in the cluster, to verify that the master and slaves are running:
```shell
./src/scripts/cloudcfg.sh list /tasks
```
You should see a single redis master task, and two redis slave tasks.
### Step Four: Create the redis slave service.
Just like the master, we want to have a service to proxy connections to the read slaves. In this case, in addition to discovery, the slave service provides transparent load balancing to clients. As before, create a service specification:
```javascript
{
"id": "redisslave",
"port": 10001,
"labels": {
"name": "redis-slave"
}
}
```
This time the label query for the service is 'name=redis-slave'
Now that you have created the service specification, create it in your cluster with the cloudcfg cli:
```shell
./src/scripts/cloudcfg.sh -c redis-slave-service.json create /services
```
### Step Five: Create the frontend service.
This is a simple PHP server that is configured to talk to both the slave and master services depdending on if the request is a read or a write. It exposes a simple AJAX interface, and serves an angular based U/X. Like the redis read slaves it is a replicated service instantiated by a replication controller. Create a file named frontend-controller.json:
```javascript
{
"id": "frontendController",
"desiredState": {
"replicas": 3,
"replicasInSet": {"name": "frontend"},
"taskTemplate": {
"desiredState": {
"manifest": {
"containers": [{
"image": "brendanburns/php-redis",
"ports": [{"containerPort": 80, "hostPort": 8080}]
}]
}
},
"labels": {"name": "frontend"}
}},
"labels": {"name": "frontend"}
}
```
With this file, you can turn up your frontend with:
```shell
./src/scripts/cloudcfg.sh -c frontend-controller.json create /replicationControllers
```
Once that's up you can list the tasks in the cluster, to verify that the master, slaves and frontends are running:
```shell
./src/scripts/cloudcfg.sh list /tasks
```
You should see a single redis master task, two redis slave and three frontend tasks.
The code for the PHP service looks like this:
```php
<?
set_include_path('.:/usr/share/php:/usr/share/pear:/vendor/predis');
error_reporting(E_ALL);
ini_set('display_errors', 1);
require 'predis/autoload.php';
if (isset($_GET['cmd']) === true) {
header('Content-Type: application/json');
if ($_GET['cmd'] == 'set') {
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => getenv('SERVICE_HOST'),
'port' => getenv('REDISMASTER_SERVICE_PORT'),
]);
$client->set($_GET['key'], $_GET['value']);
print('{"message": "Updated"}');
} else {
$read_port = getenv('REDISMASTER_SERVICE_PORT');
if (isset($_ENV['REDISSLAVE_SERVICE_PORT'])) {
$read_port = getenv('REDISSLAVE_SERVICE_PORT');
}
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => getenv('SERVICE_HOST'),
'port' => $read_port,
]);
$value = $client->get($_GET['key']);
print('{"data": "' . $value . '"}');
}
} else {
phpinfo();
} ?>
```
To play with the service itself, find the name of a frontend, grab the external IP of that host from the Google Cloud Console, and visit http://&lt;host-ip&gt;:8080, note you may need to open the firewall for port 8080 using the console or the gcloud tool.
<?
set_include_path('.:/usr/share/php:/usr/share/pear:/vendor/predis');
error_reporting(E_ALL);
ini_set('display_errors', 1);
require 'predis/autoload.php';
if (isset($_GET['cmd']) === true) {
header('Content-Type: application/json');
if ($_GET['cmd'] == 'set') {
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => getenv('SERVICE_HOST'),
'port' => getenv('REDISMASTER_SERVICE_PORT'),
]);
$client->set($_GET['key'], $_GET['value']);
print('{"message": "Updated"}');
} else {
$read_port = getenv('REDISMASTER_SERVICE_PORT');
if (isset($_ENV['REDISSLAVE_SERVICE_PORT'])) {
$read_port = getenv('REDISSLAVE_SERVICE_PORT');
}
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => getenv('SERVICE_HOST'),
'port' => $read_port,
]);
$value = $client->get($_GET['key']);
print('{"data": "' . $value . '"}');
}
} else {
phpinfo();
} ?>
{
"id": "redismaster",
"port": 10000,
"labels": {
"name": "redis-master"
}
}
{
"id": "redis-master-2",
"desiredState": {
"manifest": {
"containers": [{
"name": "master",
"image": "dockerfile/redis",
"ports": [{
"containerPort": 6379,
"hostPort": 6379
}]
}]
}
},
"labels": {
"name": "redis-master"
}
}
{
"id": "redisSlaveController",
"desiredState": {
"replicas": 2,
"replicasInSet": {"name": "redisslave"},
"taskTemplate": {
"desiredState": {
"manifest": {
"containers": [{
"image": "brendanburns/redis-slave",
"ports": [{"containerPort": 6379, "hostPort": 6380}]
}]
}
},
"labels": {"name": "redisslave"}
}},
"labels": {"name": "redisslave"}
}
{
"id": "redisslave",
"port": 10001,
"labels": {
"name": "redisslave"
}
}
#!/bin/bash
if [[ "$(grep -c "# then delete this line" $1)" == "1" ]]; then
echo "Unresolved gofmt errors. Aborting commit."
echo "The message of your attempted commit was:"
cat $1
exit 1
fi
exit 0
#!/bin/bash
errors=0
for file in $(git diff --cached --name-only | grep "\.go"); do
diff="$(gofmt -d "${file}")"
if [[ -n "$diff" ]]; then
echo "# *** ERROR: *** File ${file} has not been gofmt'd." >> $1
errors=1
fi
done
if [[ $errors == "1" ]]; then
echo "# To fix these errors, run gofmt -w <file>." >> $1
echo "# If you want to commit in spite of these format errors," >> $1
echo "# then delete this line. Otherwise, your commit will be" >> $1
echo "# aborted." >> $1
fi
/*
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
}
/*
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)
}
}
/*
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))
}
}
/*
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
}
/*
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
}
/*
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))
}
}
/*
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)
}
/*
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.")
}
}
/*
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)
}
/*
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
}
/*
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)
}
/*
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)
}
}
/*
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)
}
/*
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)
}
}
}
}
/*
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)
}
}
/*
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)
}
}
}
/*
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")
}
/*
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))
}
/*
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)
}
}
/*
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
}
/*
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")
}
}
/*
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
}
/*
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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment