mirror of https://github.com/helm/helm
This modifies the startup and initial handling of the REST API to make it modular, remove the init() methods, and remove reliance on global vars.pull/363/head
parent
b4c8e3c79d
commit
87d360afda
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors 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 router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Encoder interface {
|
||||||
|
// Encode encoders a given response
|
||||||
|
//
|
||||||
|
// When an encoder fails, it logs any necessary data and then responds to
|
||||||
|
// the client.
|
||||||
|
Encode(http.ResponseWriter, *http.Request, *Context, interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptEncodder uses the accept headers on a request to determine the response type.
|
||||||
|
//
|
||||||
|
// It supports the following encodings:
|
||||||
|
// - application/json: passed to encoding/json.Marshal
|
||||||
|
// - text/yaml: passed to gopkg.in/yaml.v2.Marshal
|
||||||
|
// - text/plain: passed to fmt.Sprintf("%V")
|
||||||
|
type AcceptEncoder struct {
|
||||||
|
DefaultEncoding string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodeds the given interface to the first available type in the Accept header.
|
||||||
|
func (e *AcceptEncoder) Encode(w http.ResponseWriter, r *http.Request, c *Context, out interface{}) {
|
||||||
|
a := r.Header.Get("accept")
|
||||||
|
fn := encoders[e.DefaultEncoding]
|
||||||
|
mt := e.DefaultEncoding
|
||||||
|
if a != "" {
|
||||||
|
mt, fn = e.parseAccept(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fn(out)
|
||||||
|
if err != nil {
|
||||||
|
Fatal(w, r, "Could not marshal data: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("content-type", mt)
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAccept parses the value of an Accept: header and returns the best match.
|
||||||
|
//
|
||||||
|
// This returns the matched MIME type and the Marshal function.
|
||||||
|
func (e *AcceptEncoder) parseAccept(h string) (string, Marshaler) {
|
||||||
|
|
||||||
|
keys := strings.Split(h, ",")
|
||||||
|
for _, k := range keys {
|
||||||
|
mt, _, err := mime.ParseMediaType(k)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enc, ok := encoders[mt]; ok {
|
||||||
|
return mt, enc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.DefaultEncoding, encoders[e.DefaultEncoding]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Marshaler func(interface{}) ([]byte, error)
|
||||||
|
|
||||||
|
var encoders = map[string]Marshaler{
|
||||||
|
"application/json": json.Marshal,
|
||||||
|
"text/yaml": yaml.Marshal,
|
||||||
|
"application/x-yaml": yaml.Marshal,
|
||||||
|
"text/plain": textMarshal,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUnsupportedKind = errors.New("unsupported kind")
|
||||||
|
|
||||||
|
// textMarshal marshals v into a text representation ONLY IN NARROW CASES.
|
||||||
|
//
|
||||||
|
// An error will have its Error() method called.
|
||||||
|
// A fmt.Stringer will have its String() method called.
|
||||||
|
// Scalar types will be marshaled with fmt.Sprintf("%v").
|
||||||
|
//
|
||||||
|
// This will only marshal scalar types for securoty reasons (namely, we don't
|
||||||
|
// want the possibility of forcing exposure of non-exported data or ptr
|
||||||
|
// addresses, etc.)
|
||||||
|
func textMarshal(v interface{}) ([]byte, error) {
|
||||||
|
switch s := v.(type) {
|
||||||
|
case error:
|
||||||
|
return []byte(s.Error()), nil
|
||||||
|
case fmt.Stringer:
|
||||||
|
return []byte(s.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error on kinds we don't support.
|
||||||
|
val := reflect.Indirect(reflect.ValueOf(v))
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Invalid, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface,
|
||||||
|
reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
|
||||||
|
return []byte{}, ErrUnsupportedKind
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf("%v", v)), nil
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors 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 router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Encoder = &AcceptEncoder{}
|
||||||
|
|
||||||
|
func TestParseAccept(t *testing.T) {
|
||||||
|
e := &AcceptEncoder{
|
||||||
|
DefaultEncoding: "application/json",
|
||||||
|
}
|
||||||
|
tests := map[string]string{
|
||||||
|
"": e.DefaultEncoding,
|
||||||
|
"*/*": e.DefaultEncoding,
|
||||||
|
// To stay true to spec, this _should_ be an error. But our thought
|
||||||
|
// on this case is that we'd rather send a default format.
|
||||||
|
"audio/*; q=0.2, audio/basic": e.DefaultEncoding,
|
||||||
|
"text/html; q=0.8, text/yaml,application/json": "text/yaml",
|
||||||
|
"application/x-yaml; foo=bar": "application/x-yaml",
|
||||||
|
"text/monkey, TEXT/YAML ; zoom=zoom ": "text/yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for in, expects := range tests {
|
||||||
|
mt, enc := e.parseAccept(in)
|
||||||
|
if mt != expects {
|
||||||
|
t.Errorf("Expected %q, got %q", expects, mt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := enc([]string{"hello", "world"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextMarshal(t *testing.T) {
|
||||||
|
tests := map[string]interface{}{
|
||||||
|
"foo": "foo",
|
||||||
|
"5": 5,
|
||||||
|
"stinky cheese": errors.New("stinky cheese"),
|
||||||
|
}
|
||||||
|
for expect, in := range tests {
|
||||||
|
if o, err := textMarshal(in); err != nil || string(o) != expect {
|
||||||
|
t.Errorf("Expected %q, got %q", expect, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := textMarshal(struct{ foo int }{5}); err != ErrUnsupportedKind {
|
||||||
|
t.Fatalf("Expected unsupported kind, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcceptEncoder(t *testing.T) {
|
||||||
|
c := &Context{
|
||||||
|
Encoder: &AcceptEncoder{DefaultEncoding: "application/json"},
|
||||||
|
}
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request, c *Context) error {
|
||||||
|
c.Encoder.Encode(w, r, c, []string{"hello", "world"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := httpHarness(c, "GET /", fn)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
res, err := http.Get(s.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
t.Fatalf("Unexpected response code %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
if mt := res.Header.Get("content-type"); mt != "application/json" {
|
||||||
|
t.Errorf("Unexpected content type: %q", mt)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read response body: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := []string{}
|
||||||
|
if err := json.Unmarshal(data, &out); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal JSON: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out[0] != "hello" {
|
||||||
|
t.Fatalf("Unexpected JSON data in slot 0: %s", out[0])
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors 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 router is an HTTP router.
|
||||||
|
|
||||||
|
This router provides appropriate dependency injection/encapsulation for the
|
||||||
|
HTTP routing layer. This removes the requirement to set global variables for
|
||||||
|
resources like database handles.
|
||||||
|
|
||||||
|
This library does not replace the default HTTP mux because there is no need.
|
||||||
|
Instead, it implements an HTTP handler.
|
||||||
|
|
||||||
|
It then defines a handler function that is given a context as well as a
|
||||||
|
request and response.
|
||||||
|
*/
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Masterminds/httputil"
|
||||||
|
"github.com/kubernetes/deployment-manager/cmd/manager/manager"
|
||||||
|
"github.com/kubernetes/deployment-manager/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LogAccess = "Access: %s %s"
|
||||||
|
|
||||||
|
// Config holds the global configuration parameters passed into the router.
|
||||||
|
//
|
||||||
|
// Config is used concurrently. Once a config is created, it should be treated
|
||||||
|
// as immutable.
|
||||||
|
type Config struct {
|
||||||
|
// Address is the host and port (:8080)
|
||||||
|
Address string
|
||||||
|
// MaxTemplateLength is the maximum length of a template.
|
||||||
|
MaxTemplateLength int64
|
||||||
|
// ExpanderName is the DNS name of the expansion service.
|
||||||
|
ExpanderName string
|
||||||
|
// ExpanderURL is the expander service's URL.
|
||||||
|
ExpanderURL string
|
||||||
|
// DeployerName is the deployer's DNS name
|
||||||
|
DeployerName string
|
||||||
|
// DeployerURL is the deployer's URL
|
||||||
|
DeployerURL string
|
||||||
|
// CredentialFile is the file to the credentials.
|
||||||
|
CredentialFile string
|
||||||
|
// CredentialSecrets tells the service to use a secrets file instead.
|
||||||
|
CredentialSecrets bool
|
||||||
|
// MongoName is the DNS name of the mongo server.
|
||||||
|
MongoName string
|
||||||
|
// MongoPort is the port for the MongoDB protocol on the mongo server.
|
||||||
|
// It is a string for historical reasons.
|
||||||
|
MongoPort string
|
||||||
|
// MongoAddress is the name and port.
|
||||||
|
MongoAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context contains dependencies that are passed to each handler function.
|
||||||
|
//
|
||||||
|
// Context carries typed information, often scoped to interfaces, so that the
|
||||||
|
// caller's contract with the service is known at compile time.
|
||||||
|
//
|
||||||
|
// Members of the context must be concurrency safe.
|
||||||
|
type Context struct {
|
||||||
|
Config *Config
|
||||||
|
// Manager is a deployment-manager/manager/manager.Manager
|
||||||
|
Manager manager.Manager
|
||||||
|
Encoder Encoder
|
||||||
|
CredentialProvider common.CredentialProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Log(msg string, v ...interface{}) {
|
||||||
|
// FIXME: This should be configurable via the context.
|
||||||
|
fmt.Fprintf(os.Stdout, msg+"\n", v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Err(msg string, v ...interface{}) {
|
||||||
|
// FIXME: This should be configurable via the context.
|
||||||
|
fmt.Fprintf(os.Stderr, msg+"\n", v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFound writes a 404 error to the client and logs an error.
|
||||||
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Log this.
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintln(w, "File Not Found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal writes a 500 response to the client and logs the message.
|
||||||
|
//
|
||||||
|
// Additional arguments are past into the the formatter as params to msg.
|
||||||
|
func Fatal(w http.ResponseWriter, r *http.Request, msg string, v ...interface{}) {
|
||||||
|
// TODO: Log this.
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintln(w, "Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerFunc responds to an individual HTTP request.
|
||||||
|
//
|
||||||
|
// Returned errors will be captured, logged, and returned as HTTP 500 errors.
|
||||||
|
type HandlerFunc func(w http.ResponseWriter, r *http.Request, c *Context) error
|
||||||
|
|
||||||
|
// Handler implements an http.Handler.
|
||||||
|
//
|
||||||
|
// This is the top level route handler.
|
||||||
|
type Handler struct {
|
||||||
|
c *Context
|
||||||
|
resolver *httputil.Resolver
|
||||||
|
routes Routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new Handler.
|
||||||
|
//
|
||||||
|
// Routes cannot be modified after construction. The order that the route
|
||||||
|
// names are returned by Routes.Paths() determines the lookup order.
|
||||||
|
func NewHandler(c *Context, r Routes) *Handler {
|
||||||
|
paths := make([]string, r.Len())
|
||||||
|
i := 0
|
||||||
|
for _, k := range r.Paths() {
|
||||||
|
paths[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Handler{
|
||||||
|
c: c,
|
||||||
|
resolver: httputil.NewResolver(paths),
|
||||||
|
routes: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP serves an HTTP request.
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.c.Log(LogAccess, r.Method, r.URL)
|
||||||
|
route, err := h.resolver.Resolve(r)
|
||||||
|
if err != nil {
|
||||||
|
NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, ok := h.routes.Get(route)
|
||||||
|
if !ok {
|
||||||
|
Fatal(w, r, "route %s missing", route)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(w, r, h.c); err != nil {
|
||||||
|
Fatal(w, r, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routes defines a container for route-to-function mapping.
|
||||||
|
type Routes interface {
|
||||||
|
Add(string, HandlerFunc)
|
||||||
|
Get(string) (HandlerFunc, bool)
|
||||||
|
Len() int
|
||||||
|
Paths() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoutes creates a default implementation of a Routes.
|
||||||
|
//
|
||||||
|
// The ordering of routes is nonderministic.
|
||||||
|
func NewRoutes() Routes {
|
||||||
|
return routeMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeMap map[string]HandlerFunc
|
||||||
|
|
||||||
|
func (r routeMap) Add(name string, fn HandlerFunc) {
|
||||||
|
r[name] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeMap) Get(name string) (HandlerFunc, bool) {
|
||||||
|
f, ok := r[name]
|
||||||
|
return f, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeMap) Len() int {
|
||||||
|
return len(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r routeMap) Paths() []string {
|
||||||
|
b := make([]string, len(r))
|
||||||
|
i := 0
|
||||||
|
for k := range r {
|
||||||
|
b[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors 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 router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Canary
|
||||||
|
var v Routes = routeMap{}
|
||||||
|
|
||||||
|
func TestHandler(t *testing.T) {
|
||||||
|
c := &Context{}
|
||||||
|
r := NewRoutes()
|
||||||
|
|
||||||
|
r.Add("GET /", func(w http.ResponseWriter, r *http.Request, c *Context) error {
|
||||||
|
fmt.Fprintln(w, "hello")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
r.Add("POST /", func(w http.ResponseWriter, r *http.Request, c *Context) error {
|
||||||
|
fmt.Fprintln(w, "goodbye")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
h := NewHandler(c, r)
|
||||||
|
|
||||||
|
s := httptest.NewServer(h)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
res, err := http.Get(s.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "hello\n" != string(data) {
|
||||||
|
t.Errorf("Expected 'hello', got %q", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpHarness is a simple test server fixture.
|
||||||
|
// Simple fixture for standing up a test server with a single route.
|
||||||
|
//
|
||||||
|
// You must Close() the returned server.
|
||||||
|
func httpHarness(c *Context, route string, fn HandlerFunc) *httptest.Server {
|
||||||
|
r := NewRoutes()
|
||||||
|
r.Add(route, fn)
|
||||||
|
h := NewHandler(c, r)
|
||||||
|
return httptest.NewServer(h)
|
||||||
|
}
|
Loading…
Reference in new issue