// Push pushes an image to the registry. // // This finds the appropriate registry by looking it up in etcd. // // Params: // - client (etcd.Getter): Client to do etcd lookups. // - tag (string): Tag to push. // // Returns: // func Push(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { // docker tag deis/slugrunner:lastest HOST:PORT/deis/slugrunner:latest // docker push HOST:PORT/deis/slugrunner:latest client := p.Get("client", nil).(etcd.Getter) host, err := client.Get("/deis/registry/host", false, false) if err != nil || host.Node == nil { return nil, err } port, err := client.Get("/deis/registry/port", false, false) if err != nil || host.Node == nil { return nil, err } registry := host.Node.Value + ":" + port.Node.Value tag := p.Get("tag", "").(string) log.Infof(c, "Pushing %s to %s. This may take some time.", tag, registry) rem := path.Join(registry, tag) out, err := exec.Command("docker", "tag", "-f", tag, rem).CombinedOutput() if err != nil { log.Warnf(c, "Failed to tag %s on host %s: %s (%s)", tag, rem, err, out) } out, err = exec.Command("docker", "-D", "push", rem).CombinedOutput() if err != nil { log.Warnf(c, "Failed to push %s to host %s: %s (%s)", tag, rem, err, out) return nil, err } log.Infof(c, "Finished pushing %s to %s.", tag, registry) return nil, nil }
// RunOnce runs the equivalent of `confd --onetime`. // // This may run the process repeatedly until either we time out (~20 minutes) or // the templates are successfully built. // // Importantly, this blocks until the run is complete. // // Params: // - node (string): The etcd node to use. (Only etcd is currently supported) // // Returns: // - The []bytes from stdout and stderr when running the program. // func RunOnce(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { node := p.Get("node", defaultEtcd).(string) dargs := []string{"-onetime", "-node", node, "-log-level", "error"} log.Info(c, "Building confd templates. This may take a moment.") limit := 1200 timeout := time.Second * 3 var lasterr error start := time.Now() for i := 0; i < limit; i++ { out, err := exec.Command("confd", dargs...).CombinedOutput() if err == nil { log.Infof(c, "Templates generated for %s on run %d", node, i) return out, nil } log.Debugf(c, "Recoverable error: %s", err) log.Debugf(c, "Output: %q", out) lasterr = err time.Sleep(timeout) log.Infof(c, "Re-trying template build. (Elapsed time: %d)", time.Now().Sub(start)/time.Second) } return nil, fmt.Errorf("Could not build confd templates before timeout. Last error: %s", lasterr) }
// Watch watches a given path, and executes a git check-repos for each event. // // It starts the watcher and then returns. The watcher runs on its own // goroutine. To stop the watching, send the returned channel a bool. // // Params: // - client (Watcher): An Etcd client. // - path (string): The path to watch func Watch(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { // etcdctl -C $ETCD watch --recursive /deis/services path := p.Get("path", "/deis/services").(string) cli := p.Get("client", nil).(client.Client) k := client.NewKeysAPI(cli) watcher := k.Watcher(path, &client.WatcherOptions{Recursive: true}) safely.GoDo(c, func() { for { // TODO: We should probably add cancellation support. response, err := watcher.Next(dctx()) if err != nil { log.Errf(c, "Etcd Watch failed: %s", err) } if response.Node == nil { log.Infof(c, "Unexpected Etcd message: %v", response) } git := exec.Command("/home/git/check-repos") if out, err := git.CombinedOutput(); err != nil { log.Errf(c, "Failed git check-repos: %s", err) log.Infof(c, "Output: %s", out) } } }) return nil, nil }
// createRepo creates a new Git repo if it is not present already. // // Largely inspired by gitreceived from Flynn. // // Returns a bool indicating whether a project was created (true) or already // existed (false). func createRepo(c cookoo.Context, repoPath, gitHome string) (bool, error) { createLock.Lock() defer createLock.Unlock() fi, err := os.Stat(repoPath) if err == nil && fi.IsDir() { // Nothing to do. log.Infof(c, "Directory %s already exists.", repoPath) return false, nil } else if os.IsNotExist(err) { log.Infof(c, "Creating new directory at %s", repoPath) // Create directory if err := os.MkdirAll(repoPath, 0755); err != nil { log.Warnf(c, "Failed to create repository: %s", err) return false, err } cmd := exec.Command("git", "init", "--bare") cmd.Dir = repoPath if out, err := cmd.CombinedOutput(); err != nil { log.Warnf(c, "git init output: %s", out) return false, err } hook, err := prereceiveHook(map[string]string{"GitHome": gitHome}) if err != nil { return true, err } ioutil.WriteFile(filepath.Join(repoPath, "hooks", "pre-receive"), hook, 0755) return true, nil } else if err == nil { return false, errors.New("Expected directory, found file.") } return false, err }
// Watch watches a given path, and executes a git check-repos for each event. // // It starts the watcher and then returns. The watcher runs on its own // goroutine. To stop the watching, send the returned channel a bool. // // Params: // - client (Watcher): An Etcd client. // - path (string): The path to watch // // Returns: // - chan bool: Send this a message to stop the watcher. func Watch(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { // etcdctl -C $ETCD watch --recursive /deis/services path := p.Get("path", "/deis/services").(string) cli, ok := p.Has("client") if !ok { return nil, errors.New("No etcd client found.") } client := cli.(Watcher) // Stupid hack because etcd watch seems to be broken, constantly complaining // that the JSON it received is malformed. safely.GoDo(c, func() { for { response, err := client.Watch(path, 0, true, nil, nil) if err != nil { log.Errf(c, "Etcd Watch failed: %s", err) time.Sleep(50 * time.Millisecond) continue } if response.Node == nil { log.Infof(c, "Unexpected Etcd message: %v", response) } git := exec.Command("/home/git/check-repos") if out, err := git.CombinedOutput(); err != nil { log.Errf(c, "Failed git check-repos: %s", err) log.Infof(c, "Output: %s", out) } } }) return nil, nil }
// ParseHostKeys parses the host key files. // // By default it looks in /etc/ssh for host keys of the patterh ssh_host_{{TYPE}}_key. // // Params: // - keytypes ([]string): Key types to parse. Defaults to []string{rsa, dsa, ecdsa} // - enableV1 (bool): Allow V1 keys. By default this is disabled. // - path (string): Override the lookup pattern. If %s, it will be replaced with the keytype. // // Returns: // []ssh.Signer func ParseHostKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { log.Debugf(c, "Parsing ssh host keys") hostKeyTypes := p.Get("keytypes", []string{"rsa", "dsa", "ecdsa"}).([]string) pathTpl := p.Get("path", "/etc/ssh/ssh_host_%s_key").(string) hostKeys := make([]ssh.Signer, 0, len(hostKeyTypes)) for _, t := range hostKeyTypes { path := fmt.Sprintf(pathTpl, t) if key, err := ioutil.ReadFile(path); err == nil { if hk, err := ssh.ParsePrivateKey(key); err == nil { log.Infof(c, "Parsed host key %s.", path) hostKeys = append(hostKeys, hk) } else { log.Errf(c, "Failed to parse host key %s (skipping): %s", path, err) } } } if c.Get("enableV1", false).(bool) { path := "/etc/ssh/ssh_host_key" if key, err := ioutil.ReadFile(path); err != nil { log.Errf(c, "Failed to read ssh_host_key") } else if hk, err := ssh.ParsePrivateKey(key); err == nil { log.Infof(c, "Parsed host key %s.", path) hostKeys = append(hostKeys, hk) } else { log.Errf(c, "Failed to parse host key %s: %s", path, err) } } return hostKeys, nil }
// FindSSHUser finds an SSH user by public key. // // Some parts of the system require that we know not only the SSH key, but also // the name of the user. That information is stored in etcd. // // Params: // - client (EtcdGetter) // - fingerprint (string): The fingerprint of the SSH key. // // Returns: // - username (string) func FindSSHUser(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { client := p.Get("client", nil).(Getter) fingerprint := p.Get("fingerprint", nil).(string) res, err := client.Get("/deis/builder/users", false, true) if err != nil { log.Warnf(c, "Error querying etcd: %s", err) return "", err } else if res.Node == nil || !res.Node.Dir { log.Warnf(c, "No users found in etcd.") return "", errors.New("Users not found") } for _, user := range res.Node.Nodes { log.Infof(c, "Checking user %s", user.Key) for _, keyprint := range user.Nodes { if strings.HasSuffix(keyprint.Key, fingerprint) { parts := strings.Split(user.Key, "/") username := parts[len(parts)-1] log.Infof(c, "Found user %s for fingerprint %s", username, fingerprint) return username, nil } } } return "", fmt.Errorf("User not found for fingerprint %s", fingerprint) }
// Serve starts a native SSH server. // // The general design of the server is that it acts as a main server for // a Cookoo app. It assumes that certain things have been configured for it, // like an ssh.ServerConfig. Once it runs, it will block until the main // process terminates. If you want to stop it prior to that, you can grab // the closer ("sshd.Closer") out of the context and send it a signal. // // Currently, the service is not generic. It only runs git hooks. // // This expects the following Context variables. // - ssh.Hostkeys ([]ssh.Signer): Host key, as an unparsed byte slice. // - ssh.Address (string): Address/port // - ssh.ServerConfig (*ssh.ServerConfig): The server config to use. // // This puts the following variables into the context: // - ssh.Closer (chan interface{}): Send a message to this to shutdown the server. func Serve(reg *cookoo.Registry, router *cookoo.Router, c cookoo.Context) cookoo.Interrupt { hostkeys := c.Get(HostKeys, []ssh.Signer{}).([]ssh.Signer) addr := c.Get(Address, "0.0.0.0:2223").(string) cfg := c.Get(ServerConfig, &ssh.ServerConfig{}).(*ssh.ServerConfig) for _, hk := range hostkeys { cfg.AddHostKey(hk) log.Infof(c, "Added hostkey.") } listener, err := net.Listen("tcp", addr) if err != nil { return err } srv := &server{ c: c, gitHome: "/home/git", } closer := make(chan interface{}, 1) c.Put("sshd.Closer", closer) log.Infof(c, "Listening on %s", addr) srv.listen(listener, cfg, closer) return nil }
// Receive receives a Git repo. // This will only work for git-receive-pack. // // Params: // - operation (string): e.g. git-receive-pack // - repoName (string): The repository name, in the form '/REPO.git'. // - channel (ssh.Channel): The channel. // - request (*ssh.Request): The channel. // - gitHome (string): Defaults to /home/git. // - fingerprint (string): The fingerprint of the user's SSH key. // - user (string): The name of the Deis user. // // Returns: // - nothing func Receive(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { if ok, z := p.Requires("channel", "request", "fingerprint", "permissions"); !ok { return nil, fmt.Errorf("Missing requirements %q", z) } repoName := p.Get("repoName", "").(string) operation := p.Get("operation", "").(string) channel := p.Get("channel", nil).(ssh.Channel) gitHome := p.Get("gitHome", "/home/git").(string) fingerprint := p.Get("fingerprint", nil).(string) user := p.Get("user", "").(string) repo, err := cleanRepoName(repoName) if err != nil { log.Warnf(c, "Illegal repo name: %s.", err) channel.Stderr().Write([]byte("No repo given")) return nil, err } repo += ".git" if _, err := createRepo(c, filepath.Join(gitHome, repo), gitHome); err != nil { log.Infof(c, "Did not create new repo: %s", err) } cmd := exec.Command("git-shell", "-c", fmt.Sprintf("%s '%s'", operation, repo)) log.Infof(c, strings.Join(cmd.Args, " ")) var errbuff bytes.Buffer cmd.Dir = gitHome cmd.Env = []string{ fmt.Sprintf("RECEIVE_USER=%s", user), fmt.Sprintf("RECEIVE_REPO=%s", repo), fmt.Sprintf("RECEIVE_FINGERPRINT=%s", fingerprint), fmt.Sprintf("SSH_ORIGINAL_COMMAND=%s '%s'", operation, repo), fmt.Sprintf("SSH_CONNECTION=%s", c.Get("SSH_CONNECTION", "0 0 0 0").(string)), } cmd.Env = append(cmd.Env, os.Environ()...) done := plumbCommand(cmd, channel, &errbuff) if err := cmd.Start(); err != nil { log.Warnf(c, "Failed git receive immediately: %s %s", err, errbuff.Bytes()) return nil, err } fmt.Printf("Waiting for git-receive to run.\n") done.Wait() fmt.Printf("Waiting for deploy.\n") if err := cmd.Wait(); err != nil { log.Errf(c, "Error on command: %s %s", err, errbuff.Bytes()) return nil, err } if errbuff.Len() > 0 { log.Warnf(c, "Unreported error: %s", errbuff.Bytes()) } log.Infof(c, "Deploy complete.\n") return nil, nil }
// handleConn handles an individual client connection. // // It manages the connection, but passes channels on to `answer()`. func (s *server) handleConn(conn net.Conn, conf *ssh.ServerConfig) { defer conn.Close() log.Info(s.c, "Accepted connection.") _, chans, reqs, err := ssh.NewServerConn(conn, conf) if err != nil { // Handshake failure. log.Errf(s.c, "Failed handshake: %s (%v)", err, conn) return } // Discard global requests. We're only concerned with channels. safely.GoDo(s.c, func() { ssh.DiscardRequests(reqs) }) condata := sshConnection(conn) // Now we handle the channels. for incoming := range chans { log.Infof(s.c, "Channel type: %s\n", incoming.ChannelType()) if incoming.ChannelType() != "session" { incoming.Reject(ssh.UnknownChannelType, "Unknown channel type") } channel, req, err := incoming.Accept() if err != nil { // Should close request and move on. panic(err) } safely.GoDo(s.c, func() { s.answer(channel, req, condata) }) } conn.Close() }
// RemoveMemberByName removes a member whose name matches the given. // // Params: // - client(client.Client): An etcd client // - name (string): The name to remove // Returns: // true if the member was found, false otherwise. func RemoveMemberByName(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { cli := p.Get("client", nil).(client.Client) name := p.Get("name", "____").(string) mem := client.NewMembersAPI(cli) members, err := mem.List(dctx()) if err != nil { log.Errf(c, "Could not get a list of members: %s", err) return false, err } remIDs := []string{} for _, member := range members { if member.Name == name { log.Infof(c, "Removing member %s (ID: %s)", name, member.ID) // If this is synchronizable, we should do it in parallel. if err := mem.Remove(dctx(), member.ID); err != nil { log.Errf(c, "Failed to remove member: %s", err) return len(remIDs) > 0, err } remIDs = append(remIDs, member.ID) } } return len(remIDs) > 0, nil }
// ParallelBuild runs multiple docker builds at the same time. // // Params: // -images ([]BuildImg): Images to build // -alwaysFetch (bool): Default false. If set to true, this will always fetch // the Docker image even if it already exists in the registry. // // Returns: // // - Waiter: A *sync.WaitGroup that is waiting for the docker downloads to finish. // // Context: // // This puts 'ParallelBuild.failN" (int) into the context to indicate how many failures // occurred during fetches. func ParallelBuild(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { images := p.Get("images", []BuildImg{}).([]BuildImg) var wg sync.WaitGroup var m sync.Mutex var fails int for _, img := range images { img := img wg.Add(1) safely.GoDo(c, func() { log.Infof(c, "Starting build for %s (tag: %s)", img.Path, img.Tag) if _, err := buildImg(c, img.Path, img.Tag); err != nil { log.Errf(c, "Failed to build docker image: %s", err) m.Lock() fails++ m.Unlock() } wg.Done() }) } // Number of failures. c.Put("ParallelBuild.failN", fails) return &wg, nil }
// ParallelBuild runs multiple docker builds at the same time. // // Params: // -images ([]BuildImg): Images to build // -alwaysFetch (bool): Default false. If set to true, this will always fetch // the Docker image even if it already exists in the registry. // // Returns: // // - Waiter: A *sync.WaitGroup that is waiting for the docker downloads to finish. // // Context: // // This puts 'ParallelBuild.failN" (int) into the context to indicate how many failures // occurred during fetches. func ParallelBuild(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { images := p.Get("images", []BuildImg{}).([]BuildImg) var wg sync.WaitGroup var m sync.Mutex var fails int for _, img := range images { img := img // HACK: ensure "docker build" is serialized by allowing only one entry in // the WaitGroup. This works around the "simultaneous docker pull" bug. wg.Wait() wg.Add(1) safely.GoDo(c, func() { log.Infof(c, "Starting build for %s (tag: %s)", img.Path, img.Tag) if _, err := buildImg(c, img.Path, img.Tag); err != nil { log.Errf(c, "Failed to build docker image: %s", err) m.Lock() fails++ m.Unlock() } wg.Done() }) } // Number of failures. c.Put("ParallelBuild.failN", fails) return &wg, nil }
// keysToEtcd copies local keys into etcd. // // It only fails if it cannot copy ssh_host_key to sshHostKey. All other // abnormal conditions are logged, but not considered to be failures. func keysToEtcd(c cookoo.Context, client Setter, ciphers []string, etcdPath string) error { firstpath := "" lpath := "/etc/ssh/ssh_host_%s_key" privkey := "%s/sshHost%sKey" for _, cipher := range ciphers { path := fmt.Sprintf(lpath, cipher) key := fmt.Sprintf(privkey, etcdPath, cipher) content, err := ioutil.ReadFile(path) if err != nil { log.Infof(c, "No key named %s", path) } else if _, err := client.Set(key, string(content), 0); err != nil { log.Errf(c, "Could not store ssh key in etcd: %s", err) } // Remember the first key's path in case the generic key is missing if firstpath == "" { firstpath = path } } // Now we set the generic key: keypath := "/etc/ssh/ssh_host_key" if _, err := os.Stat(keypath); os.IsNotExist(err) && firstpath != "" { // Use ssh_host_dsa_key if newer ssh-keygen didn't create ssh_host_key keypath = firstpath } if content, err := ioutil.ReadFile(keypath); err != nil { log.Errf(c, "Could not read the %s file.", keypath) return err } else if _, err := client.Set("sshHostKey", string(content), 0); err != nil { log.Errf(c, "Failed to set sshHostKey in etcd.") return err } return nil }
// BuildImage builds a docker image. // // Essentially, this executes: // docker build -t TAG PATH // // Params: // - path (string): The path to the image. REQUIRED // - tag (string): The tag to build. func BuildImage(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { path := p.Get("path", "").(string) tag := p.Get("tag", "").(string) log.Infof(c, "Building docker image %s (tag: %s)", path, tag) return buildImg(c, path, tag) }
// Cleanup removes any existing Docker artifacts. // // Returns true if the file exists (and was deleted), or false if no file // was deleted. func Cleanup(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { // If info is returned, then the file is there. If we get an error, we're // pretty much not going to be able to remove the file (which probably // doesn't exist). if _, err := os.Stat(DockSock); err == nil { log.Infof(c, "Removing leftover docker socket %s", DockSock) return true, os.Remove(DockSock) } return false, nil }
// GenSSHKeys generates the default set of SSH host keys. func GenSSHKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { log.Debugf(c, "Generating ssh keys for sshd") // Generate a new key out, err := exec.Command("ssh-keygen", "-A").CombinedOutput() if err != nil { log.Infof(c, "ssh-keygen: %s", out) return nil, err } return nil, nil }
// genSshKeys generates the default set of SSH host keys. func genSSHKeys(c cookoo.Context) error { // Generate a new key out, err := exec.Command("ssh-keygen", "-A").CombinedOutput() if err != nil { log.Infof(c, "ssh-keygen: %s", out) log.Errf(c, "Failed to generate SSH keys: %s", err) return err } return nil }
// AddRelease adds a release to a package. // // Params: // - rel (*model.Release) // - pkg (*model.Package) // Returns: // - pkg func AddRelease(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { pkg := p.Get("pkg", nil).(*model.Package) //rel := p.Get("rel", nil).(*model.Release) //rels := append([]*model.Release{rel}, pkg.Releases...) //pkg.Releases = rels log.Infof(c, "Updating %s", pkg.Name) err := ds(c).C("packages").Update(bson.M{"name": pkg.Name}, pkg) return pkg, err }
// Start starts a Docker daemon. // // This assumes the presence of the docker client on the host. It does not use // the API. func Start(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { // Allow insecure Docker registries on all private network ranges in RFC 1918 and RFC 6598. dargs := []string{ "-d", "--bip=172.19.42.1/16", "--insecure-registry", "10.0.0.0/8", "--insecure-registry", "172.16.0.0/12", "--insecure-registry", "192.168.0.0/16", "--insecure-registry", "100.64.0.0/10", "--exec-opt", "native.cgroupdriver=cgroupfs", } // For overlay-ish filesystems, force the overlay to kick in if it exists. // Then we can check the fstype. if err := os.MkdirAll("/", 0700); err == nil { cmd := exec.Command("findmnt", "--noheadings", "--output", "FSTYPE", "--target", "/") if out, err := cmd.Output(); err == nil && strings.TrimSpace(string(out)) == "overlay" { dargs = append(dargs, "--storage-driver=overlay") } else { log.Infof(c, "File system type: '%s' (%v)", out, err) } } log.Infof(c, "Starting docker with %s", strings.Join(dargs, " ")) cmd := exec.Command("docker", dargs...) if err := cmd.Start(); err != nil { log.Errf(c, "Failed to start Docker. %s", err) return -1, err } // Get the PID and return it. return cmd.Process.Pid, nil }
// AuthKey authenticates based on a public key. // // Params: // - metadata (ssh.ConnMetadata) // - key (ssh.PublicKey) // - authorizedKeys ([]string): List of lines from an authorized keys file. // // Returns: // *ssh.Permissions // func AuthKey(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { meta := p.Get("metadata", nil).(ssh.ConnMetadata) key := p.Get("key", nil).(ssh.PublicKey) authorized := p.Get("authorizedKeys", []string{}).([]string) auth := new(ssh.CertChecker) auth.UserKeyFallback = func(meta ssh.ConnMetadata, pk ssh.PublicKey) (*ssh.Permissions, error) { // This gives us a string in the form "ssh-rsa LONG_KEY" suppliedType := key.Type() supplied := key.Marshal() for _, allowedKey := range authorized { allowed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(allowedKey)) if err != nil { log.Infof(c, "Could not parse authorized key '%q': %s", allowedKey, err) continue } // We use a contstant time compare more as a precaution than anything // else. A timing attack here would be very difficult, but... better // safe than sorry. if allowed.Type() == suppliedType && subtle.ConstantTimeCompare(allowed.Marshal(), supplied) == 1 { log.Infof(c, "Key accepted for user %s.", meta.User()) perm := &ssh.Permissions{ Extensions: map[string]string{ "user": meta.User(), }, } return perm, nil } } return nil, fmt.Errorf("No matching keys found.") } return auth.Authenticate(meta, key) }
func buildImg(c cookoo.Context, path, tag string) ([]byte, error) { dargs := []string{"build"} if len(tag) > 0 { dargs = append(dargs, "-t", tag) } dargs = append(dargs, path) out, err := exec.Command("docker", dargs...).CombinedOutput() if len(out) > 0 { log.Infof(c, "Docker: %s", out) } return out, err }
// StoreHostKeys stores SSH hostkeys locally. // // First it tries to fetch them from etcd. If the keys are not present there, // it generates new ones and then puts them into etcd. // // Params: // - client(EtcdGetterSetter) // - ciphers([]string): A list of ciphers to generate. Defaults are dsa, // ecdsa, ed25519 and rsa. // - basepath (string): Base path in etcd (ETCD_PATH). // Returns: // func StoreHostKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { defaultCiphers := []string{"rsa", "dsa", "ecdsa", "ed25519"} client := p.Get("client", nil).(GetterSetter) ciphers := p.Get("ciphers", defaultCiphers).([]string) basepath := p.Get("basepath", "/deis/builder").(string) res, err := client.Get("sshHostKey", false, false) if err != nil || res.Node == nil { log.Infof(c, "Could not get SSH host key from etcd. Generating new ones.") if err := genSSHKeys(c); err != nil { log.Err(c, "Failed to generate SSH keys. Aborting.") return nil, err } if err := keysToEtcd(c, client, ciphers, basepath); err != nil { return nil, err } } else if err := keysToLocal(c, client, ciphers, basepath); err != nil { log.Infof(c, "Fetching SSH host keys from etcd.") return nil, err } return nil, nil }
// Set sets a value in etcd. // // Params: // - key (string): The key // - value (string): The value // - ttl (uint64): Time to live // - client (EtcdGetter): Client, usually an *etcd.Client. // // Returns: // - *etcd.Result func Set(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { key := p.Get("key", "").(string) value := p.Get("value", "").(string) ttl := p.Get("ttl", uint64(20)).(uint64) client := p.Get("client", nil).(Setter) res, err := client.Set(key, value, ttl) if err != nil { log.Infof(c, "Failed to set %s=%s", key, value) return res, err } return res, nil }
// IsRunning checks to see if etcd is running. // // It will test `count` times before giving up. // // Params: // - client (EtcdGetter) // - count (int): Number of times to try before giving up. // // Returns: // boolean true if etcd is listening. func IsRunning(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { client := p.Get("client", nil).(Getter) count := p.Get("count", 20).(int) for i := 0; i < count; i++ { _, err := client.Get("/", false, false) if err == nil { return true, nil } log.Infof(c, "Waiting for etcd to come online.") time.Sleep(250 * time.Millisecond) } log.Errf(c, "Etcd is not answering after %d attempts.", count) return false, &cookoo.FatalError{"Could not connect to Etcd."} }
// Set sets a value in etcd. // // Params: // - key (string): The key // - value (string): The value // - ttl (uint64): Time to live // - client (EtcdGetter): Client, usually an *etcd.Client. // // Returns: // - *etcd.Result func Set(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { key := p.Get("key", "").(string) value := p.Get("value", "").(string) ttl := p.Get("ttl", uint64(20)).(uint64) cli := p.Get("client", nil).(client.Client) k := client.NewKeysAPI(cli) res, err := k.Set(dctx(), key, value, &client.SetOptions{TTL: time.Second * time.Duration(ttl)}) if err != nil { log.Infof(c, "Failed to set %s=%s", key, value) return res, err } return res, nil }
// AddMember Add a new member to the cluster. // // Conceptually, this is equivalent to `etcdctl member add NAME IP`. // // Params: // - client(client.Client): An etcd client // - name (string): The name of the member to add. // - url (string): The peer ip:port or domain: port to use. // // Returns: // An etcd *client.Member. func AddMember(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { cli := p.Get("client", nil).(client.Client) name := p.Get("name", "default").(string) addr := p.Get("url", "127.0.0.1:2380").(string) mem := client.NewMembersAPI(cli) member, err := mem.Add(dctx(), addr) if err != nil { log.Errf(c, "Failed to add %s to cluster: %s", addr, err) return nil, err } log.Infof(c, "Added %s (%s) to cluster", addr, member.ID) member.Name = name return member, nil }
// AuthKey authenticates based on a public key. // // Params: // - metadata (ssh.ConnMetadata) // - key (ssh.PublicKey) // // Returns: // *ssh.Permissions // func AuthKey(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { log.Debugf(c, "Starting ssh authentication") key := p.Get("key", nil).(ssh.PublicKey) userInfo, err := controller.UserInfoFromKey(key) if err != nil { return nil, err } userInfo.Key = string(ssh.MarshalAuthorizedKey(key)) c.Put("userinfo", userInfo) log.Infof(c, "Key accepted for user %s.", userInfo.Username) perm := &ssh.Permissions{ Extensions: map[string]string{ "user": userInfo.Username, }, } return perm, nil }
// UpdateHostPort intermittently notifies etcd of the builder's address. // // If `port` is specified, this will notify etcd at 10 second intervals that // the builder is listening at $HOST:$PORT, setting the TTL to 20 seconds. // // This will notify etcd as long as the local sshd is running. // // Params: // - base (string): The base path to write the data: $base/host and $base/port. // - host (string): The hostname // - port (string): The port // - client (Setter): The client to use to write the data to etcd. // - sshPid (int): The PID for SSHD. If SSHD dies, this stops notifying. func UpdateHostPort(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { base := p.Get("base", "").(string) host := p.Get("host", "").(string) port := p.Get("port", "").(string) client := p.Get("client", nil).(Setter) sshd := p.Get("sshdPid", 0).(int) // If no port is specified, we don't do anything. if len(port) == 0 { log.Infof(c, "No external port provided. Not publishing details.") return false, nil } var ttl uint64 = 20 if err := setHostPort(client, base, host, port, ttl); err != nil { log.Errf(c, "Etcd error setting host/port: %s", err) return false, err } // Update etcd every ten seconds with this builder's host/port. safely.GoDo(c, func() { ticker := time.NewTicker(10 * time.Second) for range ticker.C { //log.Infof(c, "Setting SSHD host/port") if _, err := os.FindProcess(sshd); err != nil { log.Errf(c, "Lost SSHd process: %s", err) break } else { if err := setHostPort(client, base, host, port, ttl); err != nil { log.Errf(c, "Etcd error setting host/port: %s", err) break } } } ticker.Stop() }) return true, nil }
// Run starts confd and runs it in the background. // // If the command fails immediately on startup, an error is immediately // returned. But from that point, a goroutine watches the command and // reports if the command dies. // // Params: // - node (string): The etcd node to use. (Only etcd is currently supported) // - interval (int, default:5): The rebuilding interval. // // Returns // bool true if this succeeded. func Run(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { node := p.Get("node", defaultEtcd).(string) interval := strconv.Itoa(p.Get("interval", 5).(int)) cmd := exec.Command("confd", "-log-level", "error", "-node", node, "-interval", interval) if err := cmd.Start(); err != nil { return false, err } log.Infof(c, "Watching confd.") safely.Go(func() { if err := cmd.Wait(); err != nil { // If confd exits, builder will stop functioning as intended. So // we stop builder and let the environment restart. log.Errf(c, "Stopping builder. confd exited with error: %s", err) os.Exit(37) } }) return true, nil }