From a07dbf8752c8e33f76ad0075c61bee19dceedf57 Mon Sep 17 00:00:00 2001 From: Dave Cunningham Date: Thu, 24 Mar 2016 14:02:27 -0400 Subject: [PATCH] Factor expansion service logic as an auxiliary library --- cmd/expandybird/expander/expander.go | 22 +--- cmd/expandybird/main.go | 23 ++-- cmd/expandybird/service/service.go | 90 ------------- pkg/expansion/service.go | 92 ++++++++++++++ .../service => pkg/expansion}/service_test.go | 118 ++++++++++++------ pkg/expansion/validate.go | 45 +++++++ 6 files changed, 232 insertions(+), 158 deletions(-) delete mode 100644 cmd/expandybird/service/service.go create mode 100644 pkg/expansion/service.go rename {cmd/expandybird/service => pkg/expansion}/service_test.go (65%) create mode 100644 pkg/expansion/validate.go diff --git a/cmd/expandybird/expander/expander.go b/cmd/expandybird/expander/expander.go index 462692d23..6a4a7fb6a 100644 --- a/cmd/expandybird/expander/expander.go +++ b/cmd/expandybird/expander/expander.go @@ -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)) } diff --git a/cmd/expandybird/main.go b/cmd/expandybird/main.go index aa2536c18..7a54cabed 100644 --- a/cmd/expandybird/main.go +++ b/cmd/expandybird/main.go @@ -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()) } diff --git a/cmd/expandybird/service/service.go b/cmd/expandybird/service/service.go deleted file mode 100644 index 53669e140..000000000 --- a/cmd/expandybird/service/service.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -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 service - -import ( - "github.com/kubernetes/helm/pkg/expansion" - "github.com/kubernetes/helm/pkg/util" - - "errors" - "fmt" - "net/http" - - restful "github.com/emicklei/go-restful" -) - -// A Service wraps a web service that performs template expansion. -type Service struct { - *restful.WebService -} - -// 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 { - 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{} - if err := req.ReadEntity(&request); err != nil { - logAndReturnErrorFromHandler(http.StatusBadRequest, err.Error(), resp) - return - } - - response, err := backend.ExpandChart(request) - if err != nil { - message := fmt.Sprintf("error expanding chart: %s", err) - logAndReturnErrorFromHandler(http.StatusBadRequest, message, resp) - return - } - - util.LogHandlerExit("expandybird", http.StatusOK, "OK", resp.ResponseWriter) - message := fmt.Sprintf("\nResources:\n%s\n", response.Resources) - util.LogHandlerText("expandybird", message) - resp.WriteEntity(response) - } -} - -func logAndReturnErrorFromHandler(statusCode int, message string, resp *restful.Response) { - util.LogHandlerExit("expandybird: expand", statusCode, message, resp.ResponseWriter) - resp.WriteError(statusCode, errors.New(message)) -} diff --git a/pkg/expansion/service.go b/pkg/expansion/service.go new file mode 100644 index 000000000..e7c135b75 --- /dev/null +++ b/pkg/expansion/service.go @@ -0,0 +1,92 @@ +/* +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 ( + "github.com/kubernetes/helm/pkg/util" + + "errors" + "fmt" + "net/http" + + restful "github.com/emicklei/go-restful" +) + +// A Service wraps a web service that performs template expansion. +type Service struct { + webService *restful.WebService + server *http.Server + container *restful.Container +} + +// 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) + 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 { + badRequest(resp, err.Error()) + return + } + response, err := backend.ExpandChart(request) + if err != nil { + badRequest(resp, fmt.Sprintf("error expanding chart: %s", err)) + return + } + util.LogHandlerExit("expansion service", http.StatusOK, "OK", resp.ResponseWriter) + message := fmt.Sprintf("\nResources:\n%s\n", response.Resources) + 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 badRequest(resp *restful.Response, message string) { + statusCode := http.StatusBadRequest + util.LogHandlerExit("expansion service", statusCode, message, resp.ResponseWriter) + resp.WriteError(statusCode, errors.New(message)) +} diff --git a/cmd/expandybird/service/service_test.go b/pkg/expansion/service_test.go similarity index 65% rename from cmd/expandybird/service/service_test.go rename to pkg/expansion/service_test.go index 030e96457..8b6db35fe 100644 --- a/cmd/expandybird/service/service_test.go +++ b/pkg/expansion/service_test.go @@ -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 } diff --git a/pkg/expansion/validate.go b/pkg/expansion/validate.go new file mode 100644 index 000000000..02f97c0d9 --- /dev/null +++ b/pkg/expansion/validate.go @@ -0,0 +1,45 @@ +/* +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 +}