{"action":"created","comment":{"url":"https://api.github.com/repos/deis/deis/pulls/comments/37585564","id":37585564,"diff_hunk":"@@ -0,0 +1,529 @@\n+/*Package etcd is a library for performing common Etcd tasks.\n+ */\n+package etcd\n+\n+import (\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io/ioutil\"\n+\t\"net/http\"\n+\t\"os\"\n+\t\"os/exec\"\n+\t\"strings\"\n+\t\"time\"\n+\n+\t\"github.com/Masterminds/cookoo\"\n+\t\"github.com/Masterminds/cookoo/log\"\n+\t\"github.com/Masterminds/cookoo/safely\"\n+\n+\t\"github.com/coreos/go-etcd/etcd\"\n+)\n+\n+var (\n+\tretryCycles = 2\n+\tretrySleep  = 200 * time.Millisecond\n+)\n+\n+// Getter describes the Get behavior of an Etcd client.\n+//\n+// Usually you will want to use go-etcd/etcd.Client to satisfy this.\n+//\n+// We use an interface because it is more testable.\n+type Getter interface {\n+\tGet(string, bool, bool) (*etcd.Response, error)\n+}\n+\n+// DirCreator describes etcd's CreateDir behavior.\n+//\n+// Usually you will want to use go-etcd/etcd.Client to satisfy this.\n+type DirCreator interface {\n+\tCreateDir(string, uint64) (*etcd.Response, error)\n+}\n+\n+// Watcher watches an etcd entry.\n+type Watcher interface {\n+\tWatch(string, uint64, bool, chan *etcd.Response, chan bool) (*etcd.Response, error)\n+}\n+\n+// Setter sets a value in Etcd.\n+type Setter interface {\n+\tSet(string, string, uint64) (*etcd.Response, error)\n+}\n+\n+// GetterSetter performs get and set operations.\n+type GetterSetter interface {\n+\tGetter\n+\tSetter\n+}\n+\n+// CreateClient creates a new Etcd client and prepares it for work.\n+//\n+// Params:\n+// \t- url (string): A server to connect to.\n+// \t- retries (int): Number of times to retry a connection to the server\n+// \t- retrySleep (time.Duration): How long to sleep between retries\n+//\n+// Returns:\n+// \tThis puts an *etcd.Client into the context.\n+func CreateClient(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\turl := p.Get(\"url\", \"http://localhost:4001\").(string)\n+\n+\t// Backed this out because it's unnecessary so far.\n+\t//hosts := p.Get(\"urls\", []string{\"http://localhost:4001\"}).([]string)\n+\thosts := []string{url}\n+\tretryCycles = p.Get(\"retries\", retryCycles).(int)\n+\tretrySleep = p.Get(\"retrySleep\", retrySleep).(time.Duration)\n+\n+\t// Support `host:port` format, too.\n+\tfor i, host := range hosts {\n+\t\tif !strings.Contains(host, \"://\") {\n+\t\t\thosts[i] = \"http://\" + host\n+\t\t}\n+\t}\n+\n+\tclient := etcd.NewClient(hosts)\n+\tclient.CheckRetry = checkRetry\n+\n+\treturn client, nil\n+}\n+\n+// Get performs an etcd Get operation.\n+//\n+// Params:\n+// \t- client (EtcdGetter): Etcd client\n+// \t- path (string): The path/key to fetch\n+//\n+// Returns:\n+// - This puts an `etcd.Response` into the context, and returns an error\n+//   if the client could not connect.\n+func Get(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tcli, ok := p.Has(\"client\")\n+\tif !ok {\n+\t\treturn nil, errors.New(\"No Etcd client found.\")\n+\t}\n+\tclient := cli.(Getter)\n+\tpath := p.Get(\"path\", \"/\").(string)\n+\n+\tres, err := client.Get(path, false, false)\n+\tif err != nil {\n+\t\treturn res, err\n+\t}\n+\n+\tif !res.Node.Dir {\n+\t\treturn res, fmt.Errorf(\"Expected / to be a dir.\")\n+\t}\n+\treturn res, err\n+}\n+\n+// IsRunning checks to see if etcd is running.\n+//\n+// It will test `count` times before giving up.\n+//\n+// Params:\n+// \t- client (EtcdGetter)\n+// \t- count (int): Number of times to try before giving up.\n+//\n+// Returns:\n+// \tboolean true if etcd is listening.\n+func IsRunning(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tclient := p.Get(\"client\", nil).(Getter)\n+\tcount := p.Get(\"count\", 20).(int)\n+\tfor i := 0; i < count; i++ {\n+\t\t_, err := client.Get(\"/\", false, false)\n+\t\tif err == nil {\n+\t\t\treturn true, nil\n+\t\t}\n+\t\tlog.Infof(c, \"Waiting for etcd to come online.\")\n+\t\ttime.Sleep(250 * time.Millisecond)\n+\t}\n+\tlog.Errf(c, \"Etcd is not answering after %d attempts.\", count)\n+\treturn false, &cookoo.FatalError{\"Could not connect to Etcd.\"}\n+}\n+\n+// Set sets a value in etcd.\n+//\n+// Params:\n+// \t- key (string): The key\n+// \t- value (string): The value\n+// \t- ttl (uint64): Time to live\n+// \t- client (EtcdGetter): Client, usually an *etcd.Client.\n+//\n+// Returns:\n+// \t- *etcd.Result\n+func Set(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tkey := p.Get(\"key\", \"\").(string)\n+\tvalue := p.Get(\"value\", \"\").(string)\n+\tttl := p.Get(\"ttl\", uint64(20)).(uint64)\n+\tclient := p.Get(\"client\", nil).(Setter)\n+\n+\tres, err := client.Set(key, value, ttl)\n+\tif err != nil {\n+\t\tlog.Infof(c, \"Failed to set %s=%s\", key, value)\n+\t}\n+\n+\treturn res, err\n+}\n+\n+// FindSSHUser finds an SSH user by public key.\n+//\n+// Some parts of the system require that we know not only the SSH key, but also\n+// the name of the user. That information is stored in etcd.\n+//\n+// Params:\n+// \t- client (EtcdGetter)\n+// \t- fingerprint (string): The fingerprint of the SSH key.\n+//\n+// Returns:\n+// - username (string)\n+func FindSSHUser(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tclient := p.Get(\"client\", nil).(Getter)\n+\tfingerprint := p.Get(\"fingerprint\", nil).(string)\n+\n+\tres, err := client.Get(\"/deis/builder/users\", false, true)\n+\tif err != nil {\n+\t\tlog.Warnf(c, \"Error querying etcd: %s\", err)\n+\t\treturn \"\", err\n+\t} else if res.Node == nil || !res.Node.Dir {\n+\t\tlog.Warnf(c, \"No users found in etcd.\")\n+\t\treturn \"\", errors.New(\"Users not found\")\n+\t}\n+\tfor _, user := range res.Node.Nodes {\n+\t\tlog.Infof(c, \"Checking user %s\", user.Key)\n+\t\tfor _, keyprint := range user.Nodes {\n+\t\t\tif strings.HasSuffix(keyprint.Key, fingerprint) {\n+\t\t\t\tparts := strings.Split(user.Key, \"/\")\n+\t\t\t\tusername := parts[len(parts)-1]\n+\t\t\t\tlog.Infof(c, \"Found user %s for fingerprint %s\", username, fingerprint)\n+\t\t\t\treturn username, nil\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn \"\", fmt.Errorf(\"User not found for fingerprint %s\", fingerprint)\n+}\n+\n+// StoreHostKeys stores SSH hostkeys locally.\n+//\n+// First it tries to fetch them from etcd. If the keys are not present there,\n+// it generates new ones and then puts the new onces into etcd.\n+//\n+// Params:\n+// \t- client(EtcdGetterSetter)\n+// \t- ciphers([]string): A list of ciphers to generate. Defaults are dsa,\n+// \t\tecdsa, ed25519 and rsa.\n+// \t- basepath (string): Base path in etcd (ETCD_PATH).\n+// Returns:\n+//\n+func StoreHostKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tdefaultCiphers := []string{\"rsa\", \"dsa\", \"ecdsa\", \"ed25519\"}\n+\tclient := p.Get(\"client\", nil).(GetterSetter)\n+\tciphers := p.Get(\"ciphers\", defaultCiphers).([]string)\n+\tbasepath := p.Get(\"basepath\", \"/deis/builder\").(string)\n+\n+\tres, err := client.Get(\"sshHostKey\", false, false)\n+\tif err != nil || res.Node == nil {\n+\t\tlog.Infof(c, \"Could not get SSH host key from etcd. Generating new ones.\")\n+\t\tif err := genSSHKeys(c); err != nil {\n+\t\t\tlog.Err(c, \"Failed to generate SSH keys. Aborting.\")\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tif err := keysToEtcd(c, client, ciphers, basepath); err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t} else if err := keysToLocal(c, client, ciphers, basepath); err != nil {\n+\t\tlog.Infof(c, \"Fetching SSH host keys from etcd.\")\n+\t\treturn nil, err\n+\t}\n+\n+\treturn nil, nil\n+}\n+\n+// keysToLocal copies SSH host keys from etcd to the local file system.\n+//\n+// This only fails if the main key, sshHostKey cannot be stored or retrieved.\n+func keysToLocal(c cookoo.Context, client Getter, ciphers []string, etcdPath string) error {\n+\tlpath := \"/etc/ssh/ssh_host_%s_key\"\n+\tprivkey := \"%s/sshHost%sKey\"\n+\tfor _, cipher := range ciphers {\n+\t\tpath := fmt.Sprintf(lpath, cipher)\n+\t\tkey := fmt.Sprintf(privkey, etcdPath, cipher)\n+\t\tres, err := client.Get(key, false, false)\n+\t\tif err != nil || res.Node == nil {\n+\t\t\tcontinue\n+\t\t}\n+\n+\t\tcontent := res.Node.Value\n+\t\tif err := ioutil.WriteFile(path, []byte(content), 0600); err != nil {\n+\t\t\tlog.Errf(c, \"Error writing ssh host key file: %s\", err)\n+\t\t}\n+\t}\n+\n+\t// Now get generic key.\n+\tres, err := client.Get(\"sshHostKey\", false, false)\n+\tif err != nil || res.Node == nil {\n+\t\treturn fmt.Errorf(\"Failed to get sshHostKey from etcd. %v\", err)\n+\t}\n+\n+\tcontent := res.Node.Value\n+\tif err := ioutil.WriteFile(\"/etc/ssh/ssh_host_key\", []byte(content), 0600); err != nil {\n+\t\tlog.Errf(c, \"Error writing ssh host key file: %s\", err)\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+// keysToEtcd copies local keys into etcd.\n+//\n+// It only fails if it cannot copy ssh_host_key to sshHostKey. All other\n+// abnormal conditions are logged, but not considered to be failures.\n+func keysToEtcd(c cookoo.Context, client Setter, ciphers []string, etcdPath string) error {\n+\tlpath := \"/etc/ssh/ssh_host_%s_key\"\n+\tprivkey := \"%s/sshHost%sKey\"\n+\tfor _, cipher := range ciphers {\n+\t\tpath := fmt.Sprintf(lpath, cipher)\n+\t\tkey := fmt.Sprintf(privkey, etcdPath, cipher)\n+\t\tcontent, err := ioutil.ReadFile(path)\n+\t\tif err != nil {\n+\t\t\tlog.Infof(c, \"No key named %s\", path)\n+\t\t} else if _, err := client.Set(key, string(content), 0); err != nil {\n+\t\t\tlog.Errf(c, \"Could not store ssh key in etcd: %s\", err)\n+\t\t}\n+\t}\n+\t// Now we set the generic key:\n+\tif content, err := ioutil.ReadFile(\"/etc/ssh/ssh_host_key\"); err != nil {\n+\t\tlog.Errf(c, \"Could not read the ssh_host_key file.\")\n+\t\treturn err\n+\t} else if _, err := client.Set(\"sshHostKey\", string(content), 0); err != nil {\n+\t\tlog.Errf(c, \"Failed to set sshHostKey in etcd.\")\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+// genSshKeys generates the default set of SSH host keys.\n+func genSSHKeys(c cookoo.Context) error {\n+\t// Generate a new key\n+\tout, err := exec.Command(\"ssh-keygen\", \"-A\").CombinedOutput()\n+\tif err != nil {\n+\t\tlog.Infof(c, \"ssh-keygen: %s\", out)\n+\t\tlog.Errf(c, \"Failed to generate SSH keys: %s\", err)\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+// UpdateHostPort intermittently notifies etcd of the builder's address.\n+//\n+// If `port` is specified, this will notify etcd at 10 second intervals that\n+// the builder is listening at $HOST:$PORT, setting the TTL to 20 seconds.\n+//\n+// This will notify etcd as long as the local sshd is running.\n+//\n+// Params:\n+// \t- base (string): The base path to write the data: $base/host and $base/port.\n+// \t- host (string): The hostname\n+// \t- port (string): The port\n+// \t- client (Setter): The client to use to write the data to etcd.\n+// \t- sshPid (int): The PID for SSHD. If SSHD dies, this stops notifying.\n+func UpdateHostPort(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\n+\tbase := p.Get(\"base\", \"\").(string)\n+\thost := p.Get(\"host\", \"\").(string)\n+\tport := p.Get(\"port\", \"\").(string)\n+\tclient := p.Get(\"client\", nil).(Setter)\n+\tsshd := p.Get(\"sshdPid\", 0).(int)\n+\n+\t// If no port is specified, we don't do anything.\n+\tif len(port) == 0 {\n+\t\tlog.Infof(c, \"No external port provided. Not publishing details.\")\n+\t\treturn false, nil\n+\t}\n+\n+\tvar ttl uint64 = 20\n+\n+\tif err := setHostPort(client, base, host, port, ttl); err != nil {\n+\t\tlog.Errf(c, \"Etcd error setting host/port: %s\", err)\n+\t\treturn false, err\n+\t}\n+\n+\t// Update etcd every ten seconds with this builder's host/port.\n+\tsafely.GoDo(c, func() {\n+\t\tticker := time.NewTicker(10 * time.Second)\n+\t\tfor range ticker.C {\n+\t\t\t//log.Infof(c, \"Setting SSHD host/port\")\n+\t\t\tif _, err := os.FindProcess(sshd); err != nil {\n+\t\t\t\tlog.Errf(c, \"Lost SSHd process: %s\", err)\n+\t\t\t\tbreak\n+\t\t\t} else {\n+\t\t\t\tif err := setHostPort(client, base, host, port, ttl); err != nil {\n+\t\t\t\t\tlog.Errf(c, \"Etcd error setting host/port: %s\", err)\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\tticker.Stop()\n+\t})\n+\n+\treturn true, nil\n+}\n+\n+func setHostPort(client Setter, base, host, port string, ttl uint64) error {\n+\tif _, err := client.Set(base+\"/host\", host, ttl); err != nil {\n+\t\treturn err\n+\t}\n+\tif _, err := client.Set(base+\"/port\", port, ttl); err != nil {\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+// MakeDir makes a directory in Etcd.\n+//\n+// Params:\n+// \t- client (EtcdDirCreator): Etcd client\n+//  - path (string): The name of the directory to create.\n+// \t- ttl (uint64): Time to live.\n+// Returns:\n+// \t*etcd.Response\n+func MakeDir(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\tname := p.Get(\"path\", \"\").(string)\n+\tttl := p.Get(\"ttl\", uint64(0)).(uint64)\n+\tcli, ok := p.Has(\"client\")\n+\tif !ok {\n+\t\treturn nil, errors.New(\"No Etcd client found.\")\n+\t}\n+\tclient := cli.(DirCreator)\n+\n+\tif len(name) == 0 {\n+\t\treturn false, errors.New(\"Expected directory name to be more than zero characters.\")\n+\t}\n+\n+\tres, err := client.CreateDir(name, ttl)\n+\tif err != nil {\n+\t\treturn res, &cookoo.RecoverableError{err.Error()}\n+\t}\n+\n+\treturn res, nil\n+\n+}\n+\n+// Watch watches a given path, and executes a git check-repos for each event.\n+//\n+// It starts the watcher and then returns. The watcher runs on its own\n+// goroutine. To stop the watching, send the returned channel a bool.\n+//\n+// Params:\n+// - client (Watcher): An Etcd client.\n+// - path (string): The path to watch\n+//\n+// Returns:\n+// \t- chan bool: Send this a message to stop the watcher.\n+func Watch(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {\n+\t// etcdctl -C $ETCD watch --recursive /deis/services\n+\tpath := p.Get(\"path\", \"/deis/services\").(string)\n+\tcli, ok := p.Has(\"client\")\n+\tif !ok {\n+\t\treturn nil, errors.New(\"No etcd client found.\")\n+\t}\n+\tclient := cli.(Watcher)\n+\n+\t// Stupid hack because etcd watch seems to be broken, constantly complaining\n+\t// that the JSON it received is malformed.\n+\tsafely.GoDo(c, func() {\n+\t\tfor {\n+\t\t\tresponse, err := client.Watch(path, 0, true, nil, nil)\n+\t\t\tif err != nil {\n+\t\t\t\tlog.Errf(c, \"Etcd Watch failed: %s\", err)\n+\t\t\t\ttime.Sleep(50 * time.Millisecond)\n+\t\t\t\tcontinue\n+\t\t\t}\n+\n+\t\t\tif response.Node == nil {\n+\t\t\t\tlog.Infof(c, \"Unexpected Etcd message: %v\", response)\n+\t\t\t}\n+\t\t\tgit := exec.Command(\"/home/git/check-repos\")\n+\t\t\tif out, err := git.CombinedOutput(); err != nil {\n+\t\t\t\tlog.Errf(c, \"Failed git check-repos: %s\", err)\n+\t\t\t\tlog.Infof(c, \"Output: %s\", out)\n+\t\t\t}\n+\t\t}\n+\n+\t})\n+\n+\treturn nil, nil\n+\n+\t/* Watch seems to be broken. So we do this stupid watch loop instead.\n+\treceiver := make(chan *etcd.Response)\n+\tstop := make(chan bool)\n+\t// Buffer the channels so that we don't hang waiting for go-etcd to\n+\t// read off the channel.\n+\tstopetcd := make(chan bool, 1)\n+\tstopwatch := make(chan bool, 1)\n+\n+\n+\t// Watch for errors.\n+\tsafely.GoDo(c, func() {\n+\t\t// When a receiver is passed in, no *Response is ever returned. Instead,\n+\t\t// Watch acts like an error channel, and receiver gets all of the messages.\n+\t\t_, err := client.Watch(path, 0, true, receiver, stopetcd)\n+\t\tif err != nil {\n+\t\t\tlog.Infof(c, \"Watcher stopped with error '%s'\", err)\n+\t\t\tstopwatch <- true\n+\t\t\t//close(stopwatch)\n+\t\t}\n+\t})\n+\t// Watch for events\n+\tsafely.GoDo(c, func() {\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase msg := <-receiver:\n+\t\t\t\tif msg.Node != nil {\n+\t\t\t\t\tlog.Infof(c, \"Received notification %s for %s\", msg.Action, msg.Node.Key)\n+\t\t\t\t} else {\n+\t\t\t\t\tlog.Infof(c, \"Received unexpected etcd message: %v\", msg)\n+\t\t\t\t}\n+\t\t\t\tgit := exec.Command(\"/home/git/check-repos\")\n+\t\t\t\tif out, err := git.CombinedOutput(); err != nil {\n+\t\t\t\t\tlog.Errf(c, \"Failed git check-repos: %s\", err)\n+\t\t\t\t\tlog.Infof(c, \"Output: %s\", out)\n+\t\t\t\t}\n+\t\t\tcase <-stopwatch:\n+\t\t\t\tc.Logf(\"debug\", \"Received signal to stop watching events.\")\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t}\n+\t})\n+\t// Fan out stop requests.\n+\tsafely.GoDo(c, func() {\n+\t\t<-stop\n+\t\tstopwatch <- true\n+\t\tstopetcd <- true\n+\t\tclose(stopwatch)\n+\t\tclose(stopetcd)\n+\t})\n+\n+\treturn stop, nil\n+\t*/\n+}\n+\n+// checkRetry overrides etcd.DefaultCheckRetry.\n+//\n+// It adds configurable number of retries and configurable timesouts.\n+func checkRetry(c *etcd.Cluster, numReqs int, last http.Response, err error) error {\n+\n+\tif numReqs > retryCycles*len(c.Machines) {\n+\t\treturn fmt.Errorf(\"Tried and failed %d cluster connections: %s\", retryCycles, err)\n+\t}\n+\n+\tswitch last.StatusCode {\n+\tcase 0:\n+\t\treturn nil\n+\tcase 500:","path":"builder/etcd/etcd.go","position":521,"original_position":521,"commit_id":"a8aff73d73cd5c40ad89803c45cd1213c3b56d86","original_commit_id":"a8aff73d73cd5c40ad89803c45cd1213c3b56d86","user":{"login":"mboersma","id":73019,"avatar_url":"https://avatars.githubusercontent.com/u/73019?v=3","gravatar_id":"","url":"https://api.github.com/users/mboersma","html_url":"https://github.com/mboersma","followers_url":"https://api.github.com/users/mboersma/followers","following_url":"https://api.github.com/users/mboersma/following{/other_user}","gists_url":"https://api.github.com/users/mboersma/gists{/gist_id}","starred_url":"https://api.github.com/users/mboersma/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mboersma/subscriptions","organizations_url":"https://api.github.com/users/mboersma/orgs","repos_url":"https://api.github.com/users/mboersma/repos","events_url":"https://api.github.com/users/mboersma/events{/privacy}","received_events_url":"https://api.github.com/users/mboersma/received_events","type":"User","site_admin":false},"body":"Would the constant [`http.StatusInternalServerError`](http://golang.org/pkg/net/http/#pkg-constants) make this more readable?","created_at":"2015-08-20T21:51:50Z","updated_at":"2015-08-20T21:51:50Z","html_url":"https://github.com/deis/deis/pull/4273#discussion_r37585564","pull_request_url":"https://api.github.com/repos/deis/deis/pulls/4273","_links":{"self":{"href":"https://api.github.com/repos/deis/deis/pulls/comments/37585564"},"html":{"href":"https://github.com/deis/deis/pull/4273#discussion_r37585564"},"pull_request":{"href":"https://api.github.com/repos/deis/deis/pulls/4273"}}},"pull_request":{"url":"https://api.github.com/repos/deis/deis/pulls/4273","id":42416818,"html_url":"https://github.com/deis/deis/pull/4273","diff_url":"https://github.com/deis/deis/pull/4273.diff","patch_url":"https://github.com/deis/deis/pull/4273.patch","issue_url":"https://api.github.com/repos/deis/deis/issues/4273","number":4273,"state":"open","locked":false,"title":"feat(builder): reimplement the builder in Go","user":{"login":"technosophos","id":89193,"avatar_url":"https://avatars.githubusercontent.com/u/89193?v=3","gravatar_id":"","url":"https://api.github.com/users/technosophos","html_url":"https://github.com/technosophos","followers_url":"https://api.github.com/users/technosophos/followers","following_url":"https://api.github.com/users/technosophos/following{/other_user}","gists_url":"https://api.github.com/users/technosophos/gists{/gist_id}","starred_url":"https://api.github.com/users/technosophos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/technosophos/subscriptions","organizations_url":"https://api.github.com/users/technosophos/orgs","repos_url":"https://api.github.com/users/technosophos/repos","events_url":"https://api.github.com/users/technosophos/events{/privacy}","received_events_url":"https://api.github.com/users/technosophos/received_events","type":"User","site_admin":false},"body":"This is part of issue #3836, where the main builder scripts are\r\nrewritten as a Go executable.\r\n\r\nThis feature adds an SSH server implementation to Go, which means we\r\nhave removed one of the major runtime interops on Builder.\r\n\r\nThis changes builder's internal port, and should not break other\r\nthings, but makes Flannel configuration easier.","created_at":"2015-08-14T01:26:06Z","updated_at":"2015-08-20T21:51:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7b7fb51de36baf3ff821ced197782f4facd2b2b2","assignee":{"login":"technosophos","id":89193,"avatar_url":"https://avatars.githubusercontent.com/u/89193?v=3","gravatar_id":"","url":"https://api.github.com/users/technosophos","html_url":"https://github.com/technosophos","followers_url":"https://api.github.com/users/technosophos/followers","following_url":"https://api.github.com/users/technosophos/following{/other_user}","gists_url":"https://api.github.com/users/technosophos/gists{/gist_id}","starred_url":"https://api.github.com/users/technosophos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/technosophos/subscriptions","organizations_url":"https://api.github.com/users/technosophos/orgs","repos_url":"https://api.github.com/users/technosophos/repos","events_url":"https://api.github.com/users/technosophos/events{/privacy}","received_events_url":"https://api.github.com/users/technosophos/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/deis/deis/milestones/43","html_url":"https://github.com/deis/deis/milestones/v1.10","labels_url":"https://api.github.com/repos/deis/deis/milestones/43/labels","id":1189802,"number":43,"title":"v1.10","description":"","creator":{"login":"carmstrong","id":536009,"avatar_url":"https://avatars.githubusercontent.com/u/536009?v=3","gravatar_id":"","url":"https://api.github.com/users/carmstrong","html_url":"https://github.com/carmstrong","followers_url":"https://api.github.com/users/carmstrong/followers","following_url":"https://api.github.com/users/carmstrong/following{/other_user}","gists_url":"https://api.github.com/users/carmstrong/gists{/gist_id}","starred_url":"https://api.github.com/users/carmstrong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/carmstrong/subscriptions","organizations_url":"https://api.github.com/users/carmstrong/orgs","repos_url":"https://api.github.com/users/carmstrong/repos","events_url":"https://api.github.com/users/carmstrong/events{/privacy}","received_events_url":"https://api.github.com/users/carmstrong/received_events","type":"User","site_admin":false},"open_issues":18,"closed_issues":66,"state":"open","created_at":"2015-07-01T22:28:27Z","updated_at":"2015-08-20T19:15:46Z","due_on":"2015-09-01T06:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/deis/deis/pulls/4273/commits","review_comments_url":"https://api.github.com/repos/deis/deis/pulls/4273/comments","review_comment_url":"https://api.github.com/repos/deis/deis/pulls/comments{/number}","comments_url":"https://api.github.com/repos/deis/deis/issues/4273/comments","statuses_url":"https://api.github.com/repos/deis/deis/statuses/a8aff73d73cd5c40ad89803c45cd1213c3b56d86","head":{"label":"technosophos:feature/go-builder","ref":"feature/go-builder","sha":"a8aff73d73cd5c40ad89803c45cd1213c3b56d86","user":{"login":"technosophos","id":89193,"avatar_url":"https://avatars.githubusercontent.com/u/89193?v=3","gravatar_id":"","url":"https://api.github.com/users/technosophos","html_url":"https://github.com/technosophos","followers_url":"https://api.github.com/users/technosophos/followers","following_url":"https://api.github.com/users/technosophos/following{/other_user}","gists_url":"https://api.github.com/users/technosophos/gists{/gist_id}","starred_url":"https://api.github.com/users/technosophos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/technosophos/subscriptions","organizations_url":"https://api.github.com/users/technosophos/orgs","repos_url":"https://api.github.com/users/technosophos/repos","events_url":"https://api.github.com/users/technosophos/events{/privacy}","received_events_url":"https://api.github.com/users/technosophos/received_events","type":"User","site_admin":false},"repo":{"id":37152356,"name":"deis","full_name":"technosophos/deis","owner":{"login":"technosophos","id":89193,"avatar_url":"https://avatars.githubusercontent.com/u/89193?v=3","gravatar_id":"","url":"https://api.github.com/users/technosophos","html_url":"https://github.com/technosophos","followers_url":"https://api.github.com/users/technosophos/followers","following_url":"https://api.github.com/users/technosophos/following{/other_user}","gists_url":"https://api.github.com/users/technosophos/gists{/gist_id}","starred_url":"https://api.github.com/users/technosophos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/technosophos/subscriptions","organizations_url":"https://api.github.com/users/technosophos/orgs","repos_url":"https://api.github.com/users/technosophos/repos","events_url":"https://api.github.com/users/technosophos/events{/privacy}","received_events_url":"https://api.github.com/users/technosophos/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/technosophos/deis","description":"Your PaaS. Your Rules. ","fork":true,"url":"https://api.github.com/repos/technosophos/deis","forks_url":"https://api.github.com/repos/technosophos/deis/forks","keys_url":"https://api.github.com/repos/technosophos/deis/keys{/key_id}","collaborators_url":"https://api.github.com/repos/technosophos/deis/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/technosophos/deis/teams","hooks_url":"https://api.github.com/repos/technosophos/deis/hooks","issue_events_url":"https://api.github.com/repos/technosophos/deis/issues/events{/number}","events_url":"https://api.github.com/repos/technosophos/deis/events","assignees_url":"https://api.github.com/repos/technosophos/deis/assignees{/user}","branches_url":"https://api.github.com/repos/technosophos/deis/branches{/branch}","tags_url":"https://api.github.com/repos/technosophos/deis/tags","blobs_url":"https://api.github.com/repos/technosophos/deis/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/technosophos/deis/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/technosophos/deis/git/refs{/sha}","trees_url":"https://api.github.com/repos/technosophos/deis/git/trees{/sha}","statuses_url":"https://api.github.com/repos/technosophos/deis/statuses/{sha}","languages_url":"https://api.github.com/repos/technosophos/deis/languages","stargazers_url":"https://api.github.com/repos/technosophos/deis/stargazers","contributors_url":"https://api.github.com/repos/technosophos/deis/contributors","subscribers_url":"https://api.github.com/repos/technosophos/deis/subscribers","subscription_url":"https://api.github.com/repos/technosophos/deis/subscription","commits_url":"https://api.github.com/repos/technosophos/deis/commits{/sha}","git_commits_url":"https://api.github.com/repos/technosophos/deis/git/commits{/sha}","comments_url":"https://api.github.com/repos/technosophos/deis/comments{/number}","issue_comment_url":"https://api.github.com/repos/technosophos/deis/issues/comments{/number}","contents_url":"https://api.github.com/repos/technosophos/deis/contents/{+path}","compare_url":"https://api.github.com/repos/technosophos/deis/compare/{base}...{head}","merges_url":"https://api.github.com/repos/technosophos/deis/merges","archive_url":"https://api.github.com/repos/technosophos/deis/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/technosophos/deis/downloads","issues_url":"https://api.github.com/repos/technosophos/deis/issues{/number}","pulls_url":"https://api.github.com/repos/technosophos/deis/pulls{/number}","milestones_url":"https://api.github.com/repos/technosophos/deis/milestones{/number}","notifications_url":"https://api.github.com/repos/technosophos/deis/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/technosophos/deis/labels{/name}","releases_url":"https://api.github.com/repos/technosophos/deis/releases{/id}","created_at":"2015-06-09T19:05:33Z","updated_at":"2015-06-09T19:05:34Z","pushed_at":"2015-08-20T20:01:16Z","git_url":"git://github.com/technosophos/deis.git","ssh_url":"git@github.com:technosophos/deis.git","clone_url":"https://github.com/technosophos/deis.git","svn_url":"https://github.com/technosophos/deis","homepage":"http://deis.io","size":18824,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"deis:master","ref":"master","sha":"101bbba3788043e64545a2c67ead224c4958b048","user":{"login":"deis","id":6117407,"avatar_url":"https://avatars.githubusercontent.com/u/6117407?v=3","gravatar_id":"","url":"https://api.github.com/users/deis","html_url":"https://github.com/deis","followers_url":"https://api.github.com/users/deis/followers","following_url":"https://api.github.com/users/deis/following{/other_user}","gists_url":"https://api.github.com/users/deis/gists{/gist_id}","starred_url":"https://api.github.com/users/deis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/deis/subscriptions","organizations_url":"https://api.github.com/users/deis/orgs","repos_url":"https://api.github.com/users/deis/repos","events_url":"https://api.github.com/users/deis/events{/privacy}","received_events_url":"https://api.github.com/users/deis/received_events","type":"Organization","site_admin":false},"repo":{"id":11589713,"name":"deis","full_name":"deis/deis","owner":{"login":"deis","id":6117407,"avatar_url":"https://avatars.githubusercontent.com/u/6117407?v=3","gravatar_id":"","url":"https://api.github.com/users/deis","html_url":"https://github.com/deis","followers_url":"https://api.github.com/users/deis/followers","following_url":"https://api.github.com/users/deis/following{/other_user}","gists_url":"https://api.github.com/users/deis/gists{/gist_id}","starred_url":"https://api.github.com/users/deis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/deis/subscriptions","organizations_url":"https://api.github.com/users/deis/orgs","repos_url":"https://api.github.com/users/deis/repos","events_url":"https://api.github.com/users/deis/events{/privacy}","received_events_url":"https://api.github.com/users/deis/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/deis/deis","description":"Your PaaS. Your Rules. ","fork":false,"url":"https://api.github.com/repos/deis/deis","forks_url":"https://api.github.com/repos/deis/deis/forks","keys_url":"https://api.github.com/repos/deis/deis/keys{/key_id}","collaborators_url":"https://api.github.com/repos/deis/deis/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/deis/deis/teams","hooks_url":"https://api.github.com/repos/deis/deis/hooks","issue_events_url":"https://api.github.com/repos/deis/deis/issues/events{/number}","events_url":"https://api.github.com/repos/deis/deis/events","assignees_url":"https://api.github.com/repos/deis/deis/assignees{/user}","branches_url":"https://api.github.com/repos/deis/deis/branches{/branch}","tags_url":"https://api.github.com/repos/deis/deis/tags","blobs_url":"https://api.github.com/repos/deis/deis/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/deis/deis/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/deis/deis/git/refs{/sha}","trees_url":"https://api.github.com/repos/deis/deis/git/trees{/sha}","statuses_url":"https://api.github.com/repos/deis/deis/statuses/{sha}","languages_url":"https://api.github.com/repos/deis/deis/languages","stargazers_url":"https://api.github.com/repos/deis/deis/stargazers","contributors_url":"https://api.github.com/repos/deis/deis/contributors","subscribers_url":"https://api.github.com/repos/deis/deis/subscribers","subscription_url":"https://api.github.com/repos/deis/deis/subscription","commits_url":"https://api.github.com/repos/deis/deis/commits{/sha}","git_commits_url":"https://api.github.com/repos/deis/deis/git/commits{/sha}","comments_url":"https://api.github.com/repos/deis/deis/comments{/number}","issue_comment_url":"https://api.github.com/repos/deis/deis/issues/comments{/number}","contents_url":"https://api.github.com/repos/deis/deis/contents/{+path}","compare_url":"https://api.github.com/repos/deis/deis/compare/{base}...{head}","merges_url":"https://api.github.com/repos/deis/deis/merges","archive_url":"https://api.github.com/repos/deis/deis/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/deis/deis/downloads","issues_url":"https://api.github.com/repos/deis/deis/issues{/number}","pulls_url":"https://api.github.com/repos/deis/deis/pulls{/number}","milestones_url":"https://api.github.com/repos/deis/deis/milestones{/number}","notifications_url":"https://api.github.com/repos/deis/deis/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/deis/deis/labels{/name}","releases_url":"https://api.github.com/repos/deis/deis/releases{/id}","created_at":"2013-07-22T19:02:40Z","updated_at":"2015-08-20T12:24:07Z","pushed_at":"2015-08-20T20:58:41Z","git_url":"git://github.com/deis/deis.git","ssh_url":"git@github.com:deis/deis.git","clone_url":"https://github.com/deis/deis.git","svn_url":"https://github.com/deis/deis","homepage":"http://deis.io","size":43260,"stargazers_count":4618,"watchers_count":4618,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":715,"mirror_url":null,"open_issues_count":235,"forks":715,"open_issues":235,"watchers":4618,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/deis/deis/pulls/4273"},"html":{"href":"https://github.com/deis/deis/pull/4273"},"issue":{"href":"https://api.github.com/repos/deis/deis/issues/4273"},"comments":{"href":"https://api.github.com/repos/deis/deis/issues/4273/comments"},"review_comments":{"href":"https://api.github.com/repos/deis/deis/pulls/4273/comments"},"review_comment":{"href":"https://api.github.com/repos/deis/deis/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/deis/deis/pulls/4273/commits"},"statuses":{"href":"https://api.github.com/repos/deis/deis/statuses/a8aff73d73cd5c40ad89803c45cd1213c3b56d86"}}}}