netutil: add HostPort, serverinit: return app baseURL

Context: http://camlistore.org/issue/479

This patch allows camlistored to wait for all the apps to be serving,
before printing its own listening address.

Change-Id: I4035b115a03ef6a2a43177b83b5b65ebc50a2188
This commit is contained in:
mpl 2014-07-29 20:23:14 +02:00
parent 1763efbefe
commit 0a869ad067
6 changed files with 190 additions and 8 deletions

View File

@ -59,6 +59,8 @@ func ListenAddress() (string, error) {
if baseURL == "" {
return "", errors.New("CAMLI_APP_BACKEND_URL is undefined")
}
// TODO(mpl): see if can use netutil.TCPAddress (and get IP6 for free).
defaultPort := "80"
noScheme := strings.TrimPrefix(baseURL, "http://")
if strings.HasPrefix(baseURL, "https://") {

View File

@ -19,6 +19,8 @@ package netutil
import (
"fmt"
"net"
"net/url"
"strings"
"time"
)
@ -36,3 +38,32 @@ func AwaitReachable(addr string, maxWait time.Duration) error {
}
return fmt.Errorf("%v unreachable for %v", addr, maxWait)
}
// HostPort takes a urlStr string URL, and returns a host:port string suitable
// to passing to net.Dial, with the port set as the scheme's default port if
// absent.
func HostPort(urlStr string) (string, error) {
u, err := url.Parse(urlStr)
if err != nil {
return "", fmt.Errorf("could not parse %q as a url: %v", urlStr, err)
}
if u.Scheme == "" {
return "", fmt.Errorf("url %q has no scheme", urlStr)
}
hostPort := u.Host
if hostPort == "" || strings.HasPrefix(hostPort, ":") {
return "", fmt.Errorf("url %q has no host", urlStr)
}
idx := strings.Index(hostPort, "]")
if idx == -1 {
idx = 0
}
if !strings.Contains(hostPort[idx:], ":") {
if u.Scheme == "https" {
hostPort += ":443"
} else {
hostPort += ":80"
}
}
return hostPort, nil
}

122
pkg/netutil/netutil_test.go Normal file
View File

@ -0,0 +1,122 @@
/*
Copyright 2014 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package netutil
import (
"testing"
)
func TestHostPort(t *testing.T) {
tests := []struct {
baseURL string
wantNetAddr string
}{
// IPv4, no prefix
{
baseURL: "http://foo.com/",
wantNetAddr: "foo.com:80",
},
{
baseURL: "https://foo.com/",
wantNetAddr: "foo.com:443",
},
{
baseURL: "http://foo.com:8080/",
wantNetAddr: "foo.com:8080",
},
{
baseURL: "https://foo.com:8080/",
wantNetAddr: "foo.com:8080",
},
// IPv4, with prefix
{
baseURL: "http://foo.com/pics/",
wantNetAddr: "foo.com:80",
},
{
baseURL: "https://foo.com/pics/",
wantNetAddr: "foo.com:443",
},
{
baseURL: "http://foo.com:8080/pics/",
wantNetAddr: "foo.com:8080",
},
{
baseURL: "https://foo.com:8080/pics/",
wantNetAddr: "foo.com:8080",
},
// IPv6, no prefix
{
baseURL: "http://[::1]/",
wantNetAddr: "[::1]:80",
},
{
baseURL: "https://[::1]/",
wantNetAddr: "[::1]:443",
},
{
baseURL: "http://[::1]:8080/",
wantNetAddr: "[::1]:8080",
},
{
baseURL: "https://[::1]:8080/",
wantNetAddr: "[::1]:8080",
},
// IPv6, with prefix
{
baseURL: "http://[::1]/pics/",
wantNetAddr: "[::1]:80",
},
{
baseURL: "https://[::1]/pics/",
wantNetAddr: "[::1]:443",
},
{
baseURL: "http://[::1]:8080/pics/",
wantNetAddr: "[::1]:8080",
},
{
baseURL: "https://[::1]:8080/pics/",
wantNetAddr: "[::1]:8080",
},
}
for _, v := range tests {
got, err := HostPort(v.baseURL)
if err != nil {
t.Error(err)
continue
}
if got != v.wantNetAddr {
t.Errorf("got: %v for %v, want: %v", got, v.baseURL, v.wantNetAddr)
}
}
}

View File

@ -44,7 +44,8 @@ type Handler struct {
auth auth.AuthMode // Used for basic HTTP authenticating against the app requests.
appConfig jsonconfig.Obj // Additional parameters the app can request, or nil.
proxy *httputil.ReverseProxy // For redirecting requests to the app.
proxy *httputil.ReverseProxy // For redirecting requests to the app.
backendURL string // URL that we proxy to (i.e. base URL of the app).
}
func (a *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -80,6 +81,7 @@ func randPortBackendURL(apiHost, appHandlerPrefix string) (string, error) {
return "", fmt.Errorf("could not close random listener: %v", err)
}
// TODO(mpl): see if can use netutil.TCPAddress.
scheme := "https://"
noScheme := strings.TrimPrefix(apiHost, scheme)
if strings.HasPrefix(noScheme, "http://") {
@ -161,11 +163,12 @@ func NewHandler(conf jsonconfig.Obj, apiHost, appHandlerPrefix string) (*Handler
return nil, fmt.Errorf("could not parse backendURL %q: %v", backendURL, err)
}
return &Handler{
name: name,
envVars: envVars,
auth: basicAuth,
appConfig: appConfig,
proxy: httputil.NewSingleHostReverseProxy(proxyURL),
name: name,
envVars: envVars,
auth: basicAuth,
appConfig: appConfig,
proxy: httputil.NewSingleHostReverseProxy(proxyURL),
backendURL: backendURL,
}, nil
}
@ -235,3 +238,8 @@ func (a *Handler) AuthMode() auth.AuthMode {
func (a *Handler) AppConfig() map[string]interface{} {
return a.appConfig
}
// BackendURL returns the appBackendURL that the app handler will proxy to.
func (a *Handler) BackendURL() string {
return a.backendURL
}

View File

@ -582,6 +582,16 @@ func (config *Config) StartApps() error {
return nil
}
// AppURL returns a map of app name to app base URL for all the configured
// server apps.
func (config *Config) AppURL() map[string]string {
appURL := make(map[string]string, len(config.apps))
for _, ap := range config.apps {
appURL[ap.ProgramName()] = ap.BackendURL()
}
return appURL
}
func mustCreate(path string) *os.File {
f, err := os.Create(path)
if err != nil {

View File

@ -42,6 +42,7 @@ import (
"camlistore.org/pkg/buildinfo"
"camlistore.org/pkg/legal/legalprint"
"camlistore.org/pkg/misc"
"camlistore.org/pkg/netutil"
"camlistore.org/pkg/osutil"
"camlistore.org/pkg/serverinit"
"camlistore.org/pkg/webserver"
@ -399,8 +400,16 @@ func Main(up chan<- struct{}, down <-chan struct{}) {
exitf("StartApps: %v", err)
}
// TODO(mpl): wait for all the apps to somehow signal they have started (or failed to)
// before printing the one below?
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)
// Block forever, except during tests.