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