Commit e7119a92 authored by Jack Greenfield's avatar Jack Greenfield

Merge pull request #457 from sparkprime/expansion_service

Factor expansion service logic as an auxiliary library
parents 69850482 a07dbf87
......@@ -48,28 +48,16 @@ type expandyBirdOutput struct {
// ExpandChart passes the given configuration to the expander and returns the
// expanded configuration as a string on success.
func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) {
if request.ChartInvocation == nil {
return nil, fmt.Errorf("Request does not have invocation field")
}
if request.Chart == nil {
return nil, fmt.Errorf("Request does not have chart field")
err := expansion.ValidateRequest(request)
if err != nil {
return nil, err
}
chartInv := request.ChartInvocation
chartFile := request.Chart.Chartfile
chartMembers := request.Chart.Members
if chartInv.Type != chartFile.Name {
return nil, fmt.Errorf("Request chart invocation does not match provided chart")
}
schemaName := chartInv.Type + ".schema"
if chartFile.Expander == nil {
message := fmt.Sprintf("Chart JSON does not have expander field")
return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
}
if chartFile.Expander.Name != "ExpandyBird" {
message := fmt.Sprintf("ExpandyBird cannot do this kind of expansion: ", chartFile.Expander.Name)
return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
......@@ -132,7 +120,7 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se
name = chartInv.Type
} else if i == schemaIndex {
// Doesn't matter what it was originally called, expandyBird expects to find it here.
name = schemaName
name = chartInv.Type + ".schema"
}
cmd.Args = append(cmd.Args, name, path, string(f.Content))
}
......
......@@ -18,17 +18,16 @@ package main
import (
"github.com/kubernetes/helm/cmd/expandybird/expander"
"github.com/kubernetes/helm/cmd/expandybird/service"
"github.com/kubernetes/helm/pkg/expansion"
"github.com/kubernetes/helm/pkg/version"
"flag"
"fmt"
"log"
"net/http"
restful "github.com/emicklei/go-restful"
)
// interface that we are going to listen on
var address = flag.String("address", "", "Interface to listen on")
// port that we are going to listen on
var port = flag.Int("port", 8080, "Port to listen on")
......@@ -39,16 +38,8 @@ var expansionBinary = flag.String("expansion_binary", "../../../expansion/expans
func main() {
flag.Parse()
backend := expander.NewExpander(*expansionBinary)
wrapper := service.NewService(service.NewExpansionHandler(backend))
address := fmt.Sprintf(":%d", *port)
container := restful.DefaultContainer
server := &http.Server{
Addr: address,
Handler: container,
}
wrapper.Register(container)
service := expansion.NewService(*address, *port, backend)
log.Printf("Version: %s", version.Version)
log.Printf("Listening on %s...", address)
log.Fatal(server.ListenAndServe())
log.Printf("Listening on http://%s:%s/expand", *address, port)
log.Fatal(service.ListenAndServe())
}
......@@ -5,7 +5,7 @@ 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
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,
......@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service
package expansion
import (
"github.com/kubernetes/helm/pkg/expansion"
"github.com/kubernetes/helm/pkg/util"
"errors"
......@@ -29,62 +28,65 @@ import (
// A Service wraps a web service that performs template expansion.
type Service struct {
*restful.WebService
webService *restful.WebService
server *http.Server
container *restful.Container
}
// NewService creates and returns a new Service, initialized with a new
// restful.WebService configured with a route that dispatches to the supplied
// handler. The new Service must be registered before accepting traffic by
// calling Register.
func NewService(handler restful.RouteFunction) *Service {
// NewService encapsulates code to open an HTTP server on the given address:port that serves the
// expansion API using the given Expander backend to do the actual expansion. After calling
// NewService, call ListenAndServe to start the returned service.
func NewService(address string, port int, backend Expander) *Service {
restful.EnableTracing(true)
webService := new(restful.WebService)
webService.Consumes(restful.MIME_JSON, restful.MIME_XML)
webService.Produces(restful.MIME_JSON, restful.MIME_XML)
webService.Route(webService.POST("/expand").To(handler).
Doc("Expand a template.").
Reads(&expansion.ServiceRequest{}).
Writes(&expansion.ServiceResponse{}))
return &Service{webService}
}
// Register adds the web service wrapped by the Service to the supplied
// container. If the supplied container is nil, then the default container is
// used, instead.
func (s *Service) Register(container *restful.Container) {
if container == nil {
container = restful.DefaultContainer
}
container.Add(s.WebService)
}
// NewExpansionHandler returns a route function that handles an incoming
// template expansion request, bound to the supplied expander.
func NewExpansionHandler(backend expansion.Expander) restful.RouteFunction {
return func(req *restful.Request, resp *restful.Response) {
util.LogHandlerEntry("expandybird: expand", req.Request)
request := &expansion.ServiceRequest{}
webService.Consumes(restful.MIME_JSON)
webService.Produces(restful.MIME_JSON)
handler := func(req *restful.Request, resp *restful.Response) {
util.LogHandlerEntry("expansion service", req.Request)
request := &ServiceRequest{}
if err := req.ReadEntity(&request); err != nil {
logAndReturnErrorFromHandler(http.StatusBadRequest, err.Error(), resp)
badRequest(resp, err.Error())
return
}
response, err := backend.ExpandChart(request)
if err != nil {
message := fmt.Sprintf("error expanding chart: %s", err)
logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp)
badRequest(resp, fmt.Sprintf("error expanding chart: %s", err))
return
}
util.LogHandlerExit("expandybird", http.StatusOK, "OK", resp.ResponseWriter)
util.LogHandlerExit("expansion service", http.StatusOK, "OK", resp.ResponseWriter)
message := fmt.Sprintf("\nResources:\n%s\n", response.Resources)
util.LogHandlerText("expandybird", message)
util.LogHandlerText("expansion service", message)
resp.WriteEntity(response)
}
webService.Route(
webService.POST("/expand").
To(handler).
Doc("Expand a chart.").
Reads(&ServiceRequest{}).
Writes(&ServiceResponse{}))
container := restful.DefaultContainer
container.Add(webService)
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", address, port),
Handler: container,
}
return &Service{
webService: webService,
server: server,
container: container,
}
}
// ListenAndServe blocks forever, handling expansion requests.
func (s *Service) ListenAndServe() error {
return s.server.ListenAndServe()
}
func logAndReturnErrorFromHandler(statusCode int, message string, resp *restful.Response) {
util.LogHandlerExit("expandybird: expand", statusCode, message, resp.ResponseWriter)
func badRequest(resp *restful.Response, message string) {
statusCode := http.StatusBadRequest
util.LogHandlerExit("expansion service", statusCode, message, resp.ResponseWriter)
resp.WriteError(statusCode, errors.New(message))
}
......@@ -14,71 +14,119 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package service
package expansion
/*
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"testing"
"github.com/kubernetes/helm/cmd/expandybird/expander"
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util"
)
restful "github.com/emicklei/go-restful"
var (
testRequest = &ServiceRequest{
ChartInvocation: &common.Resource{
Name: "test_invocation",
Type: "Test Chart",
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Name: "TestChart",
Expander: &chart.Expander{
Name: "FakeExpander",
Entrypoint: "None",
},
},
Members: []*chart.Member{
{
Path: "templates/testfile",
Content: []byte("test"),
},
},
},
}
testResponse = &ServiceResponse{
Resources: []interface{}{"test"},
}
)
func GetTemplateReader(t *testing.T, description string, templateFileName string) io.Reader {
template, err := util.NewTemplateFromFileNames(templateFileName, importFileNames)
if err != nil {
t.Errorf("cannot create template for test case (%s): %s\n", err, description)
// A FakeExpander returns testResponse if it was given testRequest, otherwise raises an error.
type FakeExpander struct {
}
func (fake *FakeExpander) ExpandChart(req *ServiceRequest) (*ServiceResponse, error) {
if reflect.DeepEqual(req, testRequest) {
return testResponse, nil
}
return nil, fmt.Errorf("Test Error Response")
}
templateData, err := json.Marshal(template)
func wrapReader(value interface{}) (io.Reader, error) {
valueJSON, err := json.Marshal(value)
if err != nil {
t.Errorf("cannot marshal template for test case (%s): %s\n", err, description)
return nil, err
}
reader := bytes.NewReader(templateData)
return reader
return bytes.NewReader(valueJSON), nil
}
func GetOutputString(t *testing.T, description string) string {
output, err := ioutil.ReadFile(outputFileName)
func GeneralTest(t *testing.T, httpMeth string, url string, contentType string, req *ServiceRequest,
expResponse *ServiceResponse, expStatus int) {
service := NewService("127.0.0.1", 8080, &FakeExpander{})
handlerTester := util.NewHandlerTester(service.container)
reader, err := wrapReader(testRequest)
if err != nil {
t.Fatalf("unexpected error: %s\n", err)
}
w, err := handlerTester(httpMeth, url, contentType, reader)
if err != nil {
t.Errorf("cannot read output file for test case (%s): %s\n", err, description)
t.Fatalf("unexpected error: %s\n", err)
}
var data = w.Body.Bytes()
if w.Code != expStatus {
t.Fatalf("wrong status code:\nwant: %s\ngot: %s\ncontent: %s\n", expStatus, w.Code, data)
}
if expResponse != nil {
var response ServiceResponse
err = json.Unmarshal(data, &response)
if err != nil {
t.Fatalf("Response could not be unmarshalled: %s\nresponse: %s", err, string(data))
}
if !reflect.DeepEqual(response, *expResponse) {
t.Fatalf("Response did not match.\nwant: %s\ngot: %s\n", expResponse, response)
}
}
}
return string(output)
func TestInvalidMethod(t *testing.T) {
GeneralTest(t, "GET", "/expand", "application/json", nil, nil, http.StatusMethodNotAllowed)
}
const (
httpGETMethod = "GET"
httpPOSTMethod = "POST"
validServiceURL = "/expand"
invalidServiceURL = "http://localhost:8080/invalidurlpath"
jsonContentType = "application/json"
invalidContentType = "invalid/content-type"
inputFileName = "../test/ValidContent.yaml"
outputFileName = "../test/ExpectedOutput.yaml"
)
func TestInvalidURL(t *testing.T) {
GeneralTest(t, "POST", "/erroneus", "application/json", testRequest, nil, http.StatusNotFound)
}
var importFileNames = []string{
"../test/replicatedservice.py",
func TestInvalidMimeType(t *testing.T) {
GeneralTest(t, "POST", "/expand", "erroneus", nil, nil, http.StatusUnsupportedMediaType)
}
func TestExpandOK(t *testing.T) {
GeneralTest(t, "POST", "/expand", "application/json", testRequest, testResponse, http.StatusOK)
}
/*
type ServiceWrapperTestCase struct {
Description string
HTTPMethod string
Description string
HTTPMethod string
ServiceURLPath string
ContentType string
StatusCode int
ContentType string
StatusCode int
}
var ServiceWrapperTestCases = []ServiceWrapperTestCase{
......@@ -161,7 +209,7 @@ func TestServiceWrapper(t *testing.T) {
}
type ExpansionHandlerTestCase struct {
Description string
Description string
TemplateFileName string
}
......
/*
Copyright 2015 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 expansion
import (
"fmt"
)
// ValidateRequest does basic sanity checks on the request.
func ValidateRequest(request *ServiceRequest) error {
if request.ChartInvocation == nil {
return fmt.Errorf("Request does not have invocation field")
}
if request.Chart == nil {
return fmt.Errorf("Request does not have chart field")
}
chartInv := request.ChartInvocation
chartFile := request.Chart.Chartfile
if chartInv.Type != chartFile.Name {
return fmt.Errorf("Request chart invocation does not match provided chart")
}
if chartFile.Expander == nil {
message := fmt.Sprintf("Chart JSON does not have expander field")
return fmt.Errorf("%s: %s", chartInv.Name, message)
}
return nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment