func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Request) { st := sh.currentStatus() f := func(p string, a ...interface{}) { if len(a) == 0 { io.WriteString(rw, p) } else { fmt.Fprintf(rw, p, a...) } } f("<html><head><title>camlistored status</title></head>") f("<body>") f("<h1>camlistored status</h1>") f("<h2>Versions</h2><ul>") var envStr string if env.OnGCE() { envStr = " (on GCE)" } f("<li><b>Camlistore</b>: %s%s</li>", html.EscapeString(buildinfo.Version()), envStr) f("<li><b>Go</b>: %s/%s %s, cgo=%v</li>", runtime.GOOS, runtime.GOARCH, runtime.Version(), cgoEnabled) f("<li><b>djpeg</b>: %s", html.EscapeString(buildinfo.DjpegStatus())) f("</ul>") f("<h2>Logs</h2><ul>") f(" <li><a href='/debug/config'>/debug/config</a> - server config</li>\n") if env.OnGCE() { f(" <li><a href='/debug/logs/camlistored'>camlistored logs on Google Cloud Logging</a></li>\n") f(" <li><a href='/debug/logs/system'>system logs from Google Compute Engine</a></li>\n") } f("</ul>") f("<h2>Admin</h2>") f("<form method='post' action='restart' onsubmit='return confirm(\"Really restart now?\")'><button>restart server</button></form>") f("<h2>Handlers</h2>") f("<p>As JSON: <a href='status.json'>status.json</a>; and the <a href='%s?camli.mode=config'>discovery JSON</a>.</p>", st.rootPrefix) f("<p>Not yet pretty HTML UI:</p>") js, err := json.MarshalIndent(st, "", " ") if err != nil { log.Printf("JSON marshal error: %v", err) } jsh := html.EscapeString(string(js)) jsh = quotedPrefix.ReplaceAllStringFunc(jsh, func(in string) string { pfx := in[1 : len(in)-1] if st.isHandler(pfx) { return fmt.Sprintf("%s<a href='%s'>%s</a>%s", in[:1], pfx, pfx, in[len(in)-1:]) } return in }) f("<pre>%s</pre>", jsh) }
// LogWriter returns an environment-specific io.Writer suitable for passing // to log.SetOutput. It will also include writing to os.Stderr as well. func LogWriter() (w io.Writer) { w = os.Stderr if !env.OnGCE() { return } projID, err := metadata.ProjectID() if projID == "" { log.Printf("Error getting project ID: %v", err) return } scopes, _ := metadata.Scopes("default") haveScope := func(scope string) bool { for _, x := range scopes { if x == scope { return true } } return false } if !haveScope(logging.Scope) { log.Printf("when this Google Compute Engine VM instance was created, it wasn't granted enough access to use Google Cloud Logging (Scope URL: %v).", logging.Scope) return } logc, err := logging.NewClient(context.Background(), projID, "camlistored-stderr") if err != nil { log.Printf("Error creating Google logging client: %v", err) return } return io.MultiWriter(w, logc.Writer(logging.Debug)) }
func init() { if !env.OnGCE() { return } osutil.RegisterConfigDirFunc(func() string { v, _ := metadata.InstanceAttributeValue("camlistore-config-dir") if v == "" { return v } return path.Clean("/gcs/" + strings.TrimPrefix(v, "gs://")) }) jsonconfig.RegisterFunc("_gce_instance_meta", func(c *jsonconfig.ConfigParser, v []interface{}) (interface{}, error) { if len(v) != 1 { return nil, errors.New("only 1 argument supported after _gce_instance_meta") } attr, ok := v[0].(string) if !ok { return nil, errors.New("expected argument after _gce_instance_meta to be a string") } val, err := metadata.InstanceAttributeValue(attr) if err != nil { return nil, fmt.Errorf("error reading GCE instance attribute %q: %v", attr, err) } return val, nil }) }
func (sh *StatusHandler) googleCloudConsole() (string, error) { if !env.OnGCE() { return "", errors.New("not on GCE") } projID, err := metadata.ProjectID() if err != nil { return "", fmt.Errorf("Error getting project ID: %v", err) } return "https://console.cloud.google.com/compute/instances?project=" + projID, nil }
// LogWriter returns an environment-specific io.Writer suitable for passing // to log.SetOutput. It will also include writing to os.Stderr as well. func LogWriter() (w io.Writer) { w = os.Stderr if !env.OnGCE() { return } projID, err := metadata.ProjectID() if projID == "" { log.Printf("Error getting project ID: %v", err) return } hc, err := google.DefaultClient(oauth2.NoContext) if err != nil { log.Printf("Error creating default GCE OAuth2 client: %v", err) return } logc, err := logging.NewClient(cloud.NewContext(projID, hc), "camlistored-stderr") if err != nil { log.Printf("Error creating Google logging client: %v", err) return } return io.MultiWriter(w, logc.Writer(logging.Debug)) }
// DefaultLetsEncryptCache returns the path to the default Let's Encrypt cache // directory (or file, depending on the ACME implementation). func DefaultLetsEncryptCache() string { if env.OnGCE() { return "/tmp/camli-letsencrypt.cache" } return filepath.Join(CamliConfigDir(), "letsencrypt.cache") }
// Main sends on up when it's running, and shuts down when it receives from down. func Main(up chan<- struct{}, down <-chan struct{}) { flag.Parse() if *flagVersion { fmt.Fprintf(os.Stderr, "camlistored version: %s\nGo version: %s (%s/%s)\n", buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) return } if legalprint.MaybePrint(os.Stderr) { return } if env.OnGCE() { log.SetOutput(gce.LogWriter()) } if *flagReindex { index.SetImpendingReindex() } log.Printf("Starting camlistored version %s; Go %s (%s/%s)", buildinfo.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) shutdownc := make(chan io.Closer, 1) // receives io.Closer to cleanly shut down go handleSignals(shutdownc) // In case we're running in a Docker container with no // filesytem from which to load the root CAs, this // conditionally installs a static set if necessary. We do // this before we load the config file, which might come from // an https URL. httputil.InstallCerts() config, isNewConfig, err := loadConfig(*flagConfigFile) if err != nil { exitf("Error loading config file: %v", err) } ws := webserver.New() listen, baseURL := listenAndBaseURL(config) hostname, err := certHostname(listen, baseURL) if err != nil { exitf("Bad baseURL or listen address: %v", err) } setupTLS(ws, config, hostname) err = ws.Listen(listen) if err != nil { exitf("Listen: %v", err) } if baseURL == "" { baseURL = ws.ListenURL() } shutdownCloser, err := config.InstallHandlers(ws, baseURL, *flagReindex, nil) if err != nil { exitf("Error parsing config: %v", err) } shutdownc <- shutdownCloser urlToOpen := baseURL if !isNewConfig { // user may like to configure the server at the initial startup, // open UI if this is not the first run with a new config file. urlToOpen += config.UIPath } if *flagOpenBrowser { go osutil.OpenURL(urlToOpen) } go ws.Serve() if flagPollParent { osutil.DieOnParentDeath() } if err := config.StartApps(); err != nil { exitf("StartApps: %v", err) } for appName, appURL := range config.AppURL() { addr, err := netutil.HostPort(appURL) if err != nil { log.Printf("Could not get app %v address: %v", appName, err) continue } if err := netutil.AwaitReachable(addr, 5*time.Second); err != nil { log.Printf("Could not reach app %v: %v", appName, err) } } log.Printf("Available on %s", urlToOpen) if env.OnGCE() && strings.HasPrefix(baseURL, "https://") { go redirectFromHTTP(baseURL) } // Block forever, except during tests. up <- struct{}{} <-down osExit(0) }
// DefaultEnvConfig returns the default configuration when running on a known // environment. Currently this just includes Google Compute Engine. // If the environment isn't known (nil, nil) is returned. func DefaultEnvConfig() (*Config, error) { if !env.OnGCE() { return nil, nil } auth := "none" user, _ := metadata.InstanceAttributeValue("camlistore-username") pass, _ := metadata.InstanceAttributeValue("camlistore-password") confBucket, err := metadata.InstanceAttributeValue("camlistore-config-dir") if confBucket == "" || err != nil { return nil, fmt.Errorf("VM instance metadata key 'camlistore-config-dir' not set: %v", err) } blobBucket, err := metadata.InstanceAttributeValue("camlistore-blob-dir") if blobBucket == "" || err != nil { return nil, fmt.Errorf("VM instance metadata key 'camlistore-blob-dir' not set: %v", err) } if user != "" && pass != "" { auth = "userpass:"******":" + pass } if v := osutil.SecretRingFile(); !strings.HasPrefix(v, "/gcs/") { return nil, fmt.Errorf("Internal error: secret ring path on GCE should be at /gcs/, not %q", v) } keyId, secRing, err := getOrMakeKeyring() if err != nil { return nil, err } ipOrHost, _ := metadata.ExternalIP() host, _ := metadata.InstanceAttributeValue("camlistore-hostname") if host != "" && host != "localhost" { ipOrHost = host } highConf := &serverconfig.Config{ Auth: auth, BaseURL: fmt.Sprintf("https://%s", ipOrHost), HTTPS: true, Listen: "0.0.0.0:443", Identity: keyId, IdentitySecretRing: secRing, GoogleCloudStorage: ":" + strings.TrimPrefix(blobBucket, "gs://"), DBNames: map[string]string{}, PackRelated: true, // SourceRoot is where we look for the UI js/css/html files, and the Closure resources. // Must be in sync with misc/docker/server/Dockerfile. SourceRoot: "/camlistore", } // Detect a linked Docker MySQL container. It must have alias "mysqldb". if v := os.Getenv("MYSQLDB_PORT"); strings.HasPrefix(v, "tcp://") { hostPort := strings.TrimPrefix(v, "tcp://") highConf.MySQL = "root@" + hostPort + ":" // no password highConf.DBNames["queue-sync-to-index"] = "sync_index_queue" highConf.DBNames["ui_thumbcache"] = "ui_thumbmeta_cache" highConf.DBNames["blobpacked_index"] = "blobpacked_index" } else { // TODO: also detect Cloud SQL. highConf.KVFile = "/index.kv" } return genLowLevelConfig(highConf) }
func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Request) { st := sh.currentStatus() f := func(p string, a ...interface{}) { if len(a) == 0 { io.WriteString(rw, p) } else { fmt.Fprintf(rw, p, a...) } } f("<html><head><title>camlistored status</title></head>") f("<body>") f("<h1>camlistored status</h1>") f("<h2>Versions</h2><ul>") var envStr string if env.OnGCE() { envStr = " (on GCE)" } f("<li><b>Camlistore</b>: %s%s</li>", html.EscapeString(buildinfo.Version()), envStr) f("<li><b>Go</b>: %s/%s %s, cgo=%v</li>", runtime.GOOS, runtime.GOARCH, runtime.Version(), cgoEnabled) f("<li><b>djpeg</b>: %s", html.EscapeString(buildinfo.DjpegStatus())) f("</ul>") f("<h2>Logs</h2><ul>") f(" <li><a href='/debug/config'>/debug/config</a> - server config</li>\n") if env.OnGCE() { f(" <li><a href='/debug/logs/camlistored'>camlistored logs on Google Cloud Logging</a></li>\n") f(" <li><a href='/debug/logs/system'>system logs from Google Compute Engine</a></li>\n") } f("</ul>") f("<h2>Admin</h2>") f("<ul>") f(" <li><form method='post' action='restart' onsubmit='return confirm(\"Really restart now?\")'><button>restart server</button>") f("<input type='checkbox' name='reindex'> reindex</form></li>") if env.OnGCE() { console, err := sh.googleCloudConsole() if err != nil { log.Printf("error getting Google Cloud Console URL: %v", err) } else { f(" <li><b>Updating:</b> When a new image for Camlistore on GCE is available, you can update by hitting \"Reset\" (or \"Stop\", then \"Start\") for your instance on your <a href='%s'>Google Cloud Console</a>.<br>Alternatively, you can ssh to your instance and restart the Camlistore service with: <b>sudo systemctl restart camlistored</b>.</li>", console) } } f("</ul>") f("<h2>Handlers</h2>") f("<p>As JSON: <a href='status.json'>status.json</a>; and the <a href='%s?camli.mode=config'>discovery JSON</a>.</p>", st.rootPrefix) f("<p>Not yet pretty HTML UI:</p>") js, err := json.MarshalIndent(st, "", " ") if err != nil { log.Printf("JSON marshal error: %v", err) } jsh := html.EscapeString(string(js)) jsh = quotedPrefix.ReplaceAllStringFunc(jsh, func(in string) string { pfx := in[1 : len(in)-1] if st.isHandler(pfx) { return fmt.Sprintf("%s<a href='%s'>%s</a>%s", in[:1], pfx, pfx, in[len(in)-1:]) } return in }) f("<pre>%s</pre>", jsh) }