// 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 }
// 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 }
// 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 }
// iam injects info into the environment about a host's self. // // Sets the following environment variables. (Values represent the data format. // Instances will get its own values.) // // MY_NODEIP=10.245.1.3 // MY_SERVICE_IP=10.22.1.4 // MY_PORT_PEER=2380 // MY_PORT_CLIENT=2379 // MY_NAMESPACE=default // MY_SELFLINK=/api/v1/namespaces/default/pods/deis-etcd-1-336jp // MY_UID=62a3b54a-6956-11e5-b8ab-0800279dd272 // MY_APISERVER=https://10.247.0.1:443 // MY_NAME=deis-etcd-1-336jp // MY_IP=10.246.44.7 // MY_LABEL_NAME=deis-etcd-1 # One entry per label in the JSON // MY_ANNOTATION_NAME=deis-etcd-1 # One entry per annitation in the JSON // MY_PORT_CLIENT=4100 // MY_PORT_PEER=2380 func iam(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { me, err := aboutme.FromEnv() if err != nil { log.Errf(c, "Failed aboutme.FromEnv: %s", err) } else { if strings.TrimSpace(me.IP) == "" { log.Warn(c, "No IP found by API query.") ip, err := aboutme.MyIP() if err != nil || ip == "" { // Force pod death. log.Errf(c, "Failed to get an IP address: %s", err) os.Exit(5) } } me.ShuntEnv() os.Setenv("ETCD_NAME", me.Name) c.Put("ETCD_NAME", me.Name) } passEnv("MY_PORT_CLIENT", "$DEIS_ETCD_1_SERVICE_PORT_CLIENT") passEnv("MY_PORT_PEER", "$DEIS_ETCD_1_SERVICE_PORT_PEER") return nil, 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 }
// 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 }
/** * 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 }
// 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 }
// Utility function to get the database from a datasource. func GetDb(cxt cookoo.Context, dbname string) (*sql.DB, error) { dbO, ok := cxt.HasDatasource(dbname) if !ok { return nil, &cookoo.FatalError{fmt.Sprintf("No DB datasource named '%s' found.", dbname)} } return dbO.(*sql.DB), nil }
// Template is a template-based text formatter. // // This uses the core `text/template` to process a given string template. // // Params // - template (string): A template string. // - template.Context (bool): If true, the context will be placed into the // template renderer as 'Cxt', and can be used as `{{.Cxt.Foo}}`. False // by default. // - ... (interface{}): Values passed into the template. // // Conventionally, template variables should start with an initial capital. // // Returns a formatted string. func Template(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { format := cookoo.GetString("template", "", p) withCxt := cookoo.GetBool("template.Context", false, p) name := fmt.Sprintf("%x", md5.Sum([]byte(format))) //c.Logf("debug", "Template %s is '%s'\n", name, format) tpl, err := template.New(name).Parse(format) if err != nil { return "", err } data := p.AsMap() if withCxt { //c.Logf("debug", "Adding context.") data["Cxt"] = c.AsMap() } var out bytes.Buffer if err := tpl.Execute(&out, data); err != nil { return "", err } return out.String(), nil }
func (r *URIPathResolver) subtreeMatch(c cookoo.Context, pathName, pattern string) bool { if pattern == "**" { return true } // Find out how many slashes we have. countSlash := strings.Count(pattern, "/") // '**' matches anything. if countSlash == 0 { c.Logf("warn", "Illegal pattern: %s", pattern) return false } // Add 2 for verb plus trailer. parts := strings.SplitN(pathName, "/", countSlash+1) prefix := strings.Join(parts[0:countSlash], "/") subpattern := strings.Replace(pattern, "/**", "", -1) if ok, err := path.Match(subpattern, prefix); ok && err == nil { return true } else if err != nil { c.Logf("warn", "Parsing path `%s` gave error: %s", err) } return false }
// 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 }
// 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) } } }
// 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 }
// 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 }
// 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 }
// ParseYaml parses the glide.yaml format and returns a Configuration object. // // Params: // - filename (string): YAML filename as a string // // Context: // - yaml.File: This puts the parsed YAML file into the context. // // Returns: // - *Config: The configuration. func ParseYaml(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { fname := p.Get("filename", "glide.yaml").(string) //conf := new(Config) f, err := yaml.ReadFile(fname) if err != nil { return nil, err } c.Put("yaml.File", f) return FromYaml(f.Root) }
func setupHandler(c *cli.Context, route string, cxt cookoo.Context, router *cookoo.Router) { cxt.Put("q", c.GlobalBool("quiet")) cxt.Put("debug", c.GlobalBool("debug")) cxt.Put("no-color", c.GlobalBool("no-color")) cxt.Put("yaml", c.GlobalString("yaml")) cxt.Put("home", c.GlobalString("home")) cxt.Put("cliArgs", c.Args()) if err := router.HandleRequest(route, cxt, false); err != nil { fmt.Printf("Oops! %s\n", err) os.Exit(1) } }
// handleSignals traps kill and interrupt signals and runs shutdown(). func handleSignals(router *cookoo.Router, cxt cookoo.Context, server *http.Server) { sig := make(chan os.Signal, 1) signal.Notify(sig, os.Kill, os.Interrupt) s := <-sig cxt.Logf("info", "Received signal %s. Shutting down.", s) // Not particularly useful on its own. // server.SetKeepAlivesEnabled(false) // TODO: Implement graceful shutdowns. shutdown(router, cxt) os.Exit(0) }
// iam injects info into the environment about a host's self. // // Sets the following environment variables. (Values represent the data format. // Instances will get its own values.) // // MY_NODEIP=10.245.1.3 // MY_SERVICE_IP=10.22.1.4 // MY_PORT_PEER=2380 // MY_PORT_CLIENT=2379 // MY_NAMESPACE=default // MY_SELFLINK=/api/v1/namespaces/default/pods/deis-etcd-1-336jp // MY_UID=62a3b54a-6956-11e5-b8ab-0800279dd272 // MY_APISERVER=https://10.247.0.1:443 // MY_NAME=deis-etcd-1-336jp // MY_IP=10.246.44.7 // MY_LABEL_NAME=deis-etcd-1 # One entry per label in the JSON // MY_ANNOTATION_NAME=deis-etcd-1 # One entry per annitation in the JSON // MY_PORT_CLIENT=4100 // MY_PORT_PEER=2380 func iam(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { me, err := aboutme.FromEnv() if err != nil { log.Errf(c, "Failed aboutme.FromEnv: %s", err) } else { me.ShuntEnv() os.Setenv("ETCD_NAME", me.Name) c.Put("ETCD_NAME", me.Name) } passEnv("MY_PORT_CLIENT", "$DEIS_ETCD_1_SERVICE_PORT_CLIENT") passEnv("MY_PORT_PEER", "$DEIS_ETCD_1_SERVICE_PORT_PEER") return nil, 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 }
// sendHistory sends the accumulated history to the writer. func sendHistory(c cookoo.Context, writer ResponseWriterFlusher, data [][]byte) (int, error) { c.Logf("info", "Sending history.") var i int var d []byte for i, d = range data { _, err := writer.Write(d) if err != nil { c.Logf("warn", "Failed to write history message: %s", err) return i + 1, nil } writer.Flush() } return i + 1, 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) } } }
// 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 }
// 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 }
// Publish sends a new message to a topic. // // Params: // - topic (string): The topic to send to. // - message ([]byte): The message to send. // - withHistory (bool): Turn on history. Default is true. This only takes // effect when the channel is created. // // Datasources: // - This uses the 'drift.Medium' datasource. // // Returns: // func Publish(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { hist := p.Get("withHistory", true).(bool) topic := p.Get("topic", "").(string) if len(topic) == 0 { return nil, errors.New("No topic supplied.") } medium, _ := getMedium(c) // Is there any reason to disallow empty messages? msg := p.Get("message", []byte{}).([]byte) c.Logf("info", "Msg: %s", msg) t := fetchOrCreateTopic(medium, topic, hist, DefaultMaxHistory) return nil, t.Publish(msg) }
func addFlagsToContext(flagset *flag.FlagSet, cxt cookoo.Context) { store := func(f *flag.Flag) { // fmt.Printf("Storing %s in context with value %s.\n", f.Name, f.Value.String()) // Basically, we can tell the difference between booleans and strings, and that's it. // Other types are a loss. /* if f.IsBoolFlag != nil { cxt.Put(f.Name, f.Value.String() == "true") } else { cxt.Put(f.Name, f.Value.String()) } */ cxt.Put(f.Name, f.Value.String()) } flagset.VisitAll(store) }
// 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 }