diff --git a/README.md b/README.md index c4b606deb23..4dee02ea673 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,16 @@ cd kubernetes cluster/kube-down.sh ``` +## Running locally +In a separate tab of your terminal, run: + +``` +cd kubernetes +hack/local-up.sh +``` + +This will build and start a lightweight local cluster, consisting of a master and a single minion. Type Control-C to shut it down. While it's running, you can use `hack/localcfg.sh` in place of `cluster/cloudcfg.sh` to talk to it. + ## Where to go next? [Detailed example application](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/examples/guestbook/guestbook.md) diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 40ae6cf2df3..392bcbb6634 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -13,6 +13,7 @@ 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 @@ -53,7 +54,7 @@ func main() { } var ( - podRegistry registry.PodRegistry + podRegistry registry.PodRegistry controllerRegistry registry.ControllerRegistry serviceRegistry registry.ServiceRegistry ) @@ -77,7 +78,7 @@ func main() { random := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) storage := map[string]apiserver.RESTStorage{ - "pods": registry.MakePodRegistryStorage(podRegistry, containerInfo, registry.MakeFirstFitScheduler(machineList, podRegistry, random)), + "pods": registry.MakePodRegistryStorage(podRegistry, containerInfo, registry.MakeFirstFitScheduler(machineList, podRegistry, random)), "replicationControllers": registry.MakeControllerRegistryStorage(controllerRegistry), "services": registry.MakeServiceRegistryStorage(serviceRegistry), } diff --git a/cmd/cloudcfg/cloudcfg.go b/cmd/cloudcfg/cloudcfg.go index 5f87ba1f769..628f2f83214 100644 --- a/cmd/cloudcfg/cloudcfg.go +++ b/cmd/cloudcfg/cloudcfg.go @@ -20,7 +20,9 @@ import ( "fmt" "log" "net/http" + "net/url" "os" + "path" "strconv" "time" @@ -39,7 +41,7 @@ var ( updatePeriod = flag.Duration("u", 60*time.Second, "Update interarrival period") portSpec = flag.String("p", "", "The port spec, comma-separated list of :,...") servicePort = flag.Int("s", -1, "If positive, create and run a corresponding service on this port, only used with 'run'") - authConfig = flag.String("auth", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user") + authConfig = flag.String("auth", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if doing https.") json = flag.Bool("json", false, "If true, print raw JSON for responses") yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses") ) @@ -61,9 +63,16 @@ func main() { usage() } method := flag.Arg(0) - url := *httpServer + "/api/v1beta1" + flag.Arg(1) + secure := true + parsedUrl, err := url.Parse(*httpServer) + if err != nil { + log.Fatalf("Unable to parse %v as a URL\n", err) + } + if parsedUrl.Scheme != "" && parsedUrl.Scheme != "https" { + secure = false + } + url := *httpServer + path.Join("/api/v1beta1", flag.Arg(1)) var request *http.Request - var err error var printer cloudcfg.ResourcePrinter if *json { @@ -74,9 +83,12 @@ func main() { printer = &cloudcfg.HumanReadablePrinter{} } - auth, err := cloudcfg.LoadAuthInfo(*authConfig) - if err != nil { - log.Fatalf("Error loading auth: %#v", err) + var auth *kube_client.AuthInfo + if secure { + auth, err = cloudcfg.LoadAuthInfo(*authConfig) + if err != nil { + log.Fatalf("Error loading auth: %#v", err) + } } switch method { @@ -94,7 +106,7 @@ func main() { case "rollingupdate": client := &kube_client.Client{ Host: *httpServer, - Auth: &auth, + Auth: auth, } cloudcfg.Update(flag.Arg(1), client, *updatePeriod) case "run": @@ -108,19 +120,19 @@ func main() { 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) + err = cloudcfg.RunController(image, name, replicas, kube_client.Client{Host: *httpServer, Auth: auth}, *portSpec, *servicePort) if err != nil { log.Fatalf("Error: %#v", err) } return case "stop": - err = cloudcfg.StopController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: &auth}) + err = cloudcfg.StopController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: auth}) if err != nil { log.Fatalf("Error: %#v", err) } return case "rm": - err = cloudcfg.DeleteController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: &auth}) + err = cloudcfg.DeleteController(flag.Arg(1), kube_client.Client{Host: *httpServer, Auth: auth}) if err != nil { log.Fatalf("Error: %#v", err) } @@ -131,8 +143,7 @@ func main() { if err != nil { log.Fatalf("Error: %#v", err) } - var body string - body, err = cloudcfg.DoRequest(request, auth.User, auth.Password) + body, err := cloudcfg.DoRequest(request, auth) if err != nil { log.Fatalf("Error: %#v", err) } diff --git a/cmd/controller-manager/controller-manager.go b/cmd/controller-manager/controller-manager.go index d832873edbf..f09d7192ca3 100644 --- a/cmd/controller-manager/controller-manager.go +++ b/cmd/controller-manager/controller-manager.go @@ -13,6 +13,7 @@ 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 // pods to achieve the desired state. It listens for new controllers in etcd, and it sends requests to the // master to create/delete pods. diff --git a/cmd/kubelet/kubelet.go b/cmd/kubelet/kubelet.go index 688e7ce1178..acfa8cbb2b7 100644 --- a/cmd/kubelet/kubelet.go +++ b/cmd/kubelet/kubelet.go @@ -24,6 +24,7 @@ import ( "log" "math/rand" "os" + "os/exec" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" @@ -57,7 +58,13 @@ func main() { log.Fatal("Couldn't connnect to docker.") } + hostname, err := exec.Command("hostname", "-f").Output() + if err != nil { + log.Fatalf("Couldn't determine hostname: %v", err) + } + my_kubelet := kubelet.Kubelet{ + Hostname: string(hostname), DockerClient: dockerClient, FileCheckFrequency: *fileCheckFrequency, SyncFrequency: *syncFrequency, diff --git a/cmd/localkube/localkube.go b/cmd/localkube/localkube.go new file mode 100644 index 00000000000..b9c3a79ba66 --- /dev/null +++ b/cmd/localkube/localkube.go @@ -0,0 +1,138 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// An all-in-one binary for standing up a fake Kubernetes cluster on your +// local machine. +// Assumes that there is a pre-existing etcd server running on localhost. +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/coreos/go-etcd/etcd" + "github.com/fsouza/go-dockerclient" +) + +// kubelet flags +var ( + file = flag.String("config", "", "Path to the config file") + syncFrequency = flag.Duration("sync_frequency", 10*time.Second, "Max period between synchronizing running containers and config") + fileCheckFrequency = flag.Duration("file_check_frequency", 20*time.Second, "Duration between checking file for new data") + httpCheckFrequency = flag.Duration("http_check_frequency", 20*time.Second, "Duration between checking http for new data") + manifest_url = flag.String("manifest_url", "", "URL for accessing the container manifest") + kubelet_address = flag.String("kubelet_address", "127.0.0.1", "The address for the kubelet info server to serve on") + kubelet_port = flag.Uint("kubelet_port", 10250, "The port for the kubelete info server to serve on") +) + +// master flags +var ( + master_port = flag.Uint("master_port", 8080, "The port for the master to listen on. Default 8080.") + master_address = flag.String("master_address", "127.0.0.1", "The address for the master 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'") +) + +// flags that affect both +var ( + etcd_server = flag.String("etcd_server", "http://localhost:4001", "Url of local etcd server") +) + +// Starts kubelet services. Never returns. +func fake_kubelet() { + 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{ + Hostname: *kubelet_address, + DockerClient: dockerClient, + FileCheckFrequency: *fileCheckFrequency, + SyncFrequency: *syncFrequency, + HTTPCheckFrequency: *httpCheckFrequency, + } + my_kubelet.RunKubelet(*file, *manifest_url, *etcd_server, *kubelet_address, *kubelet_port) +} + +// Starts api services (the master). Never returns. +func api_server() { + machineList := util.StringList{*kubelet_address} + + etcdClient := etcd.NewClient([]string{*etcd_server}) + podRegistry := registry.MakeEtcdRegistry(etcdClient, machineList) + controllerRegistry := registry.MakeEtcdRegistry(etcdClient, machineList) + serviceRegistry := registry.MakeEtcdRegistry(etcdClient, machineList) + + containerInfo := &client.HTTPContainerInfo{ + Client: http.DefaultClient, + Port: *kubelet_port, + } + storage := map[string]apiserver.RESTStorage{ + "pods": registry.MakePodRegistryStorage(podRegistry, containerInfo, registry.MakeFirstFitScheduler(machineList, podRegistry)), + "replicationControllers": registry.MakeControllerRegistryStorage(controllerRegistry), + "services": registry.MakeServiceRegistryStorage(serviceRegistry), + } + + endpoints := registry.MakeEndpointController(serviceRegistry, podRegistry) + go util.Forever(func() { endpoints.SyncServiceEndpoints() }, time.Second*10) + + s := &http.Server{ + Addr: fmt.Sprintf("%s:%d", *master_address, *master_port), + Handler: apiserver.New(storage, *apiPrefix), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + log.Fatal(s.ListenAndServe()) +} + +// Starts up a controller manager. Never returns. +func controller_manager() { + controllerManager := registry.MakeReplicationManager(etcd.NewClient([]string{*etcd_server}), + client.Client{ + Host: fmt.Sprintf("http://%s:%d", *master_address, *master_port), + }) + + go util.Forever(func() { controllerManager.Synchronize() }, 20*time.Second) + go util.Forever(func() { controllerManager.WatchControllers() }, 20*time.Second) + select {} +} + +func main() { + flag.Parse() + + // Set up logger for etcd client + etcd.SetLogger(log.New(os.Stderr, "etcd ", log.LstdFlags)) + + go api_server() + go fake_kubelet() + go controller_manager() + + log.Printf("All components started.\nMaster running at: http://%s:%d\nKubelet running at: http://%s:%d\n", + *master_address, *master_port, + *kubelet_address, *kubelet_port) + select {} +} diff --git a/hack/build-go.sh b/hack/build-go.sh index 1493bc5a2bc..7a630597572 100755 --- a/hack/build-go.sh +++ b/hack/build-go.sh @@ -22,7 +22,7 @@ source $(dirname $0)/config-go.sh cd "${KUBE_TARGET}" -BINARIES="proxy integration apiserver controller-manager kubelet cloudcfg" +BINARIES="proxy integration apiserver controller-manager kubelet cloudcfg localkube" for b in $BINARIES; do echo "+++ Building ${b}" diff --git a/hack/local-up.sh b/hack/local-up.sh new file mode 100755 index 00000000000..1308420ee7e --- /dev/null +++ b/hack/local-up.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# 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. + +# This command builds and runs a local kubernetes cluster. + +if [ "$(which etcd)" == "" ]; then + echo "etcd must be in your PATH" + exit 1 +fi + +# Stop right away if the build fails +set -e + +# Only build what we need +( + source $(dirname $0)/config-go.sh + cd "${KUBE_TARGET}" + BINARIES="cloudcfg localkube" + for b in $BINARIES; do + echo "+++ Building ${b}" + go build "${KUBE_GO_PACKAGE}"/cmd/${b} + done +) + +echo "Starting etcd" + +ETCD_DIR=$(mktemp -d -t kube-integration.XXXXXX) +trap "rm -rf ${ETCD_DIR}" EXIT + +etcd -name test -data-dir ${ETCD_DIR} > /tmp/etcd.log & +ETCD_PID=$! + +sleep 5 + +echo "Running localkube as root (so it can talk to docker's unix socket)" +sudo $(dirname $0)/../output/go/localkube + +kill $ETCD_PID diff --git a/hack/localcfg.sh b/hack/localcfg.sh new file mode 100755 index 00000000000..620106fd1b9 --- /dev/null +++ b/hack/localcfg.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# 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. + +# This file is exactly like cloudcfg.sh, but it talks to a local master +# (which you're assumed to be running with localkube.sh). + +CLOUDCFG=$(dirname $0)/../output/go/cloudcfg +if [ ! -x $CLOUDCFG ]; then + echo "Could not find cloudcfg binary. Run hack/build-go.sh to build it." + exit 1 +fi + +# 8080 is the default port for the master +$CLOUDCFG -h http://localhost:8080 $@ diff --git a/pkg/cloudcfg/cloudcfg.go b/pkg/cloudcfg/cloudcfg.go index a65612476cd..7cdedc86931 100644 --- a/pkg/cloudcfg/cloudcfg.go +++ b/pkg/cloudcfg/cloudcfg.go @@ -42,25 +42,28 @@ func promptForString(field string) string { return result } -// Parse an AuthInfo object from a file path -func LoadAuthInfo(path string) (client.AuthInfo, error) { +// Parse an AuthInfo object from a file path. Prompt user and create file if it doesn't exist. +func LoadAuthInfo(path string) (*client.AuthInfo, error) { var auth client.AuthInfo if _, err := os.Stat(path); os.IsNotExist(err) { auth.User = promptForString("Username") auth.Password = promptForString("Password") data, err := json.Marshal(auth) if err != nil { - return auth, err + return &auth, err } err = ioutil.WriteFile(path, data, 0600) - return auth, err + return &auth, err } data, err := ioutil.ReadFile(path) if err != nil { - return auth, err + return nil, err } err = json.Unmarshal(data, &auth) - return auth, err + if err != nil { + return nil, err + } + return &auth, err } // Perform a rolling update of a collection of pods. @@ -99,23 +102,22 @@ func RequestWithBody(configFile, url, method string) (*http.Request, error) { if err != nil { return nil, err } - return RequestWithBodyData(data, url, method) + return requestWithBodyData(data, url, method) } -// RequestWithBodyData is a helper method that creates an HTTP request with the specified url, method +// requestWithBodyData is a helper method that creates an HTTP request with the specified url, method // and body data -// FIXME: need to be public API? -func RequestWithBodyData(data []byte, url, method string) (*http.Request, error) { +func requestWithBodyData(data []byte, url, method string) (*http.Request, error) { request, err := http.NewRequest(method, url, bytes.NewBuffer(data)) request.ContentLength = int64(len(data)) return request, err } -// Execute a request, adds authentication, and HTTPS cert ignoring. -// TODO: Make this stuff optional -// FIXME: need to be public API? -func DoRequest(request *http.Request, user, password string) (string, error) { - request.SetBasicAuth(user, password) +// Execute a request, adds authentication (if auth != nil), and HTTPS cert ignoring. +func DoRequest(request *http.Request, auth *client.AuthInfo) (string, error) { + if auth != nil { + request.SetBasicAuth(auth.User, auth.Password) + } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index de375a3f2b0..9f88df109d3 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -60,6 +60,7 @@ type DockerInterface interface { // The main kubelet implementation type Kubelet struct { + Hostname string Client registry.EtcdClient DockerClient DockerInterface FileCheckFrequency time.Duration @@ -427,15 +428,10 @@ func (kl *Kubelet) getKubeletStateFromEtcd(key string, changeChannel chan<- []ap // The channel to send new configurations across // This function loops forever and is intended to be run in a go routine. func (kl *Kubelet) SyncAndSetupEtcdWatch(changeChannel chan<- []api.ContainerManifest) { - hostname, err := exec.Command("hostname", "-f").Output() - if err != nil { - log.Printf("Couldn't determine hostname : %v", err) - return - } - key := "/registry/hosts/" + strings.TrimSpace(string(hostname)) + key := "/registry/hosts/" + strings.TrimSpace(kl.Hostname) // First fetch the initial configuration (watch only gives changes...) for { - err = kl.getKubeletStateFromEtcd(key, changeChannel) + err := kl.getKubeletStateFromEtcd(key, changeChannel) if err == nil { // We got a successful response, etcd is up, set up the watch. break