Skip to content

Commit

Permalink
Serverless zanzibar (#676)
Browse files Browse the repository at this point in the history
This introduces a new client-less feature in Zanzibar.  It decouples endpoint code from the downstream. Callers can now specify that their endpoint does not have a downstream.  It is also possible to transform incoming request objects into an outgoing object using a new middleware - transformDummyRequest 

This can be useful for the following scenarios:
1. Rapid development during the initial stages when the data model is not sufficiently clear. 
2. If all the data is already present in the request and there is no need to make a call downstream, this feature can be used to create a client-less endpoint and transform the incoming request into an outgoing response.
  • Loading branch information
tejaswiagarwal committed Dec 18, 2019
2 parents 4abd6ea + d68155d commit 6d20f85
Show file tree
Hide file tree
Showing 33 changed files with 3,942 additions and 14 deletions.
39 changes: 32 additions & 7 deletions codegen/gateway.go
Expand Up @@ -36,8 +36,10 @@ import (
)

const (
reqHeaders = "reqHeaderMap"
resHeaders = "resHeaderMap"
reqHeaders = "reqHeaderMap"
resHeaders = "resHeaderMap"
customWorkflow = "custom"
clientlessWorkflow = "clientless"
)

var mandatoryEndpointFields = []string{
Expand Down Expand Up @@ -255,6 +257,8 @@ type EndpointSpec struct {
// RespTransforms, a map from endpoint response fields to client
// response fields that should override their values.
RespTransforms map[string]FieldMapperEntry `yaml:"-"`
// DummyReqTransforms is used to transform a clientless request to response mapping
DummyReqTransforms map[string]FieldMapperEntry `yaml:"-"`
// ErrTransforms is a map from endpoint exception fields to client exception fields
// that should override their values
// Note that this feature is not yet fully implemented in the stand-alone Zanzibar codebase
Expand All @@ -275,6 +279,8 @@ type EndpointSpec struct {
ClientMethod string `yaml:"clientMethod,omitempty"`
// The client for this endpoint if httpClient or tchannelClient
ClientSpec *ClientSpec `yaml:"-"`
// DummyEndpoint checks if the endpoint is clientless
IsDummyEndpoint bool `yaml:"-"`
}

func ensureFields(config map[string]interface{}, mandatoryFields []string, yamlFile string) error {
Expand Down Expand Up @@ -346,6 +352,7 @@ func NewEndpointSpec(
var workflowImportPath string
var clientID string
var clientMethod string
var isDummyEndpoint bool

workflowType := endpointConfigObj["workflowType"].(string)
if workflowType == "httpClient" || workflowType == "tchannelClient" {
Expand All @@ -355,16 +362,19 @@ func NewEndpointSpec(
"endpoint config %q must have clientName field", yamlFile,
)
}
clientID = iclientID.(string)

if iclientID != nil {
clientID = iclientID.(string)
}
iclientMethod, ok := endpointConfigObj["clientMethod"]
if !ok {
return nil, errors.Errorf(
"endpoint config %q must have clientMethod field", yamlFile,
)
}
clientMethod = iclientMethod.(string)
} else if workflowType == "custom" {
if iclientMethod != nil {
clientMethod = iclientMethod.(string)
}
} else if workflowType == customWorkflow {
iworkflowImportPath, ok := endpointConfigObj["workflowImportPath"]
if !ok {
return nil, errors.Errorf(
Expand All @@ -373,6 +383,8 @@ func NewEndpointSpec(
)
}
workflowImportPath = iworkflowImportPath.(string)
} else if workflowType == clientlessWorkflow {
isDummyEndpoint = true
} else {
return nil, errors.Errorf(
"Invalid workflowType %q for endpoint %q",
Expand Down Expand Up @@ -418,6 +430,7 @@ func NewEndpointSpec(
ThriftMethodName: parts[1],
WorkflowType: workflowType,
WorkflowImportPath: workflowImportPath,
IsDummyEndpoint: isDummyEndpoint,
ClientID: clientID,
ClientMethod: clientMethod,
}
Expand Down Expand Up @@ -711,6 +724,14 @@ func augmentEndpointSpec(
espec.RespTransforms = resTransforms
continue
}
if name == "transformDummyReq" {
dummyResTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.DummyReqTransforms = dummyResTransforms
continue
}
if name == "transformError" {
errTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
Expand Down Expand Up @@ -912,10 +933,14 @@ func (e *EndpointSpec) SetDownstream(
clientModules []*ClientSpec,
h *PackageHelper,
) error {
if e.WorkflowType == "custom" {
if e.WorkflowType == customWorkflow {
return nil
}

if e.WorkflowType == clientlessWorkflow {
return e.ModuleSpec.SetDownstream(e, h)
}

var clientSpec *ClientSpec
for _, v := range clientModules {
if v.ClientID == e.ClientID {
Expand Down
58 changes: 58 additions & 0 deletions codegen/method.go
Expand Up @@ -137,6 +137,9 @@ type MethodSpec struct {
// Statements for converting response types
ConvertResponseGoStatements []string

// Statements for converting Dummy request types
ConvertDummyRequestGoStatements []string

// Statements for propagating headers to client requests
PropagateHeadersGoStatements []string

Expand Down Expand Up @@ -1000,6 +1003,61 @@ func (ms *MethodSpec) setTypeConverters(
return nil
}

func (ms *MethodSpec) setDummyTypeConverters(
funcSpec *compile.FunctionSpec,
reqTransforms map[string]FieldMapperEntry,
headersPropagate map[string]FieldMapperEntry,
respTransforms map[string]FieldMapperEntry,
dummyReqTransforms map[string]FieldMapperEntry,
h *PackageHelper,
) error {

dummyConverter := NewTypeConverter(h, nil)

respType := funcSpec.ResultSpec.ReturnType

dummyConverter.append(
"func convert",
PascalCase(ms.Name),
"DummyResponse(in ", ms.RequestType, ") ", ms.ResponseType, "{")

structType := compile.FieldGroup(funcSpec.ArgsSpec)

if respType == nil {
return nil
}

switch respType.(type) {
case
*compile.BoolSpec,
*compile.I8Spec,
*compile.I16Spec,
*compile.I32Spec,
*compile.EnumSpec,
*compile.I64Spec,
*compile.DoubleSpec,
*compile.StringSpec:

// TODO: Add support for primitive type by mapping the first field from request to response
return errors.Errorf(
"clientless endpoints need a complex return type")
default:
// default as struct
respFields := respType.(*compile.StructSpec).Fields
dummyConverter.append("out", " := ", "&", ms.ShortResponseType, "{}\t\n")
err := dummyConverter.GenStructConverter(structType, respFields, dummyReqTransforms)
if err != nil {
return err
}

}

dummyConverter.append("\nreturn out \t}")
ms.ConvertDummyRequestGoStatements = dummyConverter.GetLines()

return nil
}

func getQueryMethodForPrimitiveType(typeSpec compile.TypeSpec) string {
var queryMethod string

Expand Down
15 changes: 12 additions & 3 deletions codegen/module_system.go
Expand Up @@ -44,6 +44,7 @@ type EndpointMeta struct {
ClientMethodName string
WorkflowPkg string
ReqHeaders map[string]*TypedHeader
IsDummyEndpoint bool
ReqHeadersKeys []string
ReqRequiredHeadersKeys []string
ResHeaders map[string]*TypedHeader
Expand Down Expand Up @@ -1116,15 +1117,15 @@ func (g *EndpointGenerator) generateEndpointFile(
PackageName: instance.PackageInfo.GeneratedPackagePath + "/workflow",
AliasName: "workflow",
})
if e.WorkflowImportPath != "" {
if e.WorkflowType == customWorkflow {
includedPackages = append(includedPackages, GoPackageImport{
PackageName: e.WorkflowImportPath,
AliasName: "custom" + strings.Title(m.PackageName),
})
}

workflowPkg := "workflow"
if method.Downstream == nil {
if method.Downstream == nil && e.WorkflowType == "custom" {
workflowPkg = "custom" + strings.Title(m.PackageName)
}

Expand Down Expand Up @@ -1153,6 +1154,7 @@ func (g *EndpointGenerator) generateEndpointFile(
IncludedPackages: includedPackages,
Method: method,
ReqHeaders: reqHeaders,
IsDummyEndpoint: e.IsDummyEndpoint,
ReqHeadersKeys: sortedHeaders(reqHeaders, false),
ReqRequiredHeadersKeys: sortedHeaders(reqHeaders, true),
ResHeadersKeys: sortedHeaders(e.ResHeaders, false),
Expand Down Expand Up @@ -1190,7 +1192,14 @@ func (g *EndpointGenerator) generateEndpointFile(

out[endpointFilePath] = endpoint

workflow, err := g.templates.ExecTemplate("workflow.tmpl", meta, g.packageHelper)
tmpl := ""
if e.IsDummyEndpoint {
tmpl = "clientless-workflow.tmpl"
} else {
tmpl = "workflow.tmpl"
}

workflow, err := g.templates.ExecTemplate(tmpl, meta, g.packageHelper)
if err != nil {
return nil, errors.Wrap(err, "Error executing workflow template")
}
Expand Down
18 changes: 15 additions & 3 deletions codegen/service.go
Expand Up @@ -221,9 +221,10 @@ func (ms *ModuleSpec) SetDownstream(
clientMethod = e.ClientMethod

// TODO: move generated middlewares out of zanzibar
headersPropagate = e.HeadersPropagate
reqTransforms = e.ReqTransforms
respTransforms = e.RespTransforms
headersPropagate = e.HeadersPropagate
reqTransforms = e.ReqTransforms
respTransforms = e.RespTransforms
dummyReqTransforms = e.DummyReqTransforms
)
for _, v := range ms.Services {
if v.Name == serviceName {
Expand All @@ -247,6 +248,17 @@ func (ms *ModuleSpec) SetDownstream(
"Service %q does not have method %q\n", serviceName, methodName,
)
}

if e.IsDummyEndpoint {
funcSpec := method.CompiledThriftSpec
err := method.setDummyTypeConverters(funcSpec, reqTransforms, headersPropagate, respTransforms, dummyReqTransforms, h)
if err != nil {
return errors.Errorf(
"unable to set dummy type convertors for dummy endpoint")
}
return nil
}

serviceMethod, ok := clientSpec.ExposedMethods[clientMethod]
if !ok {
return errors.Errorf("Client %q does not expose method %q", clientSpec.ClientName, clientMethod)
Expand Down

0 comments on commit 6d20f85

Please sign in to comment.