codegen/gateway.go (868 lines of code) (raw):
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package codegen
import (
"fmt"
"io/ioutil"
"net/textproto"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"go.uber.org/thriftrw/compile"
)
const (
reqHeaders = "reqHeaderMap"
resHeaders = "resHeaderMap"
customWorkflow = "custom"
clientlessWorkflow = "clientless"
)
var mandatoryEndpointFields = []string{
"endpointType",
"endpointId",
"handleId",
"thriftFile",
"thriftMethodName",
"workflowType",
}
var mandatoryHTTPEndpointFields = []string{
"testFixtures",
"middlewares",
}
// ClientSpec holds information about each client in the
// gateway included its thriftFile and other meta info
type ClientSpec struct {
// ModuleSpec holds the thrift module information
ModuleSpec *ModuleSpec
// JSONFile for this spec
JSONFile string // Deprecated
// YAMLFile for this spec
YAMLFile string
// ClientType, currently "http", "tchannel" and "custom" are supported
ClientType string
// If "custom" then where to import custom code from
CustomImportPath string
// If "custom" then this is where the interface to mock comes from
CustomInterface string
// The path to the client package import
ImportPackagePath string
// The globally unique package alias for the import
ImportPackageAlias string
// ExportName is the name that should be used when initializing the module
// on a dependency struct.
ExportName string
// ExportType refers to the type returned by the module initializer
ExportType string
// ThriftFile, absolute path to thrift file
ThriftFile string
// ClientID, used for logging and metrics, must be lowercase
// and use dashes.
ClientID string
// ClientName, PascalCase name of the client, the generated
// `Clients` struct will contain a field of this name
ClientName string
// ExposedMethods is a map of exposed method name to thrift "$service::$method"
// only the method values in this map are generated for the client
ExposedMethods map[string]string
// SidecarRouter indicates the client uses the given sidecar router to
// to communicate with downstream service, it's not relevant to custom clients.
SidecarRouter string
}
// ModuleClassConfig represents the generic YAML config for
// all modules. This will be provided by the module package.
type ModuleClassConfig struct {
ClassConfigBase `yaml:",inline" json:",inline"`
Config interface{} `yaml:"config" json:"config"`
}
// Dependencies lists all dependencies of a module
type Dependencies struct {
Client []string `yaml:"client" json:"client"`
// Service []string `yaml:"service"` // example extension
}
// MiddlewareConfigConfig is the inner config object as prescribed by module_system yaml conventions
type MiddlewareConfigConfig struct {
OptionsSchemaFile string `yaml:"schema" json:"schema"`
ImportPath string `yaml:"path" json:"path"`
}
// MiddlewareConfig represents configuration for a middleware as is written in the yaml file
type MiddlewareConfig struct {
ClassConfigBase `yaml:",inline" json:",inline"`
Dependencies *Dependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty"`
Config *MiddlewareConfigConfig `yaml:"config" json:"config"`
}
// Validate the config spec attributes
func (mid *MiddlewareConfig) Validate(configDirName string) error {
if mid.Name == "" {
return errors.New("middleware config had empty name")
}
if mid.Config.ImportPath == "" {
return errors.New("middleware config had empty import path")
}
if mid.Config.OptionsSchemaFile == "" {
return errors.New("middleware config had empty schema")
}
schPath := filepath.Join(
configDirName,
mid.Config.OptionsSchemaFile,
)
bytes, err := ioutil.ReadFile(schPath)
if err != nil {
return errors.Wrapf(
err, "Cannot read middleware yaml schema: %s",
schPath,
)
}
var midOptSchema map[string]interface{}
err = yaml.Unmarshal(bytes, &midOptSchema)
if err != nil {
return errors.Wrapf(
err, "Cannot parse yaml schema for middleware options: %s",
schPath,
)
}
return nil
}
func getModuleConfigFileName(instance *ModuleInstance) string {
if instance.YAMLFileName != "" {
return instance.YAMLFileName
}
return instance.JSONFileName
}
// MiddlewareSpec holds information about each middleware at the endpoint
type MiddlewareSpec struct {
// The middleware package name.
Name string `yaml:"name"`
// Middleware specific configuration options.
Options map[string]interface{} `yaml:"options"`
// Options pretty printed for template initialization
PrettyOptions map[string]string
// Module Dependencies, clients etc.
Dependencies *Dependencies
// Go Import Path for MiddlewareHandle implementation
ImportPath string
// Location of yaml Schema file for the configured endpoint options
OptionsSchemaFile string
}
func newMiddlewareSpec(cfg *MiddlewareConfig) *MiddlewareSpec {
return &MiddlewareSpec{
Name: cfg.Name,
Dependencies: cfg.Dependencies,
ImportPath: cfg.Config.ImportPath,
OptionsSchemaFile: cfg.Config.OptionsSchemaFile,
}
}
// TypedHeader is typed header for headers resolved
// from header schema
type TypedHeader struct {
Name string
TransformTo string
Field *compile.FieldSpec
}
// EndpointSpec holds information about each endpoint in the
// gateway including its thriftFile and meta data
type EndpointSpec struct {
// ModuleSpec holds the thrift module info
ModuleSpec *ModuleSpec `yaml:"-"`
// YAMLFile for this endpoint spec
YAMLFile string `yaml:"-"`
// GoStructsFileName is where structs are generated
GoStructsFileName string `yaml:"-"`
// GoFolderName is the folder where all the endpoints
// are generated.
GoFolderName string `yaml:"-"`
// GoPackageName is the package import path.
GoPackageName string `yaml:"-"`
// EndpointType, currently only "http"
EndpointType string `yaml:"endpointType" validate:"nonzero"`
// EndpointID, used in metrics and logging, lower case.
EndpointID string `yaml:"endpointId" validate:"nonzero"`
// HandleID, used in metrics and logging, lowercase.
HandleID string `yaml:"handleId" validate:"nonzero"`
// ThriftFile, the thrift file for this endpoint
ThriftFile string `yaml:"thriftFile" validate:"nonzero"`
// ThriftFileSha, the SHA of the thrift file for this endpoint
ThriftFileSha string `yaml:"thriftFileSha,omitempty"`
// ThriftMethodName, which thrift method to use.
ThriftMethodName string `yaml:"thriftMethodName" validate:"nonzero"`
// ThriftServiceName, which thrift service to use.
ThriftServiceName string `yaml:"-"`
// TestFixtures, meta data to generate tests,
TestFixtures map[string]*EndpointTestFixture `yaml:"testFixtures,omitempty"`
// Middlewares, meta data to add middlewares,
Middlewares []MiddlewareSpec `yaml:"middlewares,omitempty"`
// HeadersPropagate, a map from endpoint request headers to
// client request fields.
HeadersPropagate map[string]FieldMapperEntry `yaml:"-"`
// ReqTransforms, a map from client request fields to endpoint
// request fields that should override their values.
ReqTransforms map[string]FieldMapperEntry `yaml:"-"`
// 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
ErrTransforms map[string]FieldMapperEntry `yaml:"-"`
// ReqHeaders maps headers from server to client
ReqHeaders map[string]*TypedHeader `yaml:"reqHeaderMap,omitempty"`
// ResHeaders maps headers from client to server
ResHeaders map[string]*TypedHeader `yaml:"resHeaderMap,omitempty"`
// DefaultHeaders a slice of headers that are forwarded to downstream when available
DefaultHeaders []string `yaml:"-"`
// WorkflowType, either "httpClient" or "custom".
// A httpClient workflow generates a http client Caller
// A custom workflow just imports the custom code
WorkflowType string `yaml:"workflowType" validate:"nonzero"`
// If "custom" then where to import custom code from
WorkflowImportPath string `yaml:"workflowImportPath"`
// Config additional configs for the endpoint
Config map[string]interface{} `yaml:"config,omitempty"`
// if "httpClient", which client to call.
ClientID string `yaml:"clientId,omitempty"`
// if "httpClient", which client method to call.
ClientMethod string `yaml:"clientMethod,omitempty"`
// The client for this endpoint if httpClient or tchannelClient
ClientSpec *ClientSpec `yaml:"-"`
// IsClientlessEndpoint checks if the endpoint is clientless
IsClientlessEndpoint bool `yaml:"-"`
}
func ensureFields(config map[string]interface{}, mandatoryFields []string, yamlFile string) error {
for i := 0; i < len(mandatoryFields); i++ {
fieldName := mandatoryFields[i]
if _, ok := config[fieldName]; !ok {
return errors.Errorf(
"config %q must have %q field", yamlFile, fieldName,
)
}
}
return nil
}
// NewEndpointSpec creates an endpoint spec from a yaml file.
func NewEndpointSpec(
yamlFile string,
h *PackageHelper,
midSpecs map[string]*MiddlewareSpec,
) (*EndpointSpec, error) {
_, err := os.Stat(yamlFile)
if err != nil {
return nil, errors.Wrapf(err, "Could not find file %s: ", yamlFile)
}
bytes, err := ioutil.ReadFile(yamlFile)
if err != nil {
return nil, errors.Wrapf(
err, "Could not read yaml file %s: ", yamlFile,
)
}
endpointConfigObj := map[string]interface{}{}
err = yaml.Unmarshal(bytes, &endpointConfigObj)
if err != nil {
return nil, errors.Wrapf(
err, "Could not parse yaml file: %s", yamlFile,
)
}
if err := ensureFields(endpointConfigObj, mandatoryEndpointFields, yamlFile); err != nil {
return nil, err
}
endpointType := endpointConfigObj["endpointType"]
if endpointType == "http" {
if err := ensureFields(endpointConfigObj, mandatoryHTTPEndpointFields, yamlFile); err != nil {
return nil, err
}
}
if endpointType != "http" && endpointType != "tchannel" {
return nil, errors.Errorf(
"Cannot support unknown endpointType for endpoint: %s", yamlFile,
)
}
thriftFile := filepath.Join(
h.IdlPath(), h.GetModuleIdlSubDir(true), endpointConfigObj["thriftFile"].(string),
)
mspec, err := NewModuleSpec(thriftFile, endpointType == "http", true, h)
if err != nil {
return nil, errors.Wrapf(
err, "Could not build module spec for thrift: %s", thriftFile,
)
}
var workflowImportPath string
var clientID string
var clientMethod string
var isClientlessEndpoint bool
workflowType := endpointConfigObj["workflowType"].(string)
if workflowType == "httpClient" || workflowType == "tchannelClient" {
iclientID, ok := endpointConfigObj["clientId"]
if !ok {
return nil, errors.Errorf(
"endpoint config %q must have clientName field", yamlFile,
)
}
if iclientID != nil {
clientID = iclientID.(string)
}
iclientMethod, ok := endpointConfigObj["clientMethod"]
if !ok {
return nil, errors.Errorf(
"endpoint config %q must have clientMethod field", yamlFile,
)
}
if iclientMethod != nil {
clientMethod = iclientMethod.(string)
}
} else if workflowType == customWorkflow {
iworkflowImportPath, ok := endpointConfigObj["workflowImportPath"]
if !ok {
return nil, errors.Errorf(
"endpoint config %q must have workflowImportPath field",
yamlFile,
)
}
workflowImportPath = iworkflowImportPath.(string)
} else if workflowType == clientlessWorkflow {
isClientlessEndpoint = true
} else {
return nil, errors.Errorf(
"Invalid workflowType %q for endpoint %q",
workflowType, yamlFile,
)
}
dirName, err := filepath.Rel(h.ConfigRoot(), filepath.Dir(yamlFile))
if err != nil {
return nil, errors.Errorf("Config file is out of config root: %s", yamlFile)
}
goFolderName := filepath.Join(h.CodeGenTargetPath(), dirName)
goStructsFileName := filepath.Join(
h.CodeGenTargetPath(),
dirName,
filepath.Base(dirName)+"_structs.go",
)
goPackageName := filepath.Join(h.GoGatewayPackageName(), dirName)
thriftInfo := endpointConfigObj["thriftMethodName"].(string)
parts := strings.Split(thriftInfo, "::")
if len(parts) != 2 {
return nil, errors.Errorf(
"Cannot read thriftMethodName %q for endpoint yaml file: %s",
thriftInfo, yamlFile,
)
}
var config map[string]interface{}
if _, ok := endpointConfigObj["config"]; !ok {
config = make(map[string]interface{})
} else {
config = endpointConfigObj["config"].(map[string]interface{})
}
espec := &EndpointSpec{
ModuleSpec: mspec,
YAMLFile: yamlFile,
GoStructsFileName: goStructsFileName,
GoFolderName: goFolderName,
GoPackageName: goPackageName,
EndpointType: endpointConfigObj["endpointType"].(string),
EndpointID: endpointConfigObj["endpointId"].(string),
HandleID: endpointConfigObj["handleId"].(string),
ThriftFile: thriftFile,
ThriftServiceName: parts[0],
ThriftMethodName: parts[1],
WorkflowType: workflowType,
WorkflowImportPath: workflowImportPath,
IsClientlessEndpoint: isClientlessEndpoint,
ClientID: clientID,
ClientMethod: clientMethod,
DefaultHeaders: h.defaultHeaders,
Config: config,
}
defaultMidSpecs, err := getOrderedDefaultMiddlewareSpecs(
h.ConfigRoot(),
h.DefaultMiddlewareSpecs(),
endpointType.(string))
if err != nil {
return nil, errors.Wrap(
err, "error getting ordered default middleware specs",
)
}
return augmentEndpointSpec(espec, endpointConfigObj, midSpecs, defaultMidSpecs)
}
func getOrderedDefaultMiddlewareSpecs(
cfgDir string,
middlewareSpecs map[string]*MiddlewareSpec,
classType string,
) ([]MiddlewareSpec, error) {
middlewareObj := map[string][]string{}
middlewareOrderingFile := filepath.Join(cfgDir, "middlewares/default.yaml")
if _, err := os.Stat(middlewareOrderingFile); os.IsNotExist(err) {
// Cannot find yaml file, use json file instead
middlewareOrderingFile = filepath.Join(cfgDir, "middlewares/default.json")
if _, err := os.Stat(middlewareOrderingFile); os.IsNotExist(err) {
// This file is not required so it is okay to skip
return nil, nil
}
}
bytes, err := ioutil.ReadFile(middlewareOrderingFile)
if err != nil {
return nil, errors.Wrapf(
err, "could not read default middleware ordering file: %s", middlewareOrderingFile,
)
}
err = yaml.Unmarshal(bytes, &middlewareObj)
if err != nil {
return nil, errors.Wrapf(
err, "could not parse default middleware ordering file: %s", middlewareOrderingFile,
)
}
middlewareOrderingObj := middlewareObj[classType]
return sortByMiddlewareOrdering(middlewareOrderingObj, middlewareSpecs)
}
// sortByMiddlewareOrdering sorts middlewareSpecs using the ordering from middlewareOrderingObj
func sortByMiddlewareOrdering(
middlewareOrderingObj []string,
middlewareSpecs map[string]*MiddlewareSpec,
) ([]MiddlewareSpec, error) {
middlewares := make([]MiddlewareSpec, 0)
for _, middlewareName := range middlewareOrderingObj {
middlewareSpec, ok := middlewareSpecs[middlewareName]
if !ok {
return nil, errors.Errorf("could not find middleware %s", middlewareName)
}
middlewares = append(middlewares, *middlewareSpec)
}
return middlewares, nil
}
func testFixtures(endpointConfigObj map[string]interface{}) (map[string]*EndpointTestFixture, error) {
field, ok := endpointConfigObj["testFixtures"]
if !ok {
return nil, errors.Errorf("missing testFixtures field")
}
testFixturesRaw, err := yaml.Marshal(field)
if err != nil {
return nil, err
}
var ret map[string]*EndpointTestFixture
err = yaml.Unmarshal(testFixturesRaw, &ret)
return ret, err
}
func loadHeadersFromConfig(endpointCfgObj map[string]interface{}, key string) (map[string]string, error) {
// TODO define endpointConfigObj to avoid type assertion
headers, ok := endpointCfgObj[key]
if !ok {
return nil, errors.Errorf("unable to parse %q", key)
}
headersMap := make(map[string]string)
for key, val := range headers.(map[string]interface{}) {
switch value := val.(type) {
case string:
headersMap[textproto.CanonicalMIMEHeaderKey(key)] = value
default:
return nil, errors.Errorf(
"unable to parse string %q in headers %q", value, headers)
}
}
return headersMap, nil
}
func sortedHeaders(headerMap map[string]*TypedHeader, filterRequired bool) []string {
var sortedArr = []string{}
for k, v := range headerMap {
if !filterRequired {
sortedArr = append(sortedArr, k)
} else if v.Field.Required {
sortedArr = append(sortedArr, k)
}
}
sort.Strings(sortedArr)
return sortedArr
}
func resolveHeaders(
espec *EndpointSpec,
endpointConfigObj map[string]interface{},
key string,
) error {
var (
keyMap = map[string]string{
reqHeaders: "http.req.metadata",
resHeaders: "http.res.metadata",
}
headersMap = make(map[string]*TypedHeader)
headerModels []string
annotationKey = keyMap[key]
)
defer func() {
if key == reqHeaders {
espec.ReqHeaders = headersMap
} else {
espec.ResHeaders = headersMap
}
}()
transformMap, err := loadHeadersFromConfig(endpointConfigObj, key)
if err != nil {
return err
}
method, err := findMethodByName(espec.ThriftMethodName, espec.ModuleSpec.Services)
if err != nil {
return err
}
for ak, av := range method.CompiledThriftSpec.Annotations {
if strings.HasSuffix(ak, annotationKey) {
headerModels = strings.Split(av, ",")
break
}
}
if len(headerModels) < 1 && len(transformMap) > 0 {
return errors.Errorf("header models %q unconfigured for transform", key)
}
if len(headerModels) < 1 {
return nil
}
for _, m := range headerModels {
typedHeaders, err := resolveHeaderModels(espec.ModuleSpec, m)
if err != nil {
return err
}
for hk, hv := range typedHeaders {
headersMap[textproto.CanonicalMIMEHeaderKey(hk)] = hv
}
}
// apply header transform
for k, v := range transformMap {
typedHeader, ok := headersMap[k]
if !ok {
return errors.Errorf("unable to find header %q to transform", k)
}
typedHeader.TransformTo = v
}
return nil
}
func resolveHeaderModels(ms *ModuleSpec, modelPath string) (map[string]*TypedHeader, error) {
const (
headerPreix = "headers"
httpRefSuffix = "http.ref"
)
loadModuleFromInclude := func(moduleName string) *compile.Module {
for pkgKey, pkg := range ms.CompiledModule.Includes {
if pkgKey == moduleName {
return pkg.Module
}
}
return nil
}
loadHeaderKeyFromField := func(field *compile.FieldSpec) *string {
for ak, av := range field.Annotations {
if avs := strings.Split(av, "."); len(avs) == 2 &&
avs[0] == headerPreix &&
strings.HasSuffix(ak, httpRefSuffix) {
headerKey := avs[1]
return &headerKey
}
}
return nil
}
loadHeadersFromCompiledModule := func(module *compile.Module, structName string) (map[string]*TypedHeader, error) {
var typeStruct *compile.StructSpec
var typedHeaders = make(map[string]*TypedHeader)
for tk, tv := range module.Types {
if ts, ok := tv.(*compile.StructSpec); tk == structName && ok {
typeStruct = ts
break
}
}
if typeStruct == nil {
return nil, errors.Errorf("unable to find typedHeaders %q", structName)
}
for _, field := range typeStruct.Fields {
hk := loadHeaderKeyFromField(field)
if hk == nil {
return nil, errors.Errorf("unable to find header key %q", field.Name)
}
headerKey := textproto.CanonicalMIMEHeaderKey(*hk)
typedHeaders[headerKey] = &TypedHeader{
Name: headerKey,
TransformTo: headerKey,
Field: field,
}
}
return typedHeaders, nil
}
switch paths := strings.Split(modelPath, "."); len(paths) {
case 2:
moduleName := paths[0]
structName := paths[1]
module := loadModuleFromInclude(moduleName)
if module == nil {
return nil, errors.Errorf("missing module spec %q", moduleName)
}
return loadHeadersFromCompiledModule(module, structName)
default:
// TODO case 1:
// default header schema path to .
return nil, errors.Errorf(
"malformed header model %q, expecting <module>.<struct>", modelPath)
}
}
func augmentEndpointSpec(
espec *EndpointSpec,
endpointConfigObj map[string]interface{},
midSpecs map[string]*MiddlewareSpec,
defaultMidSpecs []MiddlewareSpec,
) (*EndpointSpec, error) {
middlewares := defaultMidSpecs
if _, ok := endpointConfigObj["middlewares"]; ok {
endpointMids, ok := endpointConfigObj["middlewares"].([]interface{})
if !ok {
return nil, errors.Errorf(
"Unable to parse middlewares field",
)
}
for _, middleware := range endpointMids {
middlewareObj, ok := middleware.(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"Unable to parse middleware %s",
middlewareObj,
)
}
name, ok := middlewareObj["name"].(string)
if !ok {
return nil, errors.Errorf(
"Unable to parse \"name\" field in middleware %s",
middlewareObj,
)
}
// req/res transform middleware set type converter
if name == "transformRequest" {
reqTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.ReqTransforms = reqTransforms
continue
}
if name == "transformResponse" {
resTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.RespTransforms = resTransforms
continue
}
if name == "transformClientlessReq" {
dummyResTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.DummyReqTransforms = dummyResTransforms
continue
}
if name == "transformError" {
errTransforms, err := setTransformMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.ErrTransforms = errTransforms
continue
}
// req header propagate middleware set headersPropagator
if name == "headersPropagate" {
headersPropagate, err := setPropagateMiddleware(middlewareObj)
if err != nil {
return nil, err
}
espec.HeadersPropagate = headersPropagate
continue
}
// Verify the middleware name is defined.
if midSpecs[name] == nil {
return nil, errors.Errorf(
"middlewares config %q not found.", name,
)
}
// TODO(sindelar): Validate Options against middleware spec and support
// nested typed objects.
opts, ok := middlewareObj["options"].(map[string]interface{})
if !ok {
opts = make(map[string]interface{})
}
prettyOpts := map[string]string{}
for k, value := range opts {
key := k
rValue := reflect.ValueOf(value)
kind := rValue.Kind()
if kind == reflect.Slice && rValue.Len() > 0 {
rType := rValue.Type()
rElemType := rType.Elem()
elemTypeString := rElemType.String()
if rElemType.Kind() == reflect.Interface {
rFirstValue := rValue.Index(0)
rRawFirstValue := rFirstValue.Interface()
elemTypeString = reflect.TypeOf(rRawFirstValue).String()
}
str := fmt.Sprintf("[]%s{", elemTypeString)
for i := 0; i < rValue.Len(); i++ {
str += fmt.Sprintf("%#v", rValue.Index(i))
if i != rValue.Len()-1 {
str += ","
}
}
str += "}"
prettyOpts[key] = str
} else {
prettyOpts[key] = fmt.Sprintf("%#v", rValue)
}
}
middlewares = append(middlewares, MiddlewareSpec{
Name: name,
ImportPath: midSpecs[name].ImportPath,
Options: opts,
PrettyOptions: prettyOpts,
})
}
}
espec.Middlewares = middlewares
if "http" == endpointConfigObj["endpointType"] {
testFixtures, err := testFixtures(endpointConfigObj)
if err != nil {
return nil, errors.Wrap(err, "Unable to parse test cases")
}
espec.TestFixtures = testFixtures
// augment request headers
if err := resolveHeaders(espec, endpointConfigObj, reqHeaders); err != nil {
return nil, err
}
// augment response headers
if err := resolveHeaders(espec, endpointConfigObj, resHeaders); err != nil {
return nil, err
}
}
return espec, nil
}
func setPropagateMiddleware(middlewareObj map[string]interface{}) (map[string]FieldMapperEntry, error) {
fieldMap := make(map[string]FieldMapperEntry)
opts, ok := middlewareObj["options"].(map[string]interface{})
if !ok {
return nil, errors.New(
"missing or invalid options for propagate middleware",
)
}
propagates := opts["propagate"].([]interface{})
dest := make(map[string]string)
for _, propagate := range propagates {
propagateMap := propagate.(map[string]interface{})
fromField, ok := propagateMap["from"].(string)
if !ok {
return nil, errors.New(
"propagate middleware found with no source field",
)
}
toField, ok := propagateMap["to"].(string)
if !ok {
return nil, errors.New(
"propagate middleware found with no destination field",
)
}
if _, ok := dest[toField]; ok {
return nil, errors.Errorf(
"propagate multiple source field to destination field %s",
toField,
)
}
dest[toField] = toField
fieldMap[toField] = FieldMapperEntry{
QualifiedName: fromField,
}
}
return fieldMap, nil
}
func setTransformMiddleware(middlewareObj map[string]interface{}) (map[string]FieldMapperEntry, error) {
fieldMap := make(map[string]FieldMapperEntry)
opts, ok := middlewareObj["options"].(map[string]interface{})
if !ok {
return nil, errors.Errorf(
"transform middleware found with no options.",
)
}
transforms := opts["transforms"].([]interface{})
for _, transform := range transforms {
transformMap := transform.(map[string]interface{})
fromField, ok := transformMap["from"].(string)
if !ok {
return nil, errors.New(
"transform middleware found with no source field",
)
}
toField, ok := transformMap["to"].(string)
if !ok {
return nil, errors.New(
"transform middleware found with no destination field",
)
}
overrideOpt, ok := transformMap["override"].(bool)
if ok {
fieldMap[toField] = FieldMapperEntry{
QualifiedName: fromField,
Override: overrideOpt,
}
} else {
fieldMap[toField] = FieldMapperEntry{
QualifiedName: fromField,
}
}
}
return fieldMap, nil
}
// TargetEndpointPath generates a filepath for each endpoint method
func (e *EndpointSpec) TargetEndpointPath(
serviceName string, methodName string,
) string {
baseName := filepath.Base(e.GoFolderName)
fileName := baseName + "_" + strings.ToLower(serviceName) +
"_method_" + strings.ToLower(methodName) + ".go"
return filepath.Join(e.GoFolderName, fileName)
}
// TargetEndpointTestPath generates a filepath for each endpoint test
func (e *EndpointSpec) TargetEndpointTestPath(
serviceName string, methodName string,
) string {
baseName := filepath.Base(e.GoFolderName)
fileName := baseName + "_" + strings.ToLower(serviceName) +
"_method_" + strings.ToLower(methodName) + "_test.go"
return filepath.Join(e.GoFolderName, fileName)
}
// EndpointTestConfigPath generates a filepath for each endpoint test config
func (e *EndpointSpec) EndpointTestConfigPath() string {
return strings.TrimSuffix(e.YAMLFile, filepath.Ext(e.YAMLFile)) + "_test.yaml"
}
// SetDownstream configures the downstream client for this endpoint spec
func (e *EndpointSpec) SetDownstream(
clientModules []*ClientSpec,
h *PackageHelper,
) error {
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 {
clientSpec = v
break
}
}
if clientSpec == nil {
return errors.Errorf(
"When parsing endpoint yaml %q, "+
"could not find client %q in gateway",
e.YAMLFile, e.ClientID,
)
}
e.ClientSpec = clientSpec
return e.ModuleSpec.SetDownstream(e, h)
}
// EndpointConfig represent the "config" field of endpoint-config.yaml
type EndpointConfig struct {
Ratelimit int32 `yaml:"rateLimit,omitempty" json:"rateLimit"`
Endpoints []string `yaml:"endpoints" json:"endpoints"`
}
// EndpointClassConfig represents the specific config for
// an endpoint group. This is a downcast of the moduleClassConfig.
type EndpointClassConfig struct {
ClassConfigBase `yaml:",inline" json:",inline"`
Dependencies map[string][]string `yaml:"dependencies" json:"dependencies"`
Config *EndpointConfig `yaml:"config" json:"config" validate:"nonzero"`
}
func parseEndpointYamls(
endpointGroupYamls []string,
) ([]string, error) {
endpointYamls := []string{}
for _, endpointGroupYAML := range endpointGroupYamls {
bytes, err := ioutil.ReadFile(endpointGroupYAML)
if err != nil {
return nil, errors.Wrapf(
err, "Cannot read endpoint group yaml: %s",
endpointGroupYAML,
)
}
var endpointConfig EndpointClassConfig
err = yaml.Unmarshal(bytes, &endpointConfig)
if err != nil {
return nil, errors.Wrapf(
err, "Cannot parse yaml for endpoint group config: %s",
endpointGroupYAML,
)
}
endpointConfigDir := filepath.Dir(endpointGroupYAML)
for _, fileName := range endpointConfig.Config.Endpoints {
endpointYamls = append(
endpointYamls, filepath.Join(endpointConfigDir, fileName),
)
}
}
return endpointYamls, nil
}
func parseDefaultMiddlewareConfig(
defaultMiddlewareConfigDir string,
configDirName string,
) (map[string]*MiddlewareSpec, error) {
fullMiddlewareDir := filepath.Join(configDirName, defaultMiddlewareConfigDir)
_, err := ioutil.ReadDir(fullMiddlewareDir)
if err != nil {
return nil, nil
}
return parseMiddlewareConfig(defaultMiddlewareConfigDir, configDirName)
}
func parseMiddlewareConfig(
middlewareConfigDir string,
configDirName string,
) (map[string]*MiddlewareSpec, error) {
specMap := map[string]*MiddlewareSpec{}
if middlewareConfigDir == "" {
return specMap, nil
}
fullMiddlewareDir := filepath.Join(configDirName, middlewareConfigDir)
files, err := ioutil.ReadDir(fullMiddlewareDir)
if err != nil {
return nil, errors.Wrapf(
err,
"Error reading middleware config directory %q",
fullMiddlewareDir,
)
}
for _, file := range files {
if !file.IsDir() {
continue
}
instanceConfig := filepath.Join(
fullMiddlewareDir, file.Name(), "middleware-config.yaml")
if _, err := os.Stat(instanceConfig); os.IsNotExist(err) {
// Cannot find yaml file, use json file instead
instanceConfig = filepath.Join(
fullMiddlewareDir, file.Name(), "middleware-config.json")
}
bytes, err := ioutil.ReadFile(instanceConfig)
if os.IsNotExist(err) {
fmt.Printf("Could not read config file for middleware directory \"%s\" skipping...\n", file.Name())
continue
} else if err != nil {
return nil, errors.Wrapf(
err, "Cannot read middleware config yaml: %s",
instanceConfig,
)
}
var mid MiddlewareConfig
err = yaml.Unmarshal(bytes, &mid)
if err != nil {
return nil, errors.Wrapf(
err, "Cannot parse yaml for middleware config yaml: %s",
instanceConfig,
)
}
err = mid.Validate(configDirName)
if err != nil {
return nil, errors.Wrapf(
err, "Cannot validate middleware: %v",
mid,
)
}
specMap[mid.Name] = newMiddlewareSpec(&mid)
}
return specMap, nil
}