From e081bdfdba62a16305fde77ab0cd57cbf9ba3dd1 Mon Sep 17 00:00:00 2001 From: Dan Gillespie Date: Sun, 20 Mar 2016 11:19:16 -0700 Subject: [PATCH 1/2] added localkube launcher --- Makefile | 3 +- cli/cluster.go | 236 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 cli/cluster.go diff --git a/Makefile b/Makefile index 5a55ce4..06fed8b 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,7 @@ lint: .golint-install .PHONY: checkgofmt checkgofmt: # get all go files and run go fmt on them - $(GOFILES) | xargs $(GOFMT) -l - files=$$($(GOFILES) | xargs $(GOFMT) -l); echo "test $$files"; if [[ -n "$$files" ]]; then \ + files=$$($(GOFILES) | xargs $(GOFMT) -l); if [[ -n "$$files" ]]; then \ echo "Error: '$(GOFMT)' needs to be run on:"; \ echo "$${files}"; \ exit 1; \ diff --git a/cli/cluster.go b/cli/cluster.go new file mode 100644 index 0000000..1418d98 --- /dev/null +++ b/cli/cluster.go @@ -0,0 +1,236 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/codegangsta/cli" + docker "github.com/fsouza/go-dockerclient" + "github.com/mitchellh/go-homedir" + kubectlapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" + kubectlcfg "k8s.io/kubernetes/pkg/kubectl/cmd/config" +) + +const ( + LocalkubeContainerName = "/localkube" + LocalkubeImageName = "redspreadapps/localkube" + LocalkubeDefaultTag = "latest" + + DefaultHostDataDir = "~/.localkube/data" + ContainerDataDir = "/var/localkube/data" + KubectlName = "localkube" +) + +// Cluster manages the localkube Kubernetes development environment. +func (s SpreadCli) Cluster() *cli.Command { + return &cli.Command{ + Name: "cluster", + Usage: "spread cluster [-a] [-t ] [ClusterDataDirectory]", + Description: "Manages localkube Kubernetes development environment", + ArgsUsage: "-a will attach to the process and print logs to stdout, -t specfies localkube tag to use, default is latest.", + Action: func(c *cli.Context) { + action := strings.ToLower(c.Args().First()) + switch { + case "start" == action: + s.startLocalkube(c) + case "stop" == action: + s.stopLocalkube(c) + default: + s.printf("Invalid option `%s`, must choose start or stop", action) + } + }, + } +} + +func (s SpreadCli) startLocalkube(c *cli.Context) { + client := s.dockerOrErr() + + dataDir := c.Args().Get(1) + if len(dataDir) == 0 { + var err error + dataDir, err = homedir.Expand(DefaultHostDataDir) + if err != nil { + s.fatalf("Unable to expand home directory: %v", err) + } + } + + tag := c.String("t") + if len(tag) == 0 { + tag = LocalkubeDefaultTag + } + + ctrOpts := localkube(c.Bool("a"), dataDir, tag) + ctr, err := client.CreateContainer(ctrOpts) + if err != nil { + if err.Error() == "no such image" { + s.printf("Pulling localkube image...") + err = client.PullImage(docker.PullImageOptions{ + Repository: LocalkubeImageName, + Tag: tag, + }, docker.AuthConfiguration{}) + if err != nil { + s.fatalf("Failed to pull localkube image: %v", err) + } + + s.startLocalkube(c) + return + } else if err.Error() == "container already exists" { + // replace container if already exists + err = client.RemoveContainer(docker.RemoveContainerOptions{ + ID: LocalkubeContainerName, + }) + if err != nil { + s.fatalf("Failed to start container: %v", err) + } + s.startLocalkube(c) + } + s.fatalf("Failed to create localkube container: %v", err) + + } + + binds := []string{ + "/sys:/sys:rw", + "/var/lib/docker:/var/lib/docker", + "/mnt/sda1/var/lib/docker:/mnt/sda1/var/lib/docker", + "/var/lib/kubelet:/var/lib/kubelet", + "/var/run:/var/run:rw", + "/:/rootfs:ro", + } + + // if provided mount etcd data dir + if len(dataDir) != 0 { + dataBind := fmt.Sprintf("%s:%s", dataDir, ContainerDataDir) + binds = append(binds, dataBind) + } + + hostConfig := &docker.HostConfig{ + Binds: binds, + NetworkMode: "host", + RestartPolicy: docker.AlwaysRestart(), + PidMode: "host", + Privileged: true, + } + err = client.StartContainer(ctr.ID, hostConfig) + if err != nil { + s.fatalf("Failed to start localkube: %v", err) + } + + s.printf("Started localkube...") + + err = setupContext(client.Endpoint()) + if err != nil { + s.fatalf("Could not configure kubectl context.") + } + s.printf("Setup and using kubectl `%s` context.", KubectlName) + return +} + +func (s SpreadCli) stopLocalkube(c *cli.Context) { + client := s.dockerOrErr() + + ctrs, err := client.ListContainers(docker.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + "label": []string{"rsprd.com/name=localkube"}, + }, + }) + if err != nil { + s.fatalf("Could not list containers: %v", err) + } + + for _, ctr := range ctrs { + if strings.HasPrefix(ctr.Status, "Up") { + s.printf("Stopping container `%s`...\n", ctr.ID) + if err := client.StopContainer(ctr.ID, 5); err != nil { + s.fatalf("Could not kill container: %v", err) + } + } + + if err := client.RemoveContainer(docker.RemoveContainerOptions{ID: ctr.ID}); err != nil { + s.fatalf("Could not remove container: %v", err) + } + } +} + +func (s SpreadCli) dockerOrErr() *docker.Client { + client, err := docker.NewClientFromEnv() + if err != nil { + s.fatalf("Could not create Docker client: %v", err) + } + + _, err = client.Version() + if err != nil { + s.fatalf("Unable to establish connection with Docker daemon: %v", err) + } + return client +} + +func localkube(attach bool, dataDir, tag string) docker.CreateContainerOptions { + return docker.CreateContainerOptions{ + Name: LocalkubeContainerName, + Config: &docker.Config{ + Hostname: "localkube", + AttachStderr: attach, + AttachStdout: attach, + Image: fmt.Sprintf("%s:%s", LocalkubeImageName, tag), + Env: []string{ + fmt.Sprintf("KUBE_ETCD_DATA_DIRECTORY=%s", ContainerDataDir), + }, + Labels: map[string]string{ + "rsprd.com/name": "localkube", + }, + StopSignal: "SIGINT", + }, + } +} + +func identifyHost(endpoint string) (string, error) { + beginPort := strings.LastIndex(endpoint, ":") + switch { + // if using TCP use provided host + case strings.HasPrefix(endpoint, "tcp://"): + return endpoint[6:beginPort], nil + // assuming localhost if Unix + // TODO: Make this customizable + case strings.HasPrefix(endpoint, "unix://"): + return "127.0.0.1", nil + } + return "", fmt.Errorf("Could not determine localkube API server from endpoint `%s`", endpoint) +} + +func setupContext(endpoint string) error { + host, err := identifyHost(endpoint) + if err != nil { + return fmt.Errorf("Could not identify host: %v", err) + } + + pathOpts := kubectlcfg.NewDefaultPathOptions() + + config, err := pathOpts.GetStartingConfig() + if err != nil { + return fmt.Errorf("could not setup config: %v", err) + } + + cluster, exists := config.Clusters[KubectlName] + if !exists { + cluster = kubectlapi.NewCluster() + } + + // configure cluster + cluster.Server = fmt.Sprintf("%s:8080", host) + cluster.InsecureSkipTLSVerify = true + config.Clusters[KubectlName] = cluster + + context, exists := config.Contexts[KubectlName] + if !exists { + context = kubectlapi.NewContext() + } + + // configure context + context.Cluster = KubectlName + config.Contexts[KubectlName] = context + + config.CurrentContext = KubectlName + + return kubectlcfg.ModifyConfig(pathOpts, *config, true) +} From a5cd99c018cd32c6c3337ad91fe53f15943882af Mon Sep 17 00:00:00 2001 From: Dan Gillespie Date: Sun, 20 Mar 2016 11:24:39 -0700 Subject: [PATCH 2/2] fixed deps --- Godeps/Godeps.json | 5 + Makefile | 8 ++ .../github.com/mitchellh/go-homedir/LICENSE | 21 +++ .../github.com/mitchellh/go-homedir/README.md | 14 ++ .../mitchellh/go-homedir/homedir.go | 132 ++++++++++++++++++ 5 files changed, 180 insertions(+) create mode 100644 vendor/github.com/mitchellh/go-homedir/LICENSE create mode 100644 vendor/github.com/mitchellh/go-homedir/README.md create mode 100644 vendor/github.com/mitchellh/go-homedir/homedir.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1a6da8d..4d0bd13 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,7 @@ { "ImportPath": "rsprd.com/spread", "GoVersion": "go1.6", + "GodepVersion": "v60", "Packages": [ "./pkg/...", "./cli/...", @@ -189,6 +190,10 @@ "ImportPath": "github.com/matttproud/golang_protobuf_extensions/pbutil", "Rev": "fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a" }, + { + "ImportPath": "github.com/mitchellh/go-homedir", + "Rev": "981ab348d865cf048eb7d17e78ac7192632d8415" + }, { "ImportPath": "github.com/opencontainers/runc/libcontainer/cgroups", "Comment": "v0.0.7", diff --git a/Makefile b/Makefile index 06fed8b..1356c52 100644 --- a/Makefile +++ b/Makefile @@ -79,3 +79,11 @@ clean: rm -vf .gox-* .golint-* rm -rfv ./build $(GO) clean $(PKGS) || true + +.PHONY: godep +godep: + go get -u -v github.com/tools/godep + @echo "Recalculating godeps, removing Godeps and vendor if not canceled in 5 seconds" + @sleep 5 + rm -rf Godeps vendor + GO15VENDOREXPERIMENT="1" godep save -v ./pkg/... ./cli/... ./cmd/... \ No newline at end of file diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 0000000..f9c841a --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md new file mode 100644 index 0000000..d70706d --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/README.md @@ -0,0 +1,14 @@ +# go-homedir + +This is a Go library for detecting the user's home directory without +the use of cgo, so the library can be used in cross-compilation environments. + +Usage is incredibly simple, just call `homedir.Dir()` to get the home directory +for a user, and `homedir.Expand()` to expand the `~` in a path to the home +directory. + +**Why not just use `os/user`?** The built-in `os/user` package requires +cgo on Darwin systems. This means that any Go code that uses that package +cannot cross compile. But 99% of the time the use for `os/user` is just to +retrieve the home directory, which we can do for the current user without +cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go new file mode 100644 index 0000000..086514b --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir.go @@ -0,0 +1,132 @@ +package homedir + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" +) + +// DisableCache will disable caching of the home directory. Caching is enabled +// by default. +var DisableCache bool + +var homedirCache string +var cacheLock sync.RWMutex + +// Dir returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Dir() (string, error) { + if !DisableCache { + cacheLock.RLock() + cached := homedirCache + cacheLock.RUnlock() + if cached != "" { + return cached, nil + } + } + + cacheLock.Lock() + defer cacheLock.Unlock() + + var result string + var err error + if runtime.GOOS == "windows" { + result, err = dirWindows() + } else { + // Unix-like system, so just assume Unix + result, err = dirUnix() + } + + if err != nil { + return "", err + } + homedirCache = result + return result, nil +} + +// Expand expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~`, the path is +// returned as-is. +func Expand(path string) (string, error) { + if len(path) == 0 { + return path, nil + } + + if path[0] != '~' { + return path, nil + } + + if len(path) > 1 && path[1] != '/' && path[1] != '\\' { + return "", errors.New("cannot expand user-specific home dir") + } + + dir, err := Dir() + if err != nil { + return "", err + } + + return filepath.Join(dir, path[1:]), nil +} + +func dirUnix() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + // If that fails, try getent + var stdout bytes.Buffer + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If "getent" is missing, ignore it + if err == exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd = exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func dirWindows() (string, error) { + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + home = os.Getenv("USERPROFILE") + } + if home == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank") + } + + return home, nil +}