// 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) }
// 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() if fi, err := os.Stat(repoPath); err == nil { if fi.IsDir() { configPath := filepath.Join(repoPath, "config") if _, cerr := os.Stat(configPath); cerr == nil { log.Debugf(c, "Directory '%s' already exists.", repoPath) return true, nil } else { log.Warnf(c, "No config file found at `%s`; removing it and recreating.", repoPath) if err := os.RemoveAll(repoPath); err != nil { return false, fmt.Errorf("Unable to remove path '%s': %s", repoPath, err) } } } else { log.Warnf(c, "Path '%s' is not a directory; removing it and recreating.", repoPath) if err := os.RemoveAll(repoPath); err != nil { return false, fmt.Errorf("Unable to remove path '%s': %s", repoPath, err) } } } else if os.IsNotExist(err) { log.Debugf(c, "Unable to get stat for path '%s': %s .", repoPath, err) } else { return false, err } return initRepo(repoPath, gitHome, c) }
// 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 }
// 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.Debugf(s.c, "Failed handshake: %s", err) 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() }
// 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 }
// createPreReceiveHook renders preReceiveHookTpl to repoPath/hooks/pre-receive func createPreReceiveHook(c cookoo.Context, gitHome, repoPath string) error { // parse & generate the template anew each receive for each new git home var hookByteBuf bytes.Buffer if err := preReceiveHookTpl.Execute(&hookByteBuf, map[string]string{"GitHome": gitHome}); err != nil { return err } writePath := filepath.Join(repoPath, "hooks", "pre-receive") log.Debugf(c, "Writing pre-receive hook to %s", writePath) if err := ioutil.WriteFile(writePath, hookByteBuf.Bytes(), 0755); err != nil { return fmt.Errorf("Cannot write pre-receive hook to %s (%s)", writePath, err) } return nil }
// Get gets one or more environment variables and puts them into the context. // // Parameters passed in are of the form varname => defaultValue. // // r.Route("foo", "example").Does(envvar.Get).Using("HOME").WithDefault(".") // // As with all environment variables, the default value must be a string. // // WARNING: Since parameters are a map, order of processing is not // guaranteed. If order is important, you'll need to call this command // multiple times. // // For each parameter (`Using` clause), this command will look into the // environment for a matching variable. If it finds one, it will add that // variable to the context. If it does not find one, it will expand the // default value (so you can set a default to something like "$HOST:$PORT") // and also put the (unexpanded) default value back into the context in case // any subsequent call to `os.Getenv` occurs. func Get(c cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { for name, def := range params.AsMap() { var val string if val = os.Getenv(name); len(val) == 0 { def := def.(string) val = os.ExpandEnv(def) // We want to make sure that any subsequent calls to Getenv // return the same default. os.Setenv(name, val) } c.Put(name, val) log.Debugf(c, "Name: %s, Val: %s", name, val) } return true, nil }
// Set takes the given names and values and puts them into both the context // and the environment. // // Unlike Get, it does not try to retrieve the values from the environment // first. // // Values are passed through os.ExpandEnv() // // There is no guarantee of insertion order. If multiple name/value pairs // are given, they will be put into the context in whatever order they // are retrieved from the underlying map. // // Params: // accessed as map[string]string // Returns: // nothing, but inserts all name/value pairs into the context and the // environment. func Set(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { for name, def := range p.AsMap() { // Assume Nil means unset the value. if def == nil { def = "" } val := fmt.Sprintf("%v", def) val = os.ExpandEnv(val) log.Debugf(c, "Name: %s, Val: %s", name, val) os.Setenv(name, val) c.Put(name, val) } return true, 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 }
// 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) allowedkey, _ := ioutil.ReadFile("/etc/deistest.pub") allowed, _, _, _, err := ssh.ParseAuthorizedKey(allowedkey) fmt.Println(err) fmt.Println(allowed) fmt.Println(key) if compareKeys(key, allowed) { perm := &ssh.Permissions{ Extensions: map[string]string{ "user": "******", }, } return perm, nil } return nil, nil }
// Get gets one or more environment variables and puts them into the context. // // Parameters passed in are of the form varname => defaultValue. // // r.Route("foo", "example").Does(envvar.Get).Using("HOME").WithDefault(".") // // As with all environment variables, the default value must be a string. // // WARNING: Since parameters are a map, order of processing is not // guaranteed. If order is important, you'll need to call this command // multiple times. // // For each parameter (`Using` clause), this command will look into the // environment for a matching variable. If it finds one, it will add that // variable to the context. If it does not find one, it will expand the // default value (so you can set a default to something like "$HOST:$PORT") // and also put the (unexpanded) default value back into the context in case // any subsequent call to `os.Getenv` occurs. func Get(c cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { for name, def := range params.AsMap() { var val string if val = os.Getenv(name); len(val) == 0 { if def == nil { def = "" } def, ok := def.(string) if !ok { log.Warnf(c, "Could not convert %s. Type is %T", name, def) } val = os.ExpandEnv(def) // We want to make sure that any subsequent calls to Getenv // return the same default. os.Setenv(name, val) } c.Put(name, val) log.Debugf(c, "Name: %s, Val: %s", name, val) } return true, nil }
// answer handles answering requests and channel requests // // Currently, an exec must be either "ping", "git-receive-pack" or // "git-upload-pack". Anything else will result in a failure response. Right // now, we leave the channel open on failure because it is unclear what the // correct behavior for a failed exec is. // // Support for setting environment variables via `env` has been disabled. func (s *server) answer(channel ssh.Channel, requests <-chan *ssh.Request, sshConn string) error { defer channel.Close() // Answer all the requests on this connection. for req := range requests { ok := false // I think that ideally what we want to do here is pass this on to // the Cookoo router and let it handle each Type on its own. switch req.Type { case "env": o := &EnvVar{} ssh.Unmarshal(req.Payload, o) log.Debugf(s.c, "Key='%s', Value='%s'\n", o.Name, o.Value) req.Reply(true, nil) case "exec": clean := cleanExec(req.Payload) parts := strings.SplitN(clean, " ", 2) router := s.c.Get("cookoo.Router", nil).(*cookoo.Router) // TODO: Should we unset the context value 'cookoo.Router'? // We need a shallow copy of the context to avoid race conditions. cxt := s.c.Copy() cxt.Put("SSH_CONNECTION", sshConn) // Only allow commands that we know about. switch parts[0] { case "ping": cxt.Put("channel", channel) cxt.Put("request", req) sshPing := cxt.Get("route.sshd.sshPing", "sshPing").(string) err := router.HandleRequest(sshPing, cxt, true) if err != nil { log.Warnf(s.c, "Error pinging: %s", err) } return err case "git-receive-pack", "git-upload-pack": if len(parts) < 2 { log.Warn(s.c, "Expected two-part command.\n") req.Reply(ok, nil) break } req.Reply(true, nil) // We processed. Yay. cxt.Put("channel", channel) cxt.Put("request", req) cxt.Put("operation", parts[0]) cxt.Put("repository", parts[1]) sshGitReceive := cxt.Get("route.sshd.sshGitReceive", "sshGitReceive").(string) err := router.HandleRequest(sshGitReceive, cxt, true) var xs uint32 if err != nil { log.Errf(s.c, "Failed git receive: %v", err) xs = 1 } sendExitStatus(xs, channel) return nil default: log.Warnf(s.c, "Illegal command is '%s'\n", clean) req.Reply(false, nil) return nil } if err := sendExitStatus(0, channel); err != nil { log.Errf(s.c, "Failed to write exit status: %s", err) } return nil default: // We simply ignore all of the other cases and leave the // channel open to take additional requests. log.Infof(s.c, "Received request of type %s\n", req.Type) req.Reply(false, nil) } } 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) log.Debugf(c, "receiving git repo name: %s, operation: %s, fingerprint: %s, user: %s", repoName, operation, fingerprint, user) 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" repoPath := filepath.Join(gitHome, repo) log.Debugf(c, "creating repo directory %s", repoPath) if _, err := createRepo(c, repoPath); err != nil { err = fmt.Errorf("Did not create new repo (%s)", err) log.Warnf(c, err.Error()) return nil, err } log.Debugf(c, "writing pre-receive hook under %s", repoPath) if err := createPreReceiveHook(c, gitHome, repoPath); err != nil { err = fmt.Errorf("Did not write pre-receive hook (%s)", err) log.Warnf(c, err.Error()) return nil, 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()...) log.Debugf(c, "Working Dir: %s", cmd.Dir) log.Debugf(c, "Environment: %s", strings.Join(cmd.Env, ",")) inpipe, err := cmd.StdinPipe() if err != nil { return nil, err } cmd.Stdout = channel cmd.Stderr = io.MultiWriter(channel.Stderr(), &errbuff) if err := cmd.Start(); err != nil { err = fmt.Errorf("Failed to start git pre-receive hook: %s (%s)", err, errbuff.Bytes()) log.Warnf(c, err.Error()) return nil, err } if _, err := io.Copy(inpipe, channel); err != nil { err = fmt.Errorf("Failed to write git objects into the git pre-receive hook (%s)", err) log.Warnf(c, err.Error()) return nil, err } fmt.Println("Waiting for git-receive to run.") fmt.Println("Waiting for deploy.") if err := cmd.Wait(); err != nil { err = fmt.Errorf("Failed to run git pre-receive hook: %s (%s)", errbuff.Bytes(), err) log.Errf(c, err.Error()) return nil, err } if errbuff.Len() > 0 { log.Warnf(c, "Unreported error: %s", errbuff.Bytes()) } log.Infof(c, "Deploy complete.\n") return nil, nil }