| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| { | ||
| "$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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "$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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| { | ||
| "$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 | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| { | ||
| "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" | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "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"} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "items": [ | ||
| { | ||
| "id": "example1", | ||
| "port": 8000, | ||
| "labels": { | ||
| "name": "nginx" | ||
| } | ||
| }, | ||
| { | ||
| "id": "example2", | ||
| "port": 8080, | ||
| "labels": { | ||
| "env": "prod", | ||
| "name": "jetty" | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "id": "example2", | ||
| "port": 8000, | ||
| "labels": { | ||
| "name": "nginx" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| { | ||
| "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" | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "id": "php", | ||
| "desiredState": { | ||
| "manifest": { | ||
| "containers": [{ | ||
| "image": "dockerfile/nginx", | ||
| "ports": [{ | ||
| "containerPort": 80, | ||
| "hostPort": 8080 | ||
| }] | ||
| }] | ||
| } | ||
| }, | ||
| "labels": { | ||
| "name": "foo" | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| #%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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* | ||
| 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()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /* | ||
| 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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /* | ||
| 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 {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /* | ||
| 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") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| 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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /* | ||
| 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 {} | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "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"} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| ## 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://<host-ip>:8080, note you may need to open the firewall for port 8080 using the console or the gcloud tool. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <? | ||
|
|
||
| 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(); | ||
| } ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "id": "redismaster", | ||
| "port": 10000, | ||
| "labels": { | ||
| "name": "redis-master" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "id": "redis-master-2", | ||
| "desiredState": { | ||
| "manifest": { | ||
| "containers": [{ | ||
| "name": "master", | ||
| "image": "dockerfile/redis", | ||
| "ports": [{ | ||
| "containerPort": 6379, | ||
| "hostPort": 6379 | ||
| }] | ||
| }] | ||
| } | ||
| }, | ||
| "labels": { | ||
| "name": "redis-master" | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "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"} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "id": "redisslave", | ||
| "port": 10001, | ||
| "labels": { | ||
| "name": "redisslave" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| #!/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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #!/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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| // Package api includes all types used to communicate between the various | ||
| // parts of the Kubernetes system. | ||
| package api | ||
|
|
||
| // ContainerManifest corresponds to the Container Manifest format, documented at: | ||
| // https://developers.google.com/compute/docs/containers#container_manifest | ||
| // This is used as the representation of Kubernete's workloads. | ||
| type ContainerManifest struct { | ||
| Version string `yaml:"version" json:"version"` | ||
| Volumes []Volume `yaml:"volumes" json:"volumes"` | ||
| Containers []Container `yaml:"containers" json:"containers"` | ||
| Id string `yaml:"id,omitempty" json:"id,omitempty"` | ||
| } | ||
|
|
||
| type Volume struct { | ||
| Name string `yaml:"name" json:"name"` | ||
| } | ||
|
|
||
| type Port struct { | ||
| Name string `yaml:"name,omitempty" json:"name,omitempty"` | ||
| HostPort int `yaml:"hostPort,omitempty" json:"hostPort,omitempty"` | ||
| ContainerPort int `yaml:"containerPort,omitempty" json:"containerPort,omitempty"` | ||
| Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` | ||
| } | ||
|
|
||
| type VolumeMount struct { | ||
| Name string `yaml:"name,omitempty" json:"name,omitempty"` | ||
| ReadOnly bool `yaml:"readOnly,omitempty" json:"readOnly,omitempty"` | ||
| MountPath string `yaml:"mountPath,omitempty" json:"mountPath,omitempty"` | ||
| } | ||
|
|
||
| type EnvVar struct { | ||
| Name string `yaml:"name,omitempty" json:"name,omitempty"` | ||
| Value string `yaml:"value,omitempty" json:"value,omitempty"` | ||
| } | ||
|
|
||
| // Container represents a single container that is expected to be run on the host. | ||
| type Container struct { | ||
| Name string `yaml:"name,omitempty" json:"name,omitempty"` | ||
| Image string `yaml:"image,omitempty" json:"image,omitempty"` | ||
| Command string `yaml:"command,omitempty" json:"command,omitempty"` | ||
| WorkingDir string `yaml:"workingDir,omitempty" json:"workingDir,omitempty"` | ||
| Ports []Port `yaml:"ports,omitempty" json:"ports,omitempty"` | ||
| Env []EnvVar `yaml:"env,omitempty" json:"env,omitempty"` | ||
| Memory int `yaml:"memory,omitempty" json:"memory,omitempty"` | ||
| CPU int `yaml:"cpu,omitempty" json:"cpu,omitempty"` | ||
| VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty" json:"volumeMounts,omitempty"` | ||
| } | ||
|
|
||
| // Event is the representation of an event logged to etcd backends | ||
| type Event struct { | ||
| Event string `json:"event,omitempty"` | ||
| Manifest *ContainerManifest `json:"manifest,omitempty"` | ||
| Container *Container `json:"container,omitempty"` | ||
| Timestamp int64 `json:"timestamp"` | ||
| } | ||
|
|
||
| // The below types are used by kube_client and api_server. | ||
|
|
||
| // JSONBase is shared by all objects sent to, or returned from the client | ||
| type JSONBase struct { | ||
| Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` | ||
| ID string `json:"id,omitempty" yaml:"id,omitempty"` | ||
| CreationTimestamp string `json:"creationTimestamp,omitempty" yaml:"creationTimestamp,omitempty"` | ||
| SelfLink string `json:"selfLink,omitempty" yaml:"selfLink,omitempty"` | ||
| } | ||
|
|
||
| // TaskState is the state of a task, used as either input (desired state) or output (current state) | ||
| type TaskState struct { | ||
| Manifest ContainerManifest `json:"manifest,omitempty" yaml:"manifest,omitempty"` | ||
| Status string `json:"status,omitempty" yaml:"status,omitempty"` | ||
| Host string `json:"host,omitempty" yaml:"host,omitempty"` | ||
| HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` | ||
| Info interface{} `json:"info,omitempty" yaml:"info,omitempty"` | ||
| } | ||
|
|
||
| type TaskList struct { | ||
| JSONBase | ||
| Items []Task `json:"items" yaml:"items,omitempty"` | ||
| } | ||
|
|
||
| // Task is a single task, used as either input (create, update) or as output (list, get) | ||
| type Task struct { | ||
| JSONBase | ||
| Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` | ||
| DesiredState TaskState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` | ||
| CurrentState TaskState `json:"currentState,omitempty" yaml:"currentState,omitempty"` | ||
| } | ||
|
|
||
| // ReplicationControllerState is the state of a replication controller, either input (create, update) or as output (list, get) | ||
| type ReplicationControllerState struct { | ||
| Replicas int `json:"replicas" yaml:"replicas"` | ||
| ReplicasInSet map[string]string `json:"replicasInSet,omitempty" yaml:"replicasInSet,omitempty"` | ||
| TaskTemplate TaskTemplate `json:"taskTemplate,omitempty" yaml:"taskTemplate,omitempty"` | ||
| } | ||
|
|
||
| type ReplicationControllerList struct { | ||
| JSONBase | ||
| Items []ReplicationController `json:"items,omitempty" yaml:"items,omitempty"` | ||
| } | ||
|
|
||
| // ReplicationController represents the configuration of a replication controller | ||
| type ReplicationController struct { | ||
| JSONBase | ||
| DesiredState ReplicationControllerState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` | ||
| Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` | ||
| } | ||
|
|
||
| // TaskTemplate holds the information used for creating tasks | ||
| type TaskTemplate struct { | ||
| DesiredState TaskState `json:"desiredState,omitempty" yaml:"desiredState,omitempty"` | ||
| Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` | ||
| } | ||
|
|
||
| // ServiceList holds a list of services | ||
| type ServiceList struct { | ||
| Items []Service `json:"items" yaml:"items"` | ||
| } | ||
|
|
||
| // Defines a service abstraction by a name (for example, mysql) consisting of local port | ||
| // (for example 3306) that the proxy listens on, and the labels that define the service. | ||
| type Service struct { | ||
| JSONBase | ||
| Port int `json:"port,omitempty" yaml:"port,omitempty"` | ||
| Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` | ||
| } | ||
|
|
||
| // Defines the endpoints that implement the actual service, for example: | ||
| // Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"] | ||
| type Endpoints struct { | ||
| Name string | ||
| Endpoints []string | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| // Package apiserver is ... | ||
| package apiserver | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "log" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
| ) | ||
|
|
||
| // RESTStorage is a generic interface for RESTful storage services | ||
| type RESTStorage interface { | ||
| List(*url.URL) (interface{}, error) | ||
| Get(id string) (interface{}, error) | ||
| Delete(id string) error | ||
| Extract(body string) (interface{}, error) | ||
| Create(interface{}) error | ||
| Update(interface{}) error | ||
| } | ||
|
|
||
| // Status is a return value for calls that don't return other objects | ||
| type Status struct { | ||
| success bool | ||
| } | ||
|
|
||
| // ApiServer is an HTTPHandler that delegates to RESTStorage objects. | ||
| // It handles URLs of the form: | ||
| // ${prefix}/${storage_key}[/${object_name}] | ||
| // Where 'prefix' is an arbitrary string, and 'storage_key' points to a RESTStorage object stored in storage. | ||
| // | ||
| // TODO: consider migrating this to go-restful which is a more full-featured version of the same thing. | ||
| type ApiServer struct { | ||
| prefix string | ||
| storage map[string]RESTStorage | ||
| } | ||
|
|
||
| // New creates a new ApiServer object. | ||
| // 'storage' contains a map of handlers. | ||
| // 'prefix' is the hosting path prefix. | ||
| func New(storage map[string]RESTStorage, prefix string) *ApiServer { | ||
| return &ApiServer{ | ||
| storage: storage, | ||
| prefix: prefix, | ||
| } | ||
| } | ||
|
|
||
| func (server *ApiServer) handleIndex(w http.ResponseWriter) { | ||
| w.WriteHeader(http.StatusOK) | ||
| // TODO: serve this out of a file? | ||
| data := "<html><body>Welcome to Kubernetes</body></html>" | ||
| fmt.Fprint(w, data) | ||
| } | ||
|
|
||
| // HTTP Handler interface | ||
| func (server *ApiServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||
| log.Printf("%s %s", req.Method, req.RequestURI) | ||
| url, err := url.ParseRequestURI(req.RequestURI) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| if url.Path == "/index.html" || url.Path == "/" || url.Path == "" { | ||
| server.handleIndex(w) | ||
| return | ||
| } | ||
| if !strings.HasPrefix(url.Path, server.prefix) { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| requestParts := strings.Split(url.Path[len(server.prefix):], "/")[1:] | ||
| if len(requestParts) < 1 { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| storage := server.storage[requestParts[0]] | ||
| if storage == nil { | ||
| server.notFound(req, w) | ||
| return | ||
| } else { | ||
| server.handleREST(requestParts, url, req, w, storage) | ||
| } | ||
| } | ||
|
|
||
| func (server *ApiServer) notFound(req *http.Request, w http.ResponseWriter) { | ||
| w.WriteHeader(404) | ||
| fmt.Fprintf(w, "Not Found: %#v", req) | ||
| } | ||
|
|
||
| func (server *ApiServer) write(statusCode int, object interface{}, w http.ResponseWriter) { | ||
| w.WriteHeader(statusCode) | ||
| output, err := json.MarshalIndent(object, "", " ") | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| w.Write(output) | ||
| } | ||
|
|
||
| func (server *ApiServer) error(err error, w http.ResponseWriter) { | ||
| w.WriteHeader(500) | ||
| fmt.Fprintf(w, "Internal Error: %#v", err) | ||
| } | ||
|
|
||
| func (server *ApiServer) readBody(req *http.Request) (string, error) { | ||
| defer req.Body.Close() | ||
| body, err := ioutil.ReadAll(req.Body) | ||
| return string(body), err | ||
| } | ||
|
|
||
| func (server *ApiServer) handleREST(parts []string, url *url.URL, req *http.Request, w http.ResponseWriter, storage RESTStorage) { | ||
| switch req.Method { | ||
| case "GET": | ||
| switch len(parts) { | ||
| case 1: | ||
| controllers, err := storage.List(url) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| server.write(200, controllers, w) | ||
| case 2: | ||
| task, err := storage.Get(parts[1]) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| if task == nil { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| server.write(200, task, w) | ||
| default: | ||
| server.notFound(req, w) | ||
| } | ||
| return | ||
| case "POST": | ||
| if len(parts) != 1 { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| body, err := server.readBody(req) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| obj, err := storage.Extract(body) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| storage.Create(obj) | ||
| server.write(200, obj, w) | ||
| return | ||
| case "DELETE": | ||
| if len(parts) != 2 { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| err := storage.Delete(parts[1]) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| server.write(200, Status{success: true}, w) | ||
| return | ||
| case "PUT": | ||
| if len(parts) != 2 { | ||
| server.notFound(req, w) | ||
| return | ||
| } | ||
| body, err := server.readBody(req) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| } | ||
| obj, err := storage.Extract(body) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| err = storage.Update(obj) | ||
| if err != nil { | ||
| server.error(err, w) | ||
| return | ||
| } | ||
| server.write(200, obj, w) | ||
| return | ||
| default: | ||
| server.notFound(req, w) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,282 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
| package apiserver | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "reflect" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove. | ||
| func expectNoError(t *testing.T, err error) { | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| } | ||
|
|
||
| type Simple struct { | ||
| Name string | ||
| } | ||
|
|
||
| type SimpleList struct { | ||
| Items []Simple | ||
| } | ||
|
|
||
| type SimpleRESTStorage struct { | ||
| err error | ||
| list []Simple | ||
| item Simple | ||
| deleted string | ||
| updated Simple | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) List(*url.URL) (interface{}, error) { | ||
| result := SimpleList{ | ||
| Items: storage.list, | ||
| } | ||
| return result, storage.err | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) Get(id string) (interface{}, error) { | ||
| return storage.item, storage.err | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) Delete(id string) error { | ||
| storage.deleted = id | ||
| return storage.err | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) Extract(body string) (interface{}, error) { | ||
| var item Simple | ||
| json.Unmarshal([]byte(body), &item) | ||
| return item, storage.err | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) Create(interface{}) error { | ||
| return storage.err | ||
| } | ||
|
|
||
| func (storage *SimpleRESTStorage) Update(object interface{}) error { | ||
| storage.updated = object.(Simple) | ||
| return storage.err | ||
| } | ||
|
|
||
| func extractBody(response *http.Response, object interface{}) (string, error) { | ||
| defer response.Body.Close() | ||
| body, err := ioutil.ReadAll(response.Body) | ||
| if err != nil { | ||
| return string(body), err | ||
| } | ||
| err = json.Unmarshal(body, object) | ||
| return string(body), err | ||
| } | ||
|
|
||
| func TestSimpleList(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{} | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| resp, err := http.Get(server.URL + "/prefix/version/simple") | ||
| expectNoError(t, err) | ||
|
|
||
| if resp.StatusCode != 200 { | ||
| t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp) | ||
| } | ||
| } | ||
|
|
||
| func TestErrorList(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{ | ||
| err: fmt.Errorf("Test Error"), | ||
| } | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| resp, err := http.Get(server.URL + "/prefix/version/simple") | ||
| expectNoError(t, err) | ||
|
|
||
| if resp.StatusCode != 500 { | ||
| t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp) | ||
| } | ||
| } | ||
|
|
||
| func TestNonEmptyList(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{ | ||
| list: []Simple{ | ||
| Simple{ | ||
| Name: "foo", | ||
| }, | ||
| }, | ||
| } | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| resp, err := http.Get(server.URL + "/prefix/version/simple") | ||
| expectNoError(t, err) | ||
|
|
||
| if resp.StatusCode != 200 { | ||
| t.Errorf("Unexpected status: %d, Expected: %d, %#v", resp.StatusCode, 200, resp) | ||
| } | ||
|
|
||
| var listOut SimpleList | ||
| body, err := extractBody(resp, &listOut) | ||
| if len(listOut.Items) != 1 { | ||
| t.Errorf("Unexpected response: %#v", listOut) | ||
| } | ||
| if listOut.Items[0].Name != simpleStorage.list[0].Name { | ||
| t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) | ||
| } | ||
| } | ||
|
|
||
| func TestGet(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{ | ||
| item: Simple{ | ||
| Name: "foo", | ||
| }, | ||
| } | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| resp, err := http.Get(server.URL + "/prefix/version/simple/id") | ||
| var itemOut Simple | ||
| body, err := extractBody(resp, &itemOut) | ||
| expectNoError(t, err) | ||
| if itemOut.Name != simpleStorage.item.Name { | ||
| t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simpleStorage.item, string(body)) | ||
| } | ||
| } | ||
|
|
||
| func TestDelete(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{} | ||
| ID := "id" | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| client := http.Client{} | ||
| request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) | ||
| _, err = client.Do(request) | ||
| expectNoError(t, err) | ||
| if simpleStorage.deleted != ID { | ||
| t.Errorf("Unexpected delete: %s, expected %s (%s)", simpleStorage.deleted, ID) | ||
| } | ||
| } | ||
|
|
||
| func TestUpdate(t *testing.T) { | ||
| storage := map[string]RESTStorage{} | ||
| simpleStorage := SimpleRESTStorage{} | ||
| ID := "id" | ||
| storage["simple"] = &simpleStorage | ||
| handler := New(storage, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
|
|
||
| item := Simple{ | ||
| Name: "bar", | ||
| } | ||
| body, err := json.Marshal(item) | ||
| expectNoError(t, err) | ||
| client := http.Client{} | ||
| request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) | ||
| _, err = client.Do(request) | ||
| expectNoError(t, err) | ||
| if simpleStorage.updated.Name != item.Name { | ||
| t.Errorf("Unexpected update value %#v, expected %#v.", simpleStorage.updated, item) | ||
| } | ||
| } | ||
|
|
||
| func TestBadPath(t *testing.T) { | ||
| handler := New(map[string]RESTStorage{}, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
| client := http.Client{} | ||
|
|
||
| request, err := http.NewRequest("GET", server.URL+"/foobar", nil) | ||
| expectNoError(t, err) | ||
| response, err := client.Do(request) | ||
| expectNoError(t, err) | ||
| if response.StatusCode != 404 { | ||
| t.Errorf("Unexpected response %#v", response) | ||
| } | ||
| } | ||
|
|
||
| func TestMissingPath(t *testing.T) { | ||
| handler := New(map[string]RESTStorage{}, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
| client := http.Client{} | ||
|
|
||
| request, err := http.NewRequest("GET", server.URL+"/prefix/version", nil) | ||
| expectNoError(t, err) | ||
| response, err := client.Do(request) | ||
| expectNoError(t, err) | ||
| if response.StatusCode != 404 { | ||
| t.Errorf("Unexpected response %#v", response) | ||
| } | ||
| } | ||
|
|
||
| func TestMissingStorage(t *testing.T) { | ||
| handler := New(map[string]RESTStorage{ | ||
| "foo": &SimpleRESTStorage{}, | ||
| }, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
| client := http.Client{} | ||
|
|
||
| request, err := http.NewRequest("GET", server.URL+"/prefix/version/foobar", nil) | ||
| expectNoError(t, err) | ||
| response, err := client.Do(request) | ||
| expectNoError(t, err) | ||
| if response.StatusCode != 404 { | ||
| t.Errorf("Unexpected response %#v", response) | ||
| } | ||
| } | ||
|
|
||
| func TestCreate(t *testing.T) { | ||
| handler := New(map[string]RESTStorage{ | ||
| "foo": &SimpleRESTStorage{}, | ||
| }, "/prefix/version") | ||
| server := httptest.NewServer(handler) | ||
| client := http.Client{} | ||
|
|
||
| simple := Simple{Name: "foo"} | ||
| data, _ := json.Marshal(simple) | ||
| request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) | ||
| expectNoError(t, err) | ||
| response, err := client.Do(request) | ||
| expectNoError(t, err) | ||
| if response.StatusCode != 200 { | ||
| t.Errorf("Unexpected response %#v", response) | ||
| } | ||
|
|
||
| var itemOut Simple | ||
| body, err := extractBody(response, &itemOut) | ||
| expectNoError(t, err) | ||
| if !reflect.DeepEqual(itemOut, simple) { | ||
| t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, simple, string(body)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| // A client for the Kubernetes cluster management API | ||
| // There are three fundamental objects | ||
| // Task - A single running container | ||
| // TaskForce - A set of co-scheduled Task(s) | ||
| // ReplicationController - A manager for replicating TaskForces | ||
| package client | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/tls" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "io/ioutil" | ||
| "log" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| "github.com/GoogleCloudPlatform/kubernetes/pkg/api" | ||
| ) | ||
|
|
||
| // ClientInterface holds the methods for clients of Kubenetes, an interface to allow mock testing | ||
| type ClientInterface interface { | ||
| ListTasks(labelQuery map[string]string) (api.TaskList, error) | ||
| GetTask(name string) (api.Task, error) | ||
| DeleteTask(name string) error | ||
| CreateTask(api.Task) (api.Task, error) | ||
| UpdateTask(api.Task) (api.Task, error) | ||
|
|
||
| GetReplicationController(name string) (api.ReplicationController, error) | ||
| CreateReplicationController(api.ReplicationController) (api.ReplicationController, error) | ||
| UpdateReplicationController(api.ReplicationController) (api.ReplicationController, error) | ||
| DeleteReplicationController(string) error | ||
|
|
||
| GetService(name string) (api.Service, error) | ||
| CreateService(api.Service) (api.Service, error) | ||
| UpdateService(api.Service) (api.Service, error) | ||
| DeleteService(string) error | ||
| } | ||
|
|
||
| // AuthInfo is used to store authorization information | ||
| type AuthInfo struct { | ||
| User string | ||
| Password string | ||
| } | ||
|
|
||
| // Client is the actual implementation of a Kubernetes client. | ||
| // Host is the http://... base for the URL | ||
| type Client struct { | ||
| Host string | ||
| Auth *AuthInfo | ||
| httpClient *http.Client | ||
| } | ||
|
|
||
| // Underlying base implementation of performing a request. | ||
| // method is the HTTP method (e.g. "GET") | ||
| // path is the path on the host to hit | ||
| // requestBody is the body of the request. Can be nil. | ||
| // target the interface to marshal the JSON response into. Can be nil. | ||
| func (client Client) rawRequest(method, path string, requestBody io.Reader, target interface{}) ([]byte, error) { | ||
| request, err := http.NewRequest(method, client.makeURL(path), requestBody) | ||
| if err != nil { | ||
| return []byte{}, err | ||
| } | ||
| if client.Auth != nil { | ||
| request.SetBasicAuth(client.Auth.User, client.Auth.Password) | ||
| } | ||
| tr := &http.Transport{ | ||
| TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||
| } | ||
| var httpClient *http.Client | ||
| if client.httpClient != nil { | ||
| httpClient = client.httpClient | ||
| } else { | ||
| httpClient = &http.Client{Transport: tr} | ||
| } | ||
| response, err := httpClient.Do(request) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if response.StatusCode != 200 { | ||
| return nil, fmt.Errorf("request [%s %s] failed (%d) %s", method, client.makeURL(path), response.StatusCode, response.Status) | ||
| } | ||
| defer response.Body.Close() | ||
| body, err := ioutil.ReadAll(response.Body) | ||
| if err != nil { | ||
| return body, err | ||
| } | ||
| if target != nil { | ||
| err = json.Unmarshal(body, target) | ||
| } | ||
| if err != nil { | ||
| log.Printf("Failed to parse: %s\n", string(body)) | ||
| // FIXME: no need to return err here? | ||
| } | ||
| return body, err | ||
| } | ||
|
|
||
| func (client Client) makeURL(path string) string { | ||
| return client.Host + "/api/v1beta1/" + path | ||
| } | ||
|
|
||
| func EncodeLabelQuery(labelQuery map[string]string) string { | ||
| query := make([]string, 0, len(labelQuery)) | ||
| for key, value := range labelQuery { | ||
| query = append(query, key+"="+value) | ||
| } | ||
| return url.QueryEscape(strings.Join(query, ",")) | ||
| } | ||
|
|
||
| func DecodeLabelQuery(labelQuery string) map[string]string { | ||
| result := map[string]string{} | ||
| if len(labelQuery) == 0 { | ||
| return result | ||
| } | ||
| parts := strings.Split(labelQuery, ",") | ||
| for _, part := range parts { | ||
| pieces := strings.Split(part, "=") | ||
| if len(pieces) == 2 { | ||
| result[pieces[0]] = pieces[1] | ||
| } else { | ||
| log.Printf("Invalid label query: %s", labelQuery) | ||
| } | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| // ListTasks takes a label query, and returns the list of tasks that match that query | ||
| func (client Client) ListTasks(labelQuery map[string]string) (api.TaskList, error) { | ||
| path := "tasks" | ||
| if labelQuery != nil && len(labelQuery) > 0 { | ||
| path += "?labels=" + EncodeLabelQuery(labelQuery) | ||
| } | ||
| var result api.TaskList | ||
| _, err := client.rawRequest("GET", path, nil, &result) | ||
| return result, err | ||
| } | ||
|
|
||
| // GetTask takes the name of the task, and returns the corresponding Task object, and an error if it occurs | ||
| func (client Client) GetTask(name string) (api.Task, error) { | ||
| var result api.Task | ||
| _, err := client.rawRequest("GET", "tasks/"+name, nil, &result) | ||
| return result, err | ||
| } | ||
|
|
||
| // DeleteTask takes the name of the task, and returns an error if one occurs | ||
| func (client Client) DeleteTask(name string) error { | ||
| _, err := client.rawRequest("DELETE", "tasks/"+name, nil, nil) | ||
| return err | ||
| } | ||
|
|
||
| // CreateTask takes the representation of a task. Returns the server's representation of the task, and an error, if it occurs | ||
| func (client Client) CreateTask(task api.Task) (api.Task, error) { | ||
| var result api.Task | ||
| body, err := json.Marshal(task) | ||
| if err == nil { | ||
| _, err = client.rawRequest("POST", "tasks", bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| // UpdateTask takes the representation of a task to update. Returns the server's representation of the task, and an error, if it occurs | ||
| func (client Client) UpdateTask(task api.Task) (api.Task, error) { | ||
| var result api.Task | ||
| body, err := json.Marshal(task) | ||
| if err == nil { | ||
| _, err = client.rawRequest("PUT", "tasks/"+task.ID, bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| // GetReplicationController returns information about a particular replication controller | ||
| func (client Client) GetReplicationController(name string) (api.ReplicationController, error) { | ||
| var result api.ReplicationController | ||
| _, err := client.rawRequest("GET", "replicationControllers/"+name, nil, &result) | ||
| return result, err | ||
| } | ||
|
|
||
| // CreateReplicationController creates a new replication controller | ||
| func (client Client) CreateReplicationController(controller api.ReplicationController) (api.ReplicationController, error) { | ||
| var result api.ReplicationController | ||
| body, err := json.Marshal(controller) | ||
| if err == nil { | ||
| _, err = client.rawRequest("POST", "replicationControllers", bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| // UpdateReplicationController updates an existing replication controller | ||
| func (client Client) UpdateReplicationController(controller api.ReplicationController) (api.ReplicationController, error) { | ||
| var result api.ReplicationController | ||
| body, err := json.Marshal(controller) | ||
| if err == nil { | ||
| _, err = client.rawRequest("PUT", "replicationControllers/"+controller.ID, bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| func (client Client) DeleteReplicationController(name string) error { | ||
| _, err := client.rawRequest("DELETE", "replicationControllers/"+name, nil, nil) | ||
| return err | ||
| } | ||
|
|
||
| // GetReplicationController returns information about a particular replication controller | ||
| func (client Client) GetService(name string) (api.Service, error) { | ||
| var result api.Service | ||
| _, err := client.rawRequest("GET", "services/"+name, nil, &result) | ||
| return result, err | ||
| } | ||
|
|
||
| // CreateReplicationController creates a new replication controller | ||
| func (client Client) CreateService(svc api.Service) (api.Service, error) { | ||
| var result api.Service | ||
| body, err := json.Marshal(svc) | ||
| if err == nil { | ||
| _, err = client.rawRequest("POST", "services", bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| // UpdateReplicationController updates an existing replication controller | ||
| func (client Client) UpdateService(svc api.Service) (api.Service, error) { | ||
| var result api.Service | ||
| body, err := json.Marshal(svc) | ||
| if err == nil { | ||
| _, err = client.rawRequest("PUT", "services/"+svc.ID, bytes.NewBuffer(body), &result) | ||
| } | ||
| return result, err | ||
| } | ||
|
|
||
| func (client Client) DeleteService(name string) error { | ||
| _, err := client.rawRequest("DELETE", "services/"+name, nil, nil) | ||
| return err | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,391 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
| package client | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "reflect" | ||
| "testing" | ||
|
|
||
| "github.com/GoogleCloudPlatform/kubernetes/pkg/api" | ||
| "github.com/GoogleCloudPlatform/kubernetes/pkg/util" | ||
| ) | ||
|
|
||
| // TODO: This doesn't reduce typing enough to make it worth the less readable errors. Remove. | ||
| func expectNoError(t *testing.T, err error) { | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| } | ||
|
|
||
| // TODO: Move this to a common place, it's needed in multiple tests. | ||
| var apiPath = "/api/v1beta1" | ||
|
|
||
| func makeUrl(suffix string) string { | ||
| return apiPath + suffix | ||
| } | ||
|
|
||
| func TestListEmptyTasks(t *testing.T) { | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: `{ "items": []}`, | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| taskList, err := client.ListTasks(nil) | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error in listing tasks: %#v", err) | ||
| } | ||
| if len(taskList.Items) != 0 { | ||
| t.Errorf("Unexpected items in task list: %#v", taskList) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestListTasks(t *testing.T) { | ||
| expectedTaskList := api.TaskList{ | ||
| Items: []api.Task{ | ||
| api.Task{ | ||
| CurrentState: api.TaskState{ | ||
| Status: "Foobar", | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedTaskList) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedTaskList, err := client.ListTasks(nil) | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error in listing tasks: %#v", err) | ||
| } | ||
| if !reflect.DeepEqual(expectedTaskList, receivedTaskList) { | ||
| t.Errorf("Unexpected task list: %#v\nvs.\n%#v", receivedTaskList, expectedTaskList) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestListTasksLabels(t *testing.T) { | ||
| expectedTaskList := api.TaskList{ | ||
| Items: []api.Task{ | ||
| api.Task{ | ||
| CurrentState: api.TaskState{ | ||
| Status: "Foobar", | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedTaskList) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| query := map[string]string{"foo": "bar", "name": "baz"} | ||
| receivedTaskList, err := client.ListTasks(query) | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "GET", nil) | ||
| queryString := fakeHandler.RequestReceived.URL.Query().Get("labels") | ||
| queryString, _ = url.QueryUnescape(queryString) | ||
| // TODO(bburns) : This assumes some ordering in serialization that might not always | ||
| // be true, parse it into a map. | ||
| if queryString != "foo=bar,name=baz" { | ||
| t.Errorf("Unexpected label query: %s", queryString) | ||
| } | ||
| if err != nil { | ||
| t.Errorf("Unexpected error in listing tasks: %#v", err) | ||
| } | ||
| if !reflect.DeepEqual(expectedTaskList, receivedTaskList) { | ||
| t.Errorf("Unexpected task list: %#v\nvs.\n%#v", receivedTaskList, expectedTaskList) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestGetTask(t *testing.T) { | ||
| expectedTask := api.Task{ | ||
| CurrentState: api.TaskState{ | ||
| Status: "Foobar", | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedTask) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedTask, err := client.GetTask("foo") | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "GET", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| if !reflect.DeepEqual(expectedTask, receivedTask) { | ||
| t.Errorf("Received task: %#v\n doesn't match expected task: %#v", receivedTask, expectedTask) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestDeleteTask(t *testing.T) { | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: `{"success": true}`, | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| err := client.DeleteTask("foo") | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "DELETE", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestCreateTask(t *testing.T) { | ||
| requestTask := api.Task{ | ||
| CurrentState: api.TaskState{ | ||
| Status: "Foobar", | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(requestTask) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedTask, err := client.CreateTask(requestTask) | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks"), "POST", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| if !reflect.DeepEqual(requestTask, receivedTask) { | ||
| t.Errorf("Received task: %#v\n doesn't match expected task: %#v", receivedTask, requestTask) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestUpdateTask(t *testing.T) { | ||
| requestTask := api.Task{ | ||
| JSONBase: api.JSONBase{ID: "foo"}, | ||
| CurrentState: api.TaskState{ | ||
| Status: "Foobar", | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(requestTask) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedTask, err := client.UpdateTask(requestTask) | ||
| fakeHandler.ValidateRequest(t, makeUrl("/tasks/foo"), "PUT", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| expectEqual(t, requestTask, receivedTask) | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func expectEqual(t *testing.T, expected, observed interface{}) { | ||
| if !reflect.DeepEqual(expected, observed) { | ||
| t.Errorf("Unexpected inequality. Expected: %#v Observed: %#v", expected, observed) | ||
| } | ||
| } | ||
|
|
||
| func TestEncodeDecodeLabelQuery(t *testing.T) { | ||
| queryIn := map[string]string{ | ||
| "foo": "bar", | ||
| "baz": "blah", | ||
| } | ||
| queryString, _ := url.QueryUnescape(EncodeLabelQuery(queryIn)) | ||
| queryOut := DecodeLabelQuery(queryString) | ||
| expectEqual(t, queryIn, queryOut) | ||
| } | ||
|
|
||
| func TestDecodeEmpty(t *testing.T) { | ||
| query := DecodeLabelQuery("") | ||
| if len(query) != 0 { | ||
| t.Errorf("Unexpected query: %#v", query) | ||
| } | ||
| } | ||
|
|
||
| func TestDecodeBad(t *testing.T) { | ||
| query := DecodeLabelQuery("foo") | ||
| if len(query) != 0 { | ||
| t.Errorf("Unexpected query: %#v", query) | ||
| } | ||
| } | ||
|
|
||
| func TestGetController(t *testing.T) { | ||
| expectedController := api.ReplicationController{ | ||
| JSONBase: api.JSONBase{ | ||
| ID: "foo", | ||
| }, | ||
| DesiredState: api.ReplicationControllerState{ | ||
| Replicas: 2, | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedController) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedController, err := client.GetReplicationController("foo") | ||
| expectNoError(t, err) | ||
| if !reflect.DeepEqual(expectedController, receivedController) { | ||
| t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController) | ||
| } | ||
| fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "GET", nil) | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestUpdateController(t *testing.T) { | ||
| expectedController := api.ReplicationController{ | ||
| JSONBase: api.JSONBase{ | ||
| ID: "foo", | ||
| }, | ||
| DesiredState: api.ReplicationControllerState{ | ||
| Replicas: 2, | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedController) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedController, err := client.UpdateReplicationController(api.ReplicationController{ | ||
| JSONBase: api.JSONBase{ | ||
| ID: "foo", | ||
| }, | ||
| }) | ||
| expectNoError(t, err) | ||
| if !reflect.DeepEqual(expectedController, receivedController) { | ||
| t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController) | ||
| } | ||
| fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "PUT", nil) | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestDeleteController(t *testing.T) { | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: `{"success": true}`, | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| err := client.DeleteReplicationController("foo") | ||
| fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers/foo"), "DELETE", nil) | ||
| if err != nil { | ||
| t.Errorf("Unexpected error: %#v", err) | ||
| } | ||
| testServer.Close() | ||
| } | ||
|
|
||
| func TestCreateController(t *testing.T) { | ||
| expectedController := api.ReplicationController{ | ||
| JSONBase: api.JSONBase{ | ||
| ID: "foo", | ||
| }, | ||
| DesiredState: api.ReplicationControllerState{ | ||
| Replicas: 2, | ||
| }, | ||
| Labels: map[string]string{ | ||
| "foo": "bar", | ||
| "name": "baz", | ||
| }, | ||
| } | ||
| body, _ := json.Marshal(expectedController) | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: string(body), | ||
| } | ||
| testServer := httptest.NewTLSServer(&fakeHandler) | ||
| client := Client{ | ||
| Host: testServer.URL, | ||
| } | ||
| receivedController, err := client.CreateReplicationController(api.ReplicationController{ | ||
| JSONBase: api.JSONBase{ | ||
| ID: "foo", | ||
| }, | ||
| }) | ||
| expectNoError(t, err) | ||
| if !reflect.DeepEqual(expectedController, receivedController) { | ||
| t.Errorf("Unexpected controller, expected: %#v, received %#v", expectedController, receivedController) | ||
| } | ||
| fakeHandler.ValidateRequest(t, makeUrl("/replicationControllers"), "POST", nil) | ||
| testServer.Close() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
| package client | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net/http" | ||
| ) | ||
|
|
||
| type ContainerInfo interface { | ||
| GetContainerInfo(host, name string) (interface{}, error) | ||
| } | ||
|
|
||
| type HTTPContainerInfo struct { | ||
| Client *http.Client | ||
| Port uint | ||
| } | ||
|
|
||
| func (c *HTTPContainerInfo) GetContainerInfo(host, name string) (interface{}, error) { | ||
| request, err := http.NewRequest("GET", fmt.Sprintf("http://%s:%d/containerInfo?container=%s", host, c.Port, name), nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| response, err := c.Client.Do(request) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer response.Body.Close() | ||
| body, err := ioutil.ReadAll(response.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var data interface{} | ||
| err = json.Unmarshal(body, &data) | ||
| return data, err | ||
| } | ||
|
|
||
| // Useful for testing. | ||
| type FakeContainerInfo struct { | ||
| data interface{} | ||
| err error | ||
| } | ||
|
|
||
| func (c *FakeContainerInfo) GetContainerInfo(host, name string) (interface{}, error) { | ||
| return c.data, c.err | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| Copyright 2014 Google Inc. All rights reserved. | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
| package client | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "net/url" | ||
| "strconv" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/GoogleCloudPlatform/kubernetes/pkg/util" | ||
| ) | ||
|
|
||
| func TestHTTPContainerInfo(t *testing.T) { | ||
| body := `{"items":[]}` | ||
| fakeHandler := util.FakeHandler{ | ||
| StatusCode: 200, | ||
| ResponseBody: body, | ||
| } | ||
| testServer := httptest.NewServer(&fakeHandler) | ||
|
|
||
| hostUrl, err := url.Parse(testServer.URL) | ||
| expectNoError(t, err) | ||
| parts := strings.Split(hostUrl.Host, ":") | ||
|
|
||
| port, err := strconv.Atoi(parts[1]) | ||
| expectNoError(t, err) | ||
| containerInfo := &HTTPContainerInfo{ | ||
| Client: http.DefaultClient, | ||
| Port: uint(port), | ||
| } | ||
| data, err := containerInfo.GetContainerInfo(parts[0], "foo") | ||
| expectNoError(t, err) | ||
| dataString, _ := json.Marshal(data) | ||
| if string(dataString) != body { | ||
| t.Errorf("Unexpected response. Expected: %s, received %s", body, string(dataString)) | ||
| } | ||
| } |