Merge pull request #1105 from lavalamp/proxy

Add a generic proxier
This commit is contained in:
brendandburns 2014-09-08 12:51:24 -07:00
commit 753b80c9b8
8 changed files with 425 additions and 4 deletions

View File

@ -93,8 +93,10 @@ func (g *APIGroup) InstallREST(mux mux, paths ...string) {
for _, prefix := range paths {
prefix = strings.TrimRight(prefix, "/")
proxyHandler := &ProxyHandler{prefix + "/proxy/", g.handler.storage, g.handler.codec}
mux.Handle(prefix+"/", http.StripPrefix(prefix, restHandler))
mux.Handle(prefix+"/watch/", http.StripPrefix(prefix+"/watch/", watchHandler))
mux.Handle(prefix+"/proxy/", http.StripPrefix(prefix+"/proxy/", proxyHandler))
mux.Handle(prefix+"/redirect/", http.StripPrefix(prefix+"/redirect/", redirectHandler))
mux.Handle(prefix+"/operations", http.StripPrefix(prefix+"/operations", opHandler))
mux.Handle(prefix+"/operations/", http.StripPrefix(prefix+"/operations/", opHandler))

View File

@ -77,8 +77,9 @@ type SimpleRESTStorage struct {
requestedFieldSelector labels.Selector
requestedResourceVersion uint64
// The location
// The id requested, and location to return for ResourceLocation
requestedResourceLocationID string
resourceLocation string
// If non-nil, called inside the WorkFunc when answering update, delete, create.
// obj receives the original input to the update, delete, or create call.
@ -157,7 +158,7 @@ func (storage *SimpleRESTStorage) ResourceLocation(id string) (string, error) {
if err := storage.errors["resourceLocation"]; err != nil {
return "", err
}
return id, nil
return storage.resourceLocation, nil
}
func extractBody(response *http.Response, object runtime.Object) (string, error) {

View File

@ -31,6 +31,7 @@ import (
"github.com/golang/glog"
)
// TODO: replace with proxy handler on minions
func handleProxyMinion(w http.ResponseWriter, req *http.Request) {
path := strings.TrimLeft(req.URL.Path, "/")
rawQuery := req.URL.RawQuery

226
pkg/apiserver/proxy.go Normal file
View File

@ -0,0 +1,226 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 apiserver
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"path"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"code.google.com/p/go.net/html"
"github.com/golang/glog"
)
// tagsToAttrs states which attributes of which tags require URL substitution.
// Sources: http://www.w3.org/TR/REC-html40/index/attributes.html
// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1
var tagsToAttrs = map[string]util.StringSet{
"a": util.NewStringSet("href"),
"applet": util.NewStringSet("codebase"),
"area": util.NewStringSet("href"),
"audio": util.NewStringSet("src"),
"base": util.NewStringSet("href"),
"blockquote": util.NewStringSet("cite"),
"body": util.NewStringSet("background"),
"button": util.NewStringSet("formaction"),
"command": util.NewStringSet("icon"),
"del": util.NewStringSet("cite"),
"embed": util.NewStringSet("src"),
"form": util.NewStringSet("action"),
"frame": util.NewStringSet("longdesc", "src"),
"head": util.NewStringSet("profile"),
"html": util.NewStringSet("manifest"),
"iframe": util.NewStringSet("longdesc", "src"),
"img": util.NewStringSet("longdesc", "src", "usemap"),
"input": util.NewStringSet("src", "usemap", "formaction"),
"ins": util.NewStringSet("cite"),
"link": util.NewStringSet("href"),
"object": util.NewStringSet("classid", "codebase", "data", "usemap"),
"q": util.NewStringSet("cite"),
"script": util.NewStringSet("src"),
"source": util.NewStringSet("src"),
"video": util.NewStringSet("poster", "src"),
// TODO: css URLs hidden in style elements.
}
// ProxyHandler provides a http.Handler which will proxy traffic to locations
// specified by items implementing Redirector.
type ProxyHandler struct {
prefix string
storage map[string]RESTStorage
codec Codec
}
func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
parts := strings.SplitN(req.URL.Path, "/", 3)
if len(parts) < 2 {
notFound(w, req)
return
}
resourceName := parts[0]
id := parts[1]
rest := ""
if len(parts) == 3 {
rest = parts[2]
}
storage, ok := r.storage[resourceName]
if !ok {
httplog.LogOf(w).Addf("'%v' has no storage object", resourceName)
notFound(w, req)
return
}
redirector, ok := storage.(Redirector)
if !ok {
httplog.LogOf(w).Addf("'%v' is not a redirector", resourceName)
notFound(w, req)
return
}
location, err := redirector.ResourceLocation(id)
if err != nil {
status := errToAPIStatus(err)
writeJSON(status.Code, r.codec, status, w)
return
}
destURL, err := url.Parse(location)
if err != nil {
status := errToAPIStatus(err)
writeJSON(status.Code, r.codec, status, w)
return
}
destURL.Path = rest
destURL.RawQuery = req.URL.RawQuery
newReq, err := http.NewRequest(req.Method, destURL.String(), req.Body)
if err != nil {
glog.Errorf("Failed to create request: %s", err)
}
newReq.Header = req.Header
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: destURL.Host})
proxy.Transport = &proxyTransport{
proxyScheme: req.URL.Scheme,
proxyHost: req.URL.Host,
proxyPathPrepend: path.Join(r.prefix, resourceName, id),
}
proxy.ServeHTTP(w, newReq)
}
type proxyTransport struct {
proxyScheme string
proxyHost string
proxyPathPrepend string
}
func (t *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
message := fmt.Sprintf("Error: '%s'\nTrying to reach: '%v'", err.Error(), req.URL.String())
resp = &http.Response{
StatusCode: http.StatusServiceUnavailable,
Body: ioutil.NopCloser(strings.NewReader(message)),
}
return resp, nil
}
if resp.Header.Get("Content-Type") != "text/html" {
// Do nothing, simply pass through
return resp, nil
}
return t.fixLinks(req, resp)
}
// updateURLs checks and updates any of n's attributes that are listed in tagsToAttrs.
// Any URLs found are, if they're relative, updated with the necessary changes to make
// a visit to that URL also go through the proxy.
// sourceURL is the URL of the page which we're currently on; it's required to make
// relative links work.
func (t *proxyTransport) updateURLs(n *html.Node, sourceURL *url.URL) {
if n.Type != html.ElementNode {
return
}
attrs, ok := tagsToAttrs[n.Data]
if !ok {
return
}
for i, attr := range n.Attr {
if !attrs.Has(attr.Key) {
continue
}
url, err := url.Parse(attr.Val)
if err != nil {
continue
}
// Is this URL relative?
if url.Host == "" {
url.Scheme = t.proxyScheme
url.Host = t.proxyHost
url.Path = path.Join(t.proxyPathPrepend, path.Dir(sourceURL.Path), url.Path, "/")
n.Attr[i].Val = url.String()
} else if url.Host == sourceURL.Host {
url.Scheme = t.proxyScheme
url.Host = t.proxyHost
url.Path = path.Join(t.proxyPathPrepend, url.Path)
n.Attr[i].Val = url.String()
}
}
}
// scan recursively calls f for every n and every subnode of n.
func (t *proxyTransport) scan(n *html.Node, f func(*html.Node)) {
f(n)
for c := n.FirstChild; c != nil; c = c.NextSibling {
t.scan(c, f)
}
}
// fixLinks modifies links in an HTML file such that they will be redirected through the proxy if needed.
func (t *proxyTransport) fixLinks(req *http.Request, resp *http.Response) (*http.Response, error) {
defer resp.Body.Close()
doc, err := html.Parse(resp.Body)
if err != nil {
glog.Errorf("Parse failed: %v", err)
return resp, err
}
newContent := &bytes.Buffer{}
t.scan(doc, func(n *html.Node) { t.updateURLs(n, req.URL) })
if err := html.Render(newContent, doc); err != nil {
glog.Errorf("Failed to render: %v", err)
}
resp.Body = ioutil.NopCloser(newContent)
// Update header node with new content-length
// TODO: Remove any hash/signature headers here?
resp.Header.Del("Content-Length")
resp.ContentLength = int64(newContent.Len())
return resp, err
}

190
pkg/apiserver/proxy_test.go Normal file
View File

@ -0,0 +1,190 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 apiserver
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"code.google.com/p/go.net/html"
)
func parseURLOrDie(inURL string) *url.URL {
parsed, err := url.Parse(inURL)
if err != nil {
panic(err)
}
return parsed
}
// fmtHTML parses and re-emits 'in', effectively canonicalizing it.
func fmtHTML(in string) string {
doc, err := html.Parse(strings.NewReader(in))
if err != nil {
panic(err)
}
out := &bytes.Buffer{}
if err := html.Render(out, doc); err != nil {
panic(err)
}
return string(out.Bytes())
}
func TestProxyTransport_fixLinks(t *testing.T) {
testTransport := &proxyTransport{
proxyScheme: "http",
proxyHost: "foo.com",
proxyPathPrepend: "/proxy/minion/minion1:10250/",
}
testTransport2 := &proxyTransport{
proxyScheme: "https",
proxyHost: "foo.com",
proxyPathPrepend: "/proxy/minion/minion1:8080/",
}
table := map[string]struct {
input string
sourceURL string
transport *proxyTransport
output string
}{
"normal": {
input: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre>`,
sourceURL: "http://myminion.com/logs/log.log",
transport: testTransport,
output: `<pre><a href="http://foo.com/proxy/minion/minion1:10250/logs/kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/minion/minion1:10250/logs/google.log">google.log</a></pre>`,
},
"subdir": {
input: `<a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a>`,
sourceURL: "http://myminion.com/whatever/apt/somelog.log",
transport: testTransport2,
output: `<a href="https://foo.com/proxy/minion/minion1:8080/whatever/apt/kubelet.log">kubelet.log</a><a href="https://foo.com/proxy/minion/minion1:8080/whatever/apt/google.log">google.log</a>`,
},
"image": {
input: `<pre><img src="kubernetes.jpg"/></pre>`,
sourceURL: "http://myminion.com/",
transport: testTransport,
output: `<pre><img src="http://foo.com/proxy/minion/minion1:10250/kubernetes.jpg"/></pre>`,
},
"abs": {
input: `<script src="http://google.com/kubernetes.js"/>`,
sourceURL: "http://myminion.com/any/path/",
transport: testTransport,
output: `<script src="http://google.com/kubernetes.js"/>`,
},
"abs but same host": {
input: `<script src="http://myminion.com/kubernetes.js"/>`,
sourceURL: "http://myminion.com/any/path/",
transport: testTransport,
output: `<script src="http://foo.com/proxy/minion/minion1:10250/kubernetes.js"/>`,
},
}
for name, item := range table {
// Canonicalize the html so we can diff.
item.input = fmtHTML(item.input)
item.output = fmtHTML(item.output)
req := &http.Request{
Method: "GET",
URL: parseURLOrDie(item.sourceURL),
}
resp := &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(item.input)),
Close: true,
}
updatedResp, err := item.transport.fixLinks(req, resp)
if err != nil {
t.Errorf("%v: Unexpected error: %v", name, err)
continue
}
body, err := ioutil.ReadAll(updatedResp.Body)
if err != nil {
t.Errorf("%v: Unexpected error: %v", name, err)
continue
}
if e, a := item.output, string(body); e != a {
t.Errorf("%v: expected %v, but got %v", name, e, a)
}
}
}
func TestProxy(t *testing.T) {
table := []struct {
method string
path string
reqBody string
respBody string
}{
{"GET", "/some/dir", "", "answer"},
{"POST", "/some/other/dir", "question", "answer"},
{"PUT", "/some/dir/id", "different question", "answer"},
{"DELETE", "/some/dir/id", "", "ok"},
}
for _, item := range table {
proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
gotBody, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Errorf("%v - unexpected error %v", item.method, err)
}
if e, a := item.reqBody, string(gotBody); e != a {
t.Errorf("%v - expected %v, got %v", item.method, e, a)
}
fmt.Fprint(w, item.respBody)
}))
simpleStorage := &SimpleRESTStorage{
errors: map[string]error{},
resourceLocation: proxyServer.URL,
}
handler := Handle(map[string]RESTStorage{
"foo": simpleStorage,
}, codec, "/prefix/version")
server := httptest.NewServer(handler)
req, err := http.NewRequest(
item.method,
server.URL+"/prefix/version/proxy/foo/id"+item.path,
strings.NewReader(item.reqBody),
)
if err != nil {
t.Errorf("%v - unexpected error %v", item.method, err)
continue
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Errorf("%v - unexpected error %v", item.method, err)
continue
}
gotResp, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("%v - unexpected error %v", item.method, err)
}
resp.Body.Close()
if e, a := item.respBody, string(gotResp); e != a {
t.Errorf("%v - expected %v, got %v", item.method, e, a)
}
}
}

View File

@ -51,6 +51,7 @@ func TestRedirect(t *testing.T) {
for _, item := range table {
simpleStorage.errors["resourceLocation"] = item.err
simpleStorage.resourceLocation = item.id
resp, err := client.Get(server.URL + "/prefix/version/redirect/foo/" + item.id)
if resp == nil {
t.Fatalf("Unexpected nil resp")

View File

@ -180,7 +180,7 @@ func (rs *RegistryStorage) ResourceLocation(id string) (string, error) {
if len(e.Endpoints) == 0 {
return "", fmt.Errorf("no endpoints available for %v", id)
}
return e.Endpoints[rand.Intn(len(e.Endpoints))], nil
return "http://" + e.Endpoints[rand.Intn(len(e.Endpoints))], nil
}
func (rs *RegistryStorage) deleteExternalLoadBalancer(service *api.Service) error {

View File

@ -291,7 +291,7 @@ func TestServiceRegistryResourceLocation(t *testing.T) {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if e, a := "foo:80", location; e != a {
if e, a := "http://foo:80", location; e != a {
t.Errorf("Expected %v, but got %v", e, a)
}
if e, a := "foo", registry.GottenID; e != a {