// Serve creates a new Cookoo web server. // // Important details: // // - A URIPathResolver is used for resolving request names. // - The following datasources are added to the Context: // * url: A URLDatasource (Provides access to parts of the URL) // * path: A PathDatasource (Provides access to parts of a path. E.g. "/foo/bar") // * query: A QueryParameterDatasource (Provides access to URL query parameters.) // * post: A FormValuesDatasource (Provides access to form data or the body of a request.) // - The following context variables are set: // * http.Request: A pointer to the http.Request object // * http.ResponseWriter: The response writer. // * server.Address: The server's address and port (NOT ALWAYS PRESENT) // - The handler includes logic to redirect "not found" errors to a path named "@404" if present. // // Context Params: // // - server.Address: If this key exists in the context, it will be used to determine the host/port the // server runes on. EXPERIMENTAL. Default is ":8080". // // Example: // // package main // // import ( // //This is the path to Cookoo // "github.com/Masterminds/cookoo" // "github.com/Masterminds/cookoo/web" // "fmt" // ) // // func main() { // // Build a new Cookoo app. // registry, router, context := cookoo.Cookoo() // // // Fill the registry. // registry.Route("GET /", "The index").Does(web.Flush, "example"). // Using("content").WithDefault("Hello World") // // // Create a server // web.Serve(reg, router, cookoo.SyncContext(cxt)) // } // // Note that we synchronize the context before passing it into Serve(). This // is optional because each handler gets its own copy of the context already. // However, if commands pass the context to goroutines, the context ought to be // synchronized to avoid race conditions. // // Note that copies of the context are not synchronized with each other. // So by declaring the context synchronized here, you // are not therefore synchronizing across handlers. func Serve(reg *cookoo.Registry, router *cookoo.Router, cxt cookoo.Context) { addr := cxt.Get("server.Address", ":8080").(string) handler := NewCookooHandler(reg, router, cxt) // MPB: I dont think there's any real point in having a multiplexer in // this particular case. The Cookoo handler is mux enough. // // Note that we can always use Cookoo with the built-in multiplexer. It // just doesn't make sense if Cookoo's the only handler on the app. //http.Handle("/", handler) server := &http.Server{Addr: addr} // Instead of mux, set a single default handler. // What we might be losing: // - Handling of non-conforming paths. server.Handler = handler go handleSignals(router, cxt, server) err := server.ListenAndServe() //err := http.ListenAndServe(addr, nil) if err != nil { cxt.Logf("error", "Caught error while serving: %s", err) if router.HasRoute("@crash") { router.HandleRequest("@crash", cxt, false) } } }
/** * Perform authentication. * * Params: * - realm (string): The name of the realm. (Default: "web") * - datasource (string): The name of the datasource that should be used to authenticate. * This datasource must be an `auth.UserDatasource`. (Default: "auth.UserDatasource") * * Context: * - http.Request (*http.Request): The HTTP request. This is usually placed into the * context for you. * - http.ResponseWriter (http.ResponseWriter): The response. This is usually placed * into the context for you. * * Datasource: * - An auth.UserDatasource. By default, this will look for a datasource named * "auth.UserDatasource". This can be overridden by the `datasource` param. * * Returns: * - True if the user authenticated. If not, this will send a 401 and then stop * the current chain. */ func Basic(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { realm := p.Get("realm", "web").(string) dsName := p.Get("datasource", "auth.UserDatasource").(string) req := c.Get("http.Request", nil).(*http.Request) res := c.Get("http.ResponseWriter", nil).(http.ResponseWriter) ds := c.Datasource(dsName).(UserDatasource) authz := strings.TrimSpace(req.Header.Get("Authorization")) if len(authz) == 0 || !strings.Contains(authz, "Basic ") { return sendUnauthorized(realm, res) } user, pass, err := parseBasicString(authz) if err != nil { c.Logf("info", "Basic authentication parsing failed: %s", err) return sendUnauthorized(realm, res) } ok, err := ds.AuthUser(user, pass) if !ok { if err != nil { c.Logf("info", "Basic authentication caused an error: %s", err) } return sendUnauthorized(realm, res) } return ok, err }
// 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 }
// Subscribe allows an request to subscribe to topic updates. // // Params: // - topic (string): The topic to subscribe to. // - // // Returns: // func Subscribe(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { medium, err := getMedium(c) if err != nil { return nil, &cookoo.FatalError{"No medium."} } topic := p.Get("topic", "").(string) if len(topic) == 0 { return nil, errors.New("No topic is set.") } rw := c.Get("http.ResponseWriter", nil).(ResponseWriterFlusher) clientGone := rw.(http.CloseNotifier).CloseNotify() sub := NewSubscription(rw) t := fetchOrCreateTopic(medium, topic, true, DefaultMaxHistory) t.Subscribe(sub) defer func() { t.Unsubscribe(sub) sub.Close() }() sub.Listen(clientGone) return nil, nil }
// ReplayHistory sends back the history to a subscriber. // // This should be called before the client goes into active listening. // // Params: // - topic (string): The topic to fetch. // // Returns: // - int: The number of history messages sent to the client. func ReplayHistory(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { req := c.Get("http.Request", nil).(*http.Request) res := c.Get("http.ResponseWriter", nil).(ResponseWriterFlusher) medium, _ := getMedium(c) name := p.Get("topic", "").(string) // This does not manage topics. If there is no topic set, we silently fail. if len(name) == 0 { c.Log("info", "No topic name given to ReplayHistory.") return 0, nil } top, ok := medium.Topic(name) if !ok { c.Logf("info", "No topic named %s exists yet. No history replayed.", name) return 0, nil } topic, ok := top.(HistoriedTopic) if !ok { c.Logf("info", "No history for topic %s.", name) res.Header().Add(XHistoryEnabled, "False") return 0, nil } res.Header().Add(XHistoryEnabled, "True") since := req.Header.Get(XHistorySince) max := req.Header.Get(XHistoryLength) // maxLen can be used either on its own or paired with X-History-Since. maxLen := 0 if len(max) > 0 { m, err := parseHistLen(max) if err != nil { c.Logf("info", "failed to parse X-History-Length %s", max) } else { maxLen = m } } if len(since) > 0 { ts, err := parseSince(since) if err != nil { c.Logf("warn", "Failed to parse X-History-Since field %s: %s", since, err) return 0, nil } toSend := topic.Since(ts) // If maxLen is also set, we trim the list by sending the newest. ls := len(toSend) if maxLen > 0 && ls > maxLen { offset := ls - maxLen - 1 toSend = toSend[offset:] } return sendHistory(c, res, toSend) } else if maxLen > 0 { toSend := topic.Last(maxLen) return sendHistory(c, res, toSend) } return 0, nil }
// BufferPost buffers the body of the POST request into the context. // // Params: // // Returns: // - []byte with the content of the request. func BufferPost(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { req := c.Get("http.Request", nil).(*http.Request) var b bytes.Buffer _, err := io.Copy(&b, req.Body) c.Logf("info", "Received POST: %s", b.Bytes()) return b.Bytes(), err }
// Show help. // This command is useful for placing at the front of a CLI "subcommand" to have it output // help information. It will only trigger when "show" is set to true, so another command // can, for example, check for a "-h" or "-help" flag and set "show" based on that. // // Params: // - show (bool): If `true`, show help. // - summary (string): A one-line summary of the command. // - description (string): A short description of what the command does. // - usage (string): usage information. // - flags (FlagSet): Flags that are supported. The FlagSet will be converted to help text. // - writer (Writer): The location that this will write to. Default is os.Stdout // - subcommands ([]string): A list of subcommands. This will be formatted as help text. func ShowHelp(cxt cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { showHelp := false showHelpO := params.Get("show", false) switch showHelpO.(type) { case string: showHelp = strings.ToLower(showHelpO.(string)) == "true" case bool: showHelp = showHelpO.(bool) } writer := params.Get("writer", os.Stdout).(io.Writer) pmap := params.AsMap() // Last resort: If no summary, pull it from the route description. if summary, ok := pmap["summary"]; !ok || len(summary.(string)) == 0 { pmap["summary"] = cxt.Get("route.Description", "").(string) } sections := []string{"summary", "description", "usage"} if _, ok := params.Has("subcommands"); ok { sections = append(sections, "subcommands") } if showHelp { displayHelp(sections, pmap, writer) return true, new(cookoo.Stop) } return false, nil }
// MergeToYaml converts a Config object and a yaml.File to a single yaml.File. // // Params: // - conf (*Config): The configuration to merge. // - overwriteImports (bool, default true): If this is true, old config will // overwritten. If false, we attempt to merge the old and new config, with // preference to the old. // // Returns: // - The root yaml.Node of the modified config. // // Uses: // - cxt.Get("yaml.File") as the source for the YAML file. func MergeToYaml(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { root := c.Get("yaml.File", nil).(*yaml.File).Root cfg := p.Get("conf", nil).(*Config) overwrite := p.Get("overwriteImports", true).(bool) rootMap, ok := root.(yaml.Map) if !ok { return nil, fmt.Errorf("Expected root node to be a map.") } if len(cfg.Name) > 0 { rootMap["package"] = yaml.Scalar(cfg.Name) } if cfg.InCommand != "" { rootMap["incmd"] = yaml.Scalar(cfg.InCommand) } if overwrite { // Imports imports := make([]yaml.Node, len(cfg.Imports)) for i, imp := range cfg.Imports { imports[i] = imp.ToYaml() } rootMap["import"] = yaml.List(imports) } else { var err error rootMap, err = mergeImports(rootMap, cfg) if err != nil { Warn("Problem merging imports: %s\n", err) } } return root, 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 }
// Flush sends content to output. // // If no writer is specified, this will attempt to write to whatever is in the // Context with the key "http.ResponseWriter". If no suitable writer is found, it will // not write to anything at all. // // Params: // - writer: A Writer of some sort. This will try to write to the HTTP response if no writer // is specified. // - content: The content to write as a body. If this is a byte[], it is sent unchanged. Otherwise. // we first try to convert to a string, then pass it into a writer. // - contentType: The content type header (e.g. text/html). Default is text/plain // - responseCode: Integer HTTP Response Code: Default is `http.StatusOK`. // - headers: a map[string]string of HTTP headers. The keys will be run through // http.CannonicalHeaderKey() // // Note that this is optimized for writing from strings or arrays, not Readers. For larger // objects, you may find it more efficient to use a different command. // // Context: // - If this finds `web.ContentEncoding`, it will set a content-encoding header. // // Returns // // - boolean true func Flush(cxt cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { // Make sure we have a place to write this stuff. writer, ok := params.Has("writer") if writer == nil { writer, ok = cxt.Has("http.ResponseWriter") if !ok { return false, nil } } out := writer.(http.ResponseWriter) // Get the rest of the info. code := params.Get("responseCode", http.StatusOK).(int) header := out.Header() contentType := params.Get("contentType", "text/plain; charset=utf-8").(string) // Prepare the content. var content []byte rawContent, ok := params.Has("content") if !ok { // No content. Send nothing in the body. content = []byte("") } else if byteContent, ok := rawContent.([]byte); ok { // Got a byte[]; add it as is. content = byteContent } else { // Use the formatter to convert to a string, and then // cast it to bytes. content = []byte(fmt.Sprintf("%v", rawContent)) } // Add headers: header.Set(http.CanonicalHeaderKey("content-type"), contentType) te := cxt.Get(ContentEncoding, "").(string) if len(te) > 0 { header.Set(http.CanonicalHeaderKey("transfer-encoding"), te) } headerO, ok := params.Has("headers") if ok { headers := headerO.(map[string]string) for k, v := range headers { header.Add(http.CanonicalHeaderKey(k), v) } } // Send the headers. out.WriteHeader(code) //io.WriteString(out, content) out.Write(content) return true, nil }
// DropToShell executes a glide plugin. A command that's implemented by // another application is executed in a similar manner to the way git commands // work. For example, 'glide foo' would try to execute the application glide-foo. // Params: // - command: the name of the command to attempt executing. func DropToShell(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { args := c.Get("os.Args", nil).([]string) command := p.Get("command", "").(string) if len(args) == 0 { return nil, fmt.Errorf("Could not get os.Args.") } cwd, err := os.Getwd() if err != nil { return nil, err } projpath := cwd if tmp := os.Getenv("GLIDE_PROJECT"); len(tmp) != 0 { projpath = tmp } cmd := "glide-" + command var fullcmd string if fullcmd, err = exec.LookPath(cmd); err != nil { fullcmd = projpath + "/" + cmd if _, err := os.Stat(fullcmd); err != nil { return nil, fmt.Errorf("Command %s does not exist.", cmd) } } // Turning os.Args first argument from `glide` to `glide-command` args[0] = cmd // Removing the first argument (command) removed := false for i, v := range args { if removed == false && v == command { args = append(args[:i], args[i+1:]...) removed = true } } pa := os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, Dir: cwd, } fmt.Printf("Delegating to plugin %s (%v)\n", fullcmd, args) proc, err := os.StartProcess(fullcmd, args, &pa) if err != nil { return nil, err } if _, err := proc.Wait(); err != nil { return nil, err } return nil, nil }
// ServerInfo gets the server info for this request. // // This assumes that `http.Request` and `http.ResponseWriter` are in the context, which // they are by default. // // Returns: // - boolean true func ServerInfo(cxt cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { req := cxt.Get("http.Request", nil).(*http.Request) out := cxt.Get("http.ResponseWriter", nil).(http.ResponseWriter) out.Header().Add("X-Foo", "Bar") out.Header().Add("Content-type", "text/plain; charset=utf-8") fmt.Fprintf(out, "Request:\n %+v\n", req) fmt.Fprintf(out, "\n\n\nResponse:\n%+v\n", out) return true, nil }
// ServeTLS does the same as Serve, but with SSL support. // // If `server.Address` is not found in the context, the default address is // `:4433`. // // Neither certFile nor keyFile are stored in the context. These values are // considered to be security sensitive. func ServeTLS(reg *cookoo.Registry, router *cookoo.Router, cxt cookoo.Context, certFile, keyFile string) { addr := cxt.Get("server.Address", ":4433").(string) server := &http.Server{Addr: addr} server.Handler = NewCookooHandler(reg, router, cxt) go handleSignals(router, cxt, server) err := server.ListenAndServeTLS(certFile, keyFile) if err != nil { cxt.Logf("error", "Caught error while serving: %s", err) if router.HasRoute("@crash") { router.HandleRequest("@crash", cxt, false) } } }
// Return the path to the vendor directory. func VendorPath(c cookoo.Context) (string, error) { vendor := c.Get("VendorDir", "vendor").(string) filename := c.Get("yaml", "glide.yaml").(string) cwd, err := os.Getwd() if err != nil { return "", err } // Find the directory that contains glide.yaml yamldir, err := glideWD(cwd, filename) if err != nil { return cwd, err } gopath := filepath.Join(yamldir, vendor) return gopath, nil }
// LinkPackage creates a symlink to the project within the GOPATH. func LinkPackage(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { cfg := c.Get("cfg", "").(*Config) pname := p.Get("path", cfg.Name).(string) //here, err := os.Getwd() //if err != nil { // return nil, fmt.Errorf("Could not get current directory: %s", err) //} // Per issue #10, this may be nicer to work with in cases where repos are // moved. //here := "../.." depth := strings.Count(pname, "/") here := "../.." + strings.Repeat("/..", depth) gopath := os.Getenv("GOPATH") if len(gopath) == 0 { return nil, fmt.Errorf("$GOPATH appears to be unset") } if len(pname) == 0 { return nil, fmt.Errorf("glide.yaml is missing 'package:'") } base := path.Dir(pname) if base != "." { dir := fmt.Sprintf("%s/src/%s", gopath, base) if err := os.MkdirAll(dir, os.ModeDir|0755); err != nil { return nil, fmt.Errorf("Failed to make directory %s: %s", dir, err) } } ldest := fmt.Sprintf("%s/src/%s", gopath, pname) if err := os.Symlink(here, ldest); err != nil { if os.IsExist(err) { Info("Link to %s already exists. Skipping.\n", ldest) } else { return nil, fmt.Errorf("Failed to create symlink from %s to %s: %s", gopath, ldest, err) } } return ldest, nil }
// TopicExists tests whether a topic exists, and sends an HTTP 200 if yes, 404 if no. // // Params: // - topic (string): The topic to look up. // Returns: // func TopicExists(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { res := c.Get("http.ResponseWriter", nil).(ResponseWriterFlusher) name := p.Get("topic", "").(string) if len(name) == 0 { res.WriteHeader(404) return nil, nil } medium, err := getMedium(c) if err != nil { res.WriteHeader(404) return nil, nil } if _, ok := medium.Topic(name); ok { res.WriteHeader(200) return nil, nil } res.WriteHeader(404) return nil, nil }
// Shift the args N (default 1) times, returning the last shifted value. // // Params: // - n: The number of times to shift. Only the last value is returned. // - args: The name of the context slice/array to modify. This value will be retrieved // from the context. Default: "os.Args" func ShiftArgs(c cookoo.Context, params *cookoo.Params) (interface{}, cookoo.Interrupt) { n := params.Get("n", 1).(int) argName := params.Get("args", "os.Args").(string) args, ok := c.Get(argName, nil).([]string) if !ok { return nil, &cookoo.FatalError{"Could not get arg out of context: No such arg name."} } if len(args) < n { c.Put(argName, make([]string, 0)) //log.Printf("Not enough args in %s", argName) return nil, &cookoo.RecoverableError{"Not enough arguments."} } targetArg := n - 1 shifted := args[targetArg] c.Put(argName, args[n:]) return shifted, nil }
// 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 }
// InitGlide initializes a new Glide project. // // Among other things, it creates a default glide.yaml. // // Params: // - filename (string): The name of the glide YAML file. Default is glide.yaml. // - project (string): The name of the project. Default is 'main'. func InitGlide(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { fname := p.Get("filename", "glide.yaml").(string) pname := p.Get("project", "main").(string) vdir := c.Get("VendorDir", "vendor").(string) if _, err := os.Stat(fname); err == nil { cwd, _ := os.Getwd() return false, fmt.Errorf("Cowardly refusing to overwrite %s in %s", fname, cwd) } f, err := os.Create(fname) if err != nil { return false, err } fmt.Fprintf(f, yamlTpl, pname) f.Close() os.MkdirAll(vdir, 0755) Info("Initialized. You can now edit '%s'\n", fname) return true, nil }
// startEtcd starts a cluster member of a static etcd cluster. // // Params: // - discover (string): Value to pass to etcd --discovery. // - client (client.Client): A client to the discovery server. This will // periodically write data there to indicate liveness. func startEtcd(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { cli := p.Get("client", nil).(client.Client) // Use config from environment. cmd := exec.Command("etcd") cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout println(strings.Join(os.Environ(), "\n")) if err := cmd.Start(); err != nil { log.Errf(c, "Failed to start etcd: %s", err) return nil, err } // We need a way to tell starting members that there is an existing cluster, // and that that cluster has met the initial consensus requirements. This // basically stores a status record that indicates that it is an existing // and running etcd server. It allows for basically a two hour window // during which a cluster can be in an uncertain state before the entire // thing gives up and a new cluster is created. ticker := time.NewTicker(time.Minute) expires := time.Hour * 2 go func() { name := c.Get("ETCD_NAME", "").(string) tok := c.Get("DEIS_ETCD_DISCOVERY_TOKEN", "").(string) key := fmt.Sprintf(discovery.ClusterStatusKey, tok, name) for t := range ticker.C { etcd.SimpleSet(cli, key, t.String(), expires) } }() if err := cmd.Wait(); err != nil { ticker.Stop() log.Errf(c, "Etcd quit unexpectedly: %s", err) } return nil, nil }
// Configure creates a new SSH configuration object. // // Config sets a PublicKeyCallback handler that forwards public key auth // requests to the route named "pubkeyAuth". // // This assumes certain details about our environment, like the location of the // host keys. It also provides only key-based authentication. // ConfigureServerSshConfig // // Returns: // An *ssh.ServerConfig func Configure(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { router := c.Get("cookoo.Router", nil).(*cookoo.Router) cfg := &ssh.ServerConfig{ PublicKeyCallback: func(m ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) { c.Put("metadata", m) c.Put("key", k) pubkeyAuth := c.Get("route.sshd.pubkeyAuth", "pubkeyAuth").(string) err := router.HandleRequest(pubkeyAuth, c, true) return c.Get("pubkeyAuth", &ssh.Permissions{}).(*ssh.Permissions), err }, } return cfg, nil }
// Debug displays debugging info. func Debug(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { w := c.Get("http.ResponseWriter", nil).(http.ResponseWriter) r := c.Get("http.Request", nil).(*http.Request) reqInfoHandler(w, r) return nil, nil }
// readBody reads the body of an http request // // Params: // // Returns: // []byte func readBody(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { r := c.Get("http.Request", nil).(*http.Request) return ioutil.ReadAll(r.Body) }
// 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 }