Factor expansion service logic as an auxiliary library

pull/457/head
Dave Cunningham 9 years ago
parent 56125b94f6
commit a07dbf8752

@ -48,28 +48,16 @@ type expandyBirdOutput struct {
// ExpandChart passes the given configuration to the expander and returns the // ExpandChart passes the given configuration to the expander and returns the
// expanded configuration as a string on success. // expanded configuration as a string on success.
func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) { func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) {
if request.ChartInvocation == nil {
return nil, fmt.Errorf("Request does not have invocation field") err := expansion.ValidateRequest(request)
} if err != nil {
if request.Chart == nil { return nil, err
return nil, fmt.Errorf("Request does not have chart field")
} }
chartInv := request.ChartInvocation chartInv := request.ChartInvocation
chartFile := request.Chart.Chartfile chartFile := request.Chart.Chartfile
chartMembers := request.Chart.Members 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" { if chartFile.Expander.Name != "ExpandyBird" {
message := fmt.Sprintf("ExpandyBird cannot do this kind of expansion: ", chartFile.Expander.Name) message := fmt.Sprintf("ExpandyBird cannot do this kind of expansion: ", chartFile.Expander.Name)
return nil, fmt.Errorf("%s: %s", chartInv.Name, message) 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 name = chartInv.Type
} else if i == schemaIndex { } else if i == schemaIndex {
// Doesn't matter what it was originally called, expandyBird expects to find it here. // 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)) cmd.Args = append(cmd.Args, name, path, string(f.Content))
} }

@ -18,17 +18,16 @@ package main
import ( import (
"github.com/kubernetes/helm/cmd/expandybird/expander" "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" "github.com/kubernetes/helm/pkg/version"
"flag" "flag"
"fmt"
"log" "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 // port that we are going to listen on
var port = flag.Int("port", 8080, "Port 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() { func main() {
flag.Parse() flag.Parse()
backend := expander.NewExpander(*expansionBinary) backend := expander.NewExpander(*expansionBinary)
wrapper := service.NewService(service.NewExpansionHandler(backend)) service := expansion.NewService(*address, *port, backend)
address := fmt.Sprintf(":%d", *port)
container := restful.DefaultContainer
server := &http.Server{
Addr: address,
Handler: container,
}
wrapper.Register(container)
log.Printf("Version: %s", version.Version) log.Printf("Version: %s", version.Version)
log.Printf("Listening on %s...", address) log.Printf("Listening on http://%s:%s/expand", *address, port)
log.Fatal(server.ListenAndServe()) log.Fatal(service.ListenAndServe())
} }

@ -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))
}

@ -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))
}

@ -14,65 +14,113 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package service package expansion
/*
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"reflect" "reflect"
"testing" "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/common"
"github.com/kubernetes/helm/pkg/util" "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 { // A FakeExpander returns testResponse if it was given testRequest, otherwise raises an error.
template, err := util.NewTemplateFromFileNames(templateFileName, importFileNames) type FakeExpander struct {
if err != nil {
t.Errorf("cannot create template for test case (%s): %s\n", err, description)
} }
templateData, err := json.Marshal(template) func (fake *FakeExpander) ExpandChart(req *ServiceRequest) (*ServiceResponse, error) {
if err != nil { if reflect.DeepEqual(req, testRequest) {
t.Errorf("cannot marshal template for test case (%s): %s\n", err, description) return testResponse, nil
}
return nil, fmt.Errorf("Test Error Response")
} }
reader := bytes.NewReader(templateData) func wrapReader(value interface{}) (io.Reader, error) {
return reader valueJSON, err := json.Marshal(value)
if err != nil {
return nil, err
}
return bytes.NewReader(valueJSON), nil
} }
func GetOutputString(t *testing.T, description string) string { func GeneralTest(t *testing.T, httpMeth string, url string, contentType string, req *ServiceRequest,
output, err := ioutil.ReadFile(outputFileName) 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 { 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 ( func TestInvalidURL(t *testing.T) {
httpGETMethod = "GET" GeneralTest(t, "POST", "/erroneus", "application/json", testRequest, nil, http.StatusNotFound)
httpPOSTMethod = "POST" }
validServiceURL = "/expand"
invalidServiceURL = "http://localhost:8080/invalidurlpath"
jsonContentType = "application/json"
invalidContentType = "invalid/content-type"
inputFileName = "../test/ValidContent.yaml"
outputFileName = "../test/ExpectedOutput.yaml"
)
var importFileNames = []string{ func TestInvalidMimeType(t *testing.T) {
"../test/replicatedservice.py", 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 { type ServiceWrapperTestCase struct {
Description string Description string
HTTPMethod string HTTPMethod string

@ -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
}
Loading…
Cancel
Save