codegen/module_system.go (1,573 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 ( "encoding/json" "io/ioutil" "net/textproto" "os" "path/filepath" "sort" "strconv" "strings" "sync" validator2 "gopkg.in/validator.v2" "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/uber/zanzibar/parallelize" "go.uber.org/thriftrw/compile" ) // EndpointMeta saves meta data used to render an endpoint. type EndpointMeta struct { Instance *ModuleInstance Spec *EndpointSpec GatewayPackageName string IncludedPackages []GoPackageImport Method *MethodSpec ClientName string ClientID string ClientType string ClientMethodName string WorkflowPkg string ReqHeaders map[string]*TypedHeader IsClientlessEndpoint bool ReqHeadersKeys []string ReqRequiredHeadersKeys []string ResHeaders map[string]*TypedHeader ResHeadersKeys []string ResRequiredHeadersKeys []string TraceKey string DeputyReqHeader string DefaultHeaders []string } // EndpointCollectionMeta saves information used to generate an initializer // for a collection of endpoints type EndpointCollectionMeta struct { Instance *ModuleInstance EndpointMeta []*EndpointMeta } // StructMeta saves information to generate an endpoint's thrift structs file type StructMeta struct { Instance *ModuleInstance Spec *ModuleSpec } // EndpointTestMeta saves meta data used to render an endpoint test. type EndpointTestMeta struct { Instance *ModuleInstance Method *MethodSpec TestFixtures map[string]*EndpointTestFixture `yaml:"testFixtures" json:"testFixtures"` ReqHeaders map[string]*TypedHeader ResHeaders map[string]*TypedHeader ClientName string ClientID string RelativePathToRoot string IncludedPackages []GoPackageImport } // FixtureBlob implements default string used for (http | tchannel) // request/response type FixtureBlob map[string]interface{} func toStringMap(i map[string]interface{}) map[string]interface{} { m := make(map[string]interface{}, len(i)) for k, v := range i { key := k switch val := v.(type) { case map[string]interface{}: m[key] = toStringMap(val) case FixtureBlob: m[key] = toStringMap(val) default: m[key] = v } } return m } // String convert FixtureBlob to string func (fb *FixtureBlob) String() string { str, err := json.Marshal(toStringMap(*fb)) if err != nil { panic(err) } return string(str) } // BodyType can either be `json` or `string` type BodyType string // ProtocalType can either be `http` or `tchannel` type ProtocalType string // HTTPMethodType can only be valid http method type HTTPMethodType string // FixtureBody is used to create http body in test fixtures type FixtureBody struct { BodyType BodyType `yaml:"bodyType,omitempty" json:"bodyType,omitempty"` BodyString string `yaml:"bodyString,omitempty" json:"bodyString,omitempty"` // set BodyString if response body is string BodyJSON *FixtureBlob `yaml:"bodyJson,omitempty" json:"bodyJson,omitempty"` // set Body if response body is object } // String convert FixtureBody to string // This String() panics inside if type and value mismatch during unmarshal // because template cannot handle errors func (fb *FixtureBody) String() string { switch fb.BodyType { case "string": return fb.BodyString case "json": if fb.BodyJSON == nil { panic(errors.New("invalid http body type")) } return fb.BodyJSON.String() default: panic(errors.New("invalid http body type")) } } // FixtureHTTPResponse is test fixture for http response type FixtureHTTPResponse struct { StatusCode int `yaml:"statusCode" json:"statusCode"` Body *FixtureBody `yaml:"body,omitempty" json:"body,omitempty"` } // FixtureResponse is test fixture for client/endpoint response type FixtureResponse struct { ResponseType ProtocalType `yaml:"responseType" json:"responseType"` HTTPResponse *FixtureHTTPResponse `yaml:"httpResponse,omitempty" json:"httpResponse,omitempty"` TChannelResponse FixtureBlob `yaml:"tchannelResponse,omitempty" json:"tchannelResponse,omitempty"` } // Body returns the string representation of FixtureResponse func (fr *FixtureResponse) Body() string { switch fr.ResponseType { case "tchannel": return fr.TChannelResponse.String() case "http": res := "" if fr.HTTPResponse.Body != nil { res = fr.HTTPResponse.Body.String() } return res default: panic(errors.New("invalid response type")) } } // FixtureHTTPRequest is test fixture for client/endpoint request type FixtureHTTPRequest struct { Method HTTPMethodType `yaml:"method,omitempty" json:"method,omitempty"` Body *FixtureBody `yaml:"body,omitempty" json:"body,omitempty"` } // FixtureRequest is test fixture for client/endpoint request type FixtureRequest struct { RequestType ProtocalType `yaml:"requestType" json:"requestType"` HTTPRequest *FixtureHTTPRequest `yaml:"httpRequest,omitempty" json:"httpRequest,omitempty"` TChannelRequest FixtureBlob `yaml:"tchannelRequest,omitempty" json:"tchannelRequest,omitempty"` } // Body returns the string representation of FixtureRequest func (fr *FixtureRequest) Body() string { switch fr.RequestType { case "tchannel": return fr.TChannelRequest.String() case "http": res := "" if fr.HTTPRequest.Body != nil { res = fr.HTTPRequest.Body.String() } return res default: panic(errors.New("invalid request type")) } } // FixtureHeaders implements default string used for headers type FixtureHeaders map[string]interface{} // EndpointTestFixture saves mocked requests/responses for an endpoint test. type EndpointTestFixture struct { TestName string `yaml:"testName" json:"testName"` EndpointID string `yaml:"endpointId" json:"endpointId"` HandleID string `yaml:"handleId" json:"handleId"` EndpointRequest FixtureRequest `yaml:"endpointRequest" json:"endpointRequest"` // there's no difference between http or tchannel request EndpointReqHeaders FixtureHeaders `yaml:"endpointReqHeaders" json:"endpointReqHeaders"` EndpointResponse FixtureResponse `yaml:"endpointResponse" json:"endpointResponse"` EndpointResHeaders FixtureHeaders `yaml:"endpointResHeaders" json:"endpointResHeaders"` ClientTestFixtures map[string]*ClientTestFixture `yaml:"clientTestFixtures" json:"clientTestFixtures"` TestServiceName string `yaml:"testServiceName" json:"testServiceName"` // The service module that mounts the endpoint } // ClientTestFixture saves mocked client request/response for an endpoint test. type ClientTestFixture struct { ClientID string `yaml:"clientId" json:"clientId"` ClientMethod string `yaml:"clientMethod" json:"clientMethod"` ClientRequest FixtureRequest `yaml:"clientRequest" json:"clientRequest"` // there's no difference between http or tchannel request ClientReqHeaders FixtureHeaders `yaml:"clientReqHeaders" json:"clientReqHeaders"` ClientResponse FixtureResponse `yaml:"clientResponse" json:"clientResponse"` ClientResHeaders FixtureHeaders `yaml:"clientResHeaders" json:"clientResHeaders"` } // NewDefaultModuleSystemWithMockHook creates a fresh instance of the default zanzibar // module system (clients, endpoints, services) with a post build hook to generate client and service mocks func NewDefaultModuleSystemWithMockHook( h *PackageHelper, clientsMock bool, workflowMock bool, serviceMock bool, configFile string, parallelizeFactor int, selectiveBuilding bool, hooks ...PostGenHook, ) (*ModuleSystem, error) { t, err := NewDefaultTemplate() if err != nil { return nil, err } var clientMockGenHook, workflowMockGenHook, serviceMockGenHook PostGenHook if clientsMock { clientMockGenHook, err = ClientMockGenHook(h, t, parallelizeFactor) if err != nil { return nil, errors.Wrap(err, "error creating client mock gen hook") } hooks = append(hooks, clientMockGenHook) } if workflowMock { workflowMockGenHook = WorkflowMockGenHook(h, t) hooks = append(hooks, workflowMockGenHook) } if serviceMock { serviceMockGenHook = ServiceMockGenHook(h, t, configFile) hooks = append(hooks, serviceMockGenHook) } return NewDefaultModuleSystem(h, selectiveBuilding, hooks...) } // NewDefaultModuleSystem creates a fresh instance of the default zanzibar // module system (clients, endpoints, services) func NewDefaultModuleSystem( h *PackageHelper, selectiveBuilding bool, hooks ...PostGenHook, ) (*ModuleSystem, error) { system := NewModuleSystem(h.moduleSearchPaths, h.defaultDependencies, selectiveBuilding, hooks...) tmpl, err := NewDefaultTemplate() if err != nil { return nil, err } // Register client module class and type generators if err := system.RegisterClass(ModuleClass{ Name: "client", NamePlural: "clients", ClassType: MultiModule, }); err != nil { return nil, errors.Wrapf(err, "Error registering client class") } if err := system.RegisterClassType("client", "http", newHTTPClientGenerator(tmpl, h)); err != nil { return nil, errors.Wrapf( err, "Error registering HTTP client class type", ) } if err := system.RegisterClassType("client", "tchannel", newTChannelClientGenerator(tmpl, h)); err != nil { return nil, errors.Wrapf( err, "Error registering TChannel client class type", ) } if err := system.RegisterClassType("client", "custom", newCustomClientGenerator(tmpl, h)); err != nil { return nil, errors.Wrapf( err, "Error registering custom client class type", ) } if err := system.RegisterClassType("client", "grpc", newGRPCClientGenerator(tmpl, h)); err != nil { return nil, errors.Wrapf( err, "Error registering grpc client class type", ) } if err := system.RegisterClass(ModuleClass{ Name: "middleware", NamePlural: "middlewares", ClassType: MultiModule, DependsOn: []string{"client"}, }); err != nil { return nil, errors.Wrapf( err, "Error registering middleware class", ) } if err := system.RegisterClassType("middleware", "http", &MiddlewareGenerator{ templates: tmpl, packageHelper: h, }); err != nil { return nil, errors.Wrapf( err, "Error registering Gateway middleware class type", ) } if err := system.RegisterClassType("middleware", "tchannel", &MiddlewareGenerator{ templates: tmpl, packageHelper: h, }); err != nil { return nil, errors.Wrapf( err, "Error registering Gateway middleware class type", ) } // Register endpoint module class and type generators if err := system.RegisterClass(ModuleClass{ Name: "endpoint", NamePlural: "endpoints", ClassType: MultiModule, DependsOn: []string{"client", "middleware"}, }); err != nil { return nil, errors.Wrapf(err, "Error registering endpoint class") } if err := system.RegisterClassType("endpoint", "http", &EndpointGenerator{ templates: tmpl, packageHelper: h, }); err != nil { return nil, errors.Wrapf( err, "Error registering HTTP endpoint class type", ) } if err := system.RegisterClassType("endpoint", "tchannel", &EndpointGenerator{ templates: tmpl, packageHelper: h, }); err != nil { return nil, errors.Wrapf( err, "Error registering TChannel endpoint class type", ) } if err := system.RegisterClass(ModuleClass{ Name: "service", NamePlural: "services", ClassType: MultiModule, DependsOn: []string{"endpoint"}, }); err != nil { return nil, errors.Wrapf( err, "Error registering service class", ) } if err := system.RegisterClassType("service", "gateway", NewGatewayServiceGenerator(tmpl, h)); err != nil { return nil, errors.Wrapf( err, "Error registering Gateway service class type", ) } return system, nil } func readEndpointConfig(rawConfig []byte) (*EndpointClassConfig, error) { var endpointConfig EndpointClassConfig if err := yaml.Unmarshal(rawConfig, &endpointConfig); err != nil { return nil, errors.Wrapf( err, "Error reading config for endpoint instance", ) } return &endpointConfig, nil } /* * HTTP Client Generator */ // httpClientGenerator generates an instance of a zanzibar http client type httpClientGenerator struct { templates *Template packageHelper *PackageHelper validator *validator2.Validator } // newHTTPClientGenerator generates an instance of a zanzibar http client func newHTTPClientGenerator(templates *Template, packageHelper *PackageHelper) *httpClientGenerator { validator := getExposedMethodValidator() return &httpClientGenerator{ templates: templates, packageHelper: packageHelper, validator: validator, } } // ComputeSpec returns the spec for a HTTP client func (g *httpClientGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { // Parse the client config from the endpoint YAML file clientConfig, err := newClientConfig(instance.YAMLFileRaw, g.validator) if err != nil { return nil, errors.Wrapf( err, "Error reading HTTP client %q YAML config", instance.InstanceName, ) } clientSpec, err := clientConfig.NewClientSpec( instance, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error initializing HTTPClientSpec for %q", instance.InstanceName, ) } return clientSpec, nil } // GetClientQPSLevels gets mapping from client's circuit breaker name to qps level func GetClientQPSLevels(qpsLevels map[string]int, methods map[string]string, clientID string) map[string]string { clientQPSLevels := make(map[string]string) for _, methodName := range methods { key := clientID + "-" + methodName if qps, ok := qpsLevels[key]; ok { qpsLevel := strconv.Itoa(qps) clientQPSLevels[key] = qpsLevel } else { // if no qps level for method // sets as default (default circuit breaker parameters will be assigned) clientQPSLevels[key] = "default" } } return clientQPSLevels } // Generate returns the HTTP client build result, which contains the files and // the generated client spec func (g *httpClientGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { clientSpecUntyped, err := g.ComputeSpec(instance) if err != nil { return nil, errors.Wrapf( err, "Error initializing HTTPClientSpec for %q", instance.InstanceName, ) } clientSpec := clientSpecUntyped.(*ClientSpec) exposedMethods := reverseExposedMethods(clientSpec) sort.Sort(&clientSpec.ModuleSpec.Services) // transfer only the methods that belong to the client with the qps level var clientQPSLevels map[string]string = GetClientQPSLevels(instance.QPSLevels, exposedMethods, clientSpec.ClientID) clientMeta := &ClientMeta{ Instance: instance, ExportName: clientSpec.ExportName, ExportType: clientSpec.ExportType, Services: clientSpec.ModuleSpec.Services, IncludedPackages: clientSpec.ModuleSpec.IncludedPackages, ClientID: clientSpec.ClientID, ExposedMethods: exposedMethods, QPSLevels: clientQPSLevels, SidecarRouter: clientSpec.SidecarRouter, } client, err := ExecuteDefaultOrCustomTemplate( "http_client.tmpl", g.templates, instance.CustomTemplates, instance.Config, clientMeta, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error executing HTTP client template for %q", instance.InstanceName, ) } // When it is possible to generate structs for all module types, the // module system will do this transparently. For now we are opting in // on a per-generator basis. dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { return nil, errors.Wrapf( err, "Error generating dependencies struct for %q %q", instance.ClassName, instance.InstanceName, ) } baseName := filepath.Base(instance.Directory) clientFilePath := baseName + ".go" files := map[string][]byte{ clientFilePath: client, } if dependencies != nil { files["module/dependencies.go"] = dependencies } return &BuildResult{ Files: files, Spec: clientSpec, }, nil } // PopulateQPSLevels loops through endpoint dir and gets qps levels func PopulateQPSLevels(EndpointsBaseDir string, isEnabled bool) (map[string]int, error) { qpsLevels := make(map[string]int) if !isEnabled { return qpsLevels, nil } filesList := []string{} endpointFiles, err := GetListOfAllFilesInEndpointDir(EndpointsBaseDir, filesList) if err != nil { return nil, errors.Wrapf( err, "error in getting endpoint files", ) } for _, endpointFile := range endpointFiles { config, err := UnmarshalEndpointFile(endpointFile) if err != nil { return nil, errors.Wrapf( err, "error in unmarshalling endpoint file %q", endpointFile, ) } clientMethod, methodOK := config["clientMethod"] clientID, clientOK := config["clientId"] qpsLevel, qpsOK := config["qpsLevel"] // these fields exist in the config files hasFields := methodOK && clientOK && qpsOK if hasFields { // when yaml files have no values for these keys(fields) clientless := clientID == nil || clientID == "" methodless := clientMethod == nil || clientMethod == "" if !clientless && !methodless { // unique key because of potential clients having same method names (staging) key := clientID.(string) + "-" + clientMethod.(string) // store highest qps level for circuit breaker in qpsLevels map thisQPSLevel := int(qpsLevel.(float64)) if currentQPSLevel, ok := qpsLevels[key]; ok { if thisQPSLevel > currentQPSLevel { qpsLevels[key] = thisQPSLevel } } else { qpsLevels[key] = thisQPSLevel } } } } return qpsLevels, nil } // UnmarshalEndpointFile unmarshals endpoint file into config func UnmarshalEndpointFile(endpointFile string) (map[string]interface{}, error) { var config map[string]interface{} bytes, err := ioutil.ReadFile(endpointFile) if err != nil { return nil, errors.Wrapf( err, "error in reading endpoint file %q", endpointFile, ) } fileExtension := filepath.Ext(endpointFile) if fileExtension == ".json" { err = json.Unmarshal(bytes, &config) if err != nil { return nil, errors.Wrapf( err, "error in unmarshalling json %q", endpointFile, ) } } if fileExtension == ".yaml" { err = yaml.Unmarshal(bytes, &config) if err != nil { return nil, errors.Wrapf( err, "error in unmarshalling yaml %q", endpointFile, ) } } return config, nil } // GetListOfAllFilesInEndpointDir gets all the endpoint config files // takes empty filesList array as parameter to add to during recursion func GetListOfAllFilesInEndpointDir(filePath string, filesList []string) ([]string, error) { fileInfo, err := os.Stat(filePath) if err != nil { return nil, errors.Errorf( "error in getting file info for file path %q", filePath, ) } if fileInfo.IsDir() { items, err := ioutil.ReadDir(filePath) if err != nil { return nil, errors.Errorf( "error in reading base directory %q", filePath, ) } for _, item := range items { filesList, _ = GetListOfAllFilesInEndpointDir(filePath+"/"+item.Name(), filesList) } } else { filepathExt := filepath.Ext(filePath) // checks to see if file is yaml or json file hasCorrectExtensions := filepathExt == ".json" || filepathExt == ".yaml" if !strings.Contains(filePath, "endpoint-config") && hasCorrectExtensions { filesList = append(filesList, filePath) return filesList, nil } } return filesList, nil } /* * TChannel Client Generator */ // tchannelClientGenerator generates an instance of a zanzibar TChannel client type tchannelClientGenerator struct { templates *Template packageHelper *PackageHelper validator *validator2.Validator } // newTChannelClientGenerator generates an instance of a zanzibar TChannel client func newTChannelClientGenerator(templates *Template, packageHelper *PackageHelper) *tchannelClientGenerator { validator := getExposedMethodValidator() return &tchannelClientGenerator{ templates: templates, packageHelper: packageHelper, validator: validator, } } func getExposedMethodValidator() *validator2.Validator { validator := validator2.NewValidator() _ = validator.SetValidationFunc("exposedMethods", validateExposedMethods) return validator } // ComputeSpec computes the TChannel client spec func (g *tchannelClientGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { // Parse the client config from the endpoint YAML file clientConfig, err := newClientConfig(instance.YAMLFileRaw, g.validator) if err != nil { return nil, errors.Wrapf( err, "Error reading TChannel client %q YAML config", instance.InstanceName, ) } clientSpec, err := clientConfig.NewClientSpec( instance, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error initializing TChannelClientSpec for %q", instance.InstanceName, ) } return clientSpec, nil } // Generate returns the TChannel client build result, which contains the files // and the generated client spec func (g *tchannelClientGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { clientSpecUntyped, err := g.ComputeSpec(instance) if err != nil { return nil, errors.Wrapf( err, "Error initializing TChannelClientSpec for %q", instance.InstanceName, ) } clientSpec := clientSpecUntyped.(*ClientSpec) exposedMethods := reverseExposedMethods(clientSpec) sort.Sort(clientSpec.ModuleSpec.Services) var clientQPSLevels map[string]string = GetClientQPSLevels(instance.QPSLevels, exposedMethods, clientSpec.ClientID) clientMeta := &ClientMeta{ Instance: instance, ExportName: clientSpec.ExportName, ExportType: clientSpec.ExportType, Services: clientSpec.ModuleSpec.Services, IncludedPackages: clientSpec.ModuleSpec.IncludedPackages, ClientID: clientSpec.ClientID, ExposedMethods: exposedMethods, QPSLevels: clientQPSLevels, SidecarRouter: clientSpec.SidecarRouter, DeputyReqHeader: g.packageHelper.DeputyReqHeader(), } client, err := ExecuteDefaultOrCustomTemplate( "tchannel_client.tmpl", g.templates, instance.CustomTemplates, instance.Config, clientMeta, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error executing TChannel client template for %q", instance.InstanceName, ) } // When it is possible to generate structs for all module types, the // module system will do this transparently. For now we are opting in // on a per-generator basis. dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { return nil, errors.Wrapf( err, "Error generating dependencies struct for %q %q", instance.ClassName, instance.InstanceName, ) } baseName := filepath.Base(instance.Directory) clientFilePath := baseName + ".go" files := map[string][]byte{ clientFilePath: client, } genTestServer, _ := instance.Config["genTestServer"].(bool) if genTestServer { server, err := ExecuteDefaultOrCustomTemplate( "tchannel_client_test_server.tmpl", g.templates, instance.CustomTemplates, instance.Config, clientMeta, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error executing TChannel server template for %q", instance.InstanceName, ) } serverFilePath := baseName + "_test_server.go" files[serverFilePath] = server } if dependencies != nil { files["module/dependencies.go"] = dependencies } return &BuildResult{ Files: files, Spec: clientSpec, }, nil } // reverse index and filter the exposed methods map as the gen-thrift-spec can be subset func reverseExposedMethods(clientSpec *ClientSpec) map[string]string { reversed := map[string]string{} for exposedMethod, idlMethod := range clientSpec.ExposedMethods { if hasMethod(clientSpec, idlMethod) { reversed[idlMethod] = exposedMethod } } return reversed } func hasMethod(cspec *ClientSpec, idlMethod string) bool { segments := strings.Split(idlMethod, "::") service := segments[0] method := segments[1] if cspec.ModuleSpec.Services != nil { return hasThriftMethod(cspec.ModuleSpec.Services, service, method) } return hasProtoMethod(cspec.ModuleSpec.ProtoServices, service, method) } func hasThriftMethod(thriftSpec []*ServiceSpec, service, method string) bool { for _, s := range thriftSpec { if s.Name == service { for _, m := range s.Methods { if m.Name == method { return true } } } } return false } func hasProtoMethod(protoSpec []*ProtoService, service, method string) bool { for _, s := range protoSpec { if s.Name == service { for _, m := range s.RPC { if m.Name == method { return true } } } } return false } /* * Custom Client Generator */ // customClientGenerator gathers the custom client spec for future use in ClientsInitGenerator type customClientGenerator struct { templates *Template packageHelper *PackageHelper validator *validator2.Validator } // newCustomClientGenerator generates custom client spec for future use in ClientsInitGenerator func newCustomClientGenerator(templates *Template, packageHelper *PackageHelper) *customClientGenerator { validator := validator2.NewValidator() return &customClientGenerator{ templates: templates, packageHelper: packageHelper, validator: validator, } } // ComputeSpec computes the client spec for a custom client func (g *customClientGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { // Parse the client config from the endpoint YAML file clientConfig, err := newClientConfig(instance.YAMLFileRaw, g.validator) if err != nil { return nil, errors.Wrapf( err, "Error reading custom client %q YAML config", instance.InstanceName, ) } clientSpec, err := clientConfig.NewClientSpec( instance, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error initializing CustomClientSpec for %q", instance.InstanceName, ) } return clientSpec, nil } // Generate returns the custom client build result, which contains the // generated client spec and no files func (g *customClientGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { clientSpecUntyped, err := g.ComputeSpec(instance) if err != nil { return nil, errors.Wrapf( err, "Error initializing CustomClientSpec for %q", instance.InstanceName, ) } clientSpec := clientSpecUntyped.(*ClientSpec) // When it is possible to generate structs for all module types, the // module system will do this transparently. For now we are opting in // on a per-generator basis. dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { return nil, errors.Wrapf( err, "Error generating dependencies struct for %q %q", instance.ClassName, instance.InstanceName, ) } files := map[string][]byte{} if dependencies != nil { files["module/dependencies.go"] = dependencies } return &BuildResult{ Files: files, Spec: clientSpec, }, nil } /* * gRPC client generator */ // gRPCClientGenerator generates grpc clients. type gRPCClientGenerator struct { templates *Template packageHelper *PackageHelper validator *validator2.Validator } // NewgrpcClientGenerator generates an instance of a zanzibar grpc client func newGRPCClientGenerator(templates *Template, packageHelper *PackageHelper) *gRPCClientGenerator { validator := getExposedMethodValidator() return &gRPCClientGenerator{ templates: templates, packageHelper: packageHelper, validator: validator, } } // ComputeSpec returns the spec for a gRPC client func (g *gRPCClientGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { // Parse the client config from the endpoint YAML file clientConfig, err := newClientConfig(instance.YAMLFileRaw, g.validator) if err != nil { return nil, errors.Wrapf( err, "error reading gRPC client %q YAML config", instance.InstanceName, ) } clientSpec, err := clientConfig.NewClientSpec( instance, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "error initializing gRPCClientSpec for %q", instance.InstanceName, ) } return clientSpec, nil } // Generate returns the gRPC client build result, which contains the files and // the generated client spec func (g *gRPCClientGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { clientSpecUntyped, err := g.ComputeSpec(instance) if err != nil { return nil, errors.Wrapf( err, "error initializing GRPCClientSpec for %q", instance.InstanceName, ) } clientSpec := clientSpecUntyped.(*ClientSpec) reversedMethods := reverseExposedMethods(clientSpec) serviceNames := map[string]struct{}{} for key := range reversedMethods { serviceName := strings.Split(key, "::")[0] serviceNames[serviceName] = struct{}{} } services := ServiceSpecs{} for name := range serviceNames { services = append(services, &ServiceSpec{Name: name}) } sort.Sort(&services) var clientQPSLevels map[string]string = GetClientQPSLevels(instance.QPSLevels, reversedMethods, clientSpec.ClientID) // @rpatali: Update all struct to use more general field IDLFile instead of thriftFile. clientMeta := &ClientMeta{ ProtoServices: clientSpec.ModuleSpec.ProtoServices, Instance: instance, ExportName: clientSpec.ExportName, ExportType: clientSpec.ExportType, Services: services, IncludedPackages: clientSpec.ModuleSpec.IncludedPackages, ClientID: clientSpec.ClientID, ExposedMethods: reversedMethods, QPSLevels: clientQPSLevels, } client, err := ExecuteDefaultOrCustomTemplate( "grpc_client.tmpl", g.templates, instance.CustomTemplates, instance.Config, clientMeta, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error executing gRPC client template for %q", instance.InstanceName, ) } // When it is possible to generate structs for all module types, the // module system will do this transparently. For now we are opting in // on a per-generator basis. dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { return nil, errors.Wrapf( err, "Error generating dependencies struct for %q %q", instance.ClassName, instance.InstanceName, ) } baseName := filepath.Base(instance.Directory) clientFilePath := baseName + ".go" files := map[string][]byte{ clientFilePath: client, } if dependencies != nil { files["module/dependencies.go"] = dependencies } return &BuildResult{ Files: files, Spec: clientSpec, }, nil } /* * Endpoint Generator */ // EndpointGenerator generates a group of zanzibar endpoints that proxy corresponding clients type EndpointGenerator struct { templates *Template packageHelper *PackageHelper } // ComputeSpec computes the endpoint specs for a group of endpoints func (g *EndpointGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { endpointYamls := []string{} endpointSpecs := []*EndpointSpec{} clientSpecs := readClientDependencySpecs(instance) endpointConfig, err := readEndpointConfig(instance.YAMLFileRaw) if err != nil { return nil, errors.Wrapf( err, "Error reading HTTP endpoint %q YAML config", instance.InstanceName, ) } endpointConfigDir := filepath.Join( instance.BaseDirectory, instance.Directory, ) for _, fileName := range endpointConfig.Config.Endpoints { endpointYamls = append( endpointYamls, filepath.Join(endpointConfigDir, fileName), ) } var wg sync.WaitGroup wg.Add(len(endpointYamls)) ch := make(chan endpointSpecRes, len(endpointYamls)) for _, yamlFile := range endpointYamls { go func(yamlFile string) { defer wg.Done() espec, err := NewEndpointSpec(yamlFile, g.packageHelper, g.packageHelper.MiddlewareSpecs()) if err != nil { err = errors.Wrapf( err, "Error creating endpoint spec for endpoint: %s", yamlFile, ) ch <- endpointSpecRes{err: err} return } err = espec.SetDownstream(clientSpecs, g.packageHelper) if err != nil { err = errors.Wrapf( err, "Error parsing downstream info for endpoint: %s", yamlFile, ) ch <- endpointSpecRes{err: err} return } ch <- endpointSpecRes{espec: espec, err: err} }(yamlFile) } go func() { wg.Wait() close(ch) }() for endpointSpecRes := range ch { if endpointSpecRes.err != nil { return nil, endpointSpecRes.err } endpointSpecs = append(endpointSpecs, endpointSpecRes.espec) } return endpointSpecs, nil } type endpointSpecRes struct { espec *EndpointSpec err error } // Generate returns the endpoint build result, which contains a file per // endpoint handler and a list of handler specs func (g *EndpointGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { endpointMeta := make([]*EndpointMeta, 0) endpointSpecsUntyped, err := g.ComputeSpec(instance) if err != nil { return nil, errors.Wrapf( err, "Error generating endpoint specs for %q", instance.InstanceName, ) } endpointSpecs := endpointSpecsUntyped.([]*EndpointSpec) for _, espec := range endpointSpecs { // allow configured header to pass down to switch downstream service dynmamic reqHeaders := espec.ReqHeaders if reqHeaders == nil { reqHeaders = make(map[string]*TypedHeader) } shk := textproto.CanonicalMIMEHeaderKey(g.packageHelper.DeputyReqHeader()) reqHeaders[shk] = &TypedHeader{ Name: shk, TransformTo: shk, Field: &compile.FieldSpec{Required: false}, } espec.ReqHeaders = reqHeaders } var fileMap sync.Map runner := parallelize.NewUnboundedRunner(len(endpointSpecs) + 1) for _, espec := range endpointSpecs { f := func(especInf interface{}) (interface{}, error) { espec := especInf.(*EndpointSpec) meta, err := g.generateEndpointFile(espec, instance, &fileMap) if err != nil { err = errors.Wrapf( err, "Error executing endpoint template %q", instance.InstanceName, ) return nil, err } err = g.generateEndpointTestFile(espec, instance, &fileMap) if err != nil { err = errors.Wrapf( err, "Error executing endpoint test template %q", instance.InstanceName, ) return nil, err } return meta, err } wrk := &parallelize.SingleParamWork{Data: espec, Func: f} runner.SubmitWork(wrk) } f := func() (interface{}, error) { dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { err = errors.Wrapf( err, "Error generating service dependencies for %s", instance.InstanceName, ) return nil, err } if dependencies != nil { fileMap.Store("module/dependencies.go", dependencies) } return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) results, err := runner.GetResult() if err != nil { return nil, err } for _, meta := range results { if meta == nil { continue } endpointMeta = append(endpointMeta, meta.(*EndpointMeta)) } // sort for deterministic code gen order in endpoint.go file sort.SliceStable(endpointMeta, func(i, j int) bool { return endpointMeta[i].Spec.HandleID < endpointMeta[j].Spec.HandleID }) endpointCollection, err := ExecuteDefaultOrCustomTemplate( "endpoint_collection.tmpl", g.templates, instance.CustomTemplates, instance.Config, &EndpointCollectionMeta{ Instance: instance, EndpointMeta: endpointMeta, }, g.packageHelper, ) if err != nil { return nil, errors.Wrapf( err, "Error generating service dependencies for %s", instance.InstanceName, ) } fileMap.Store("endpoint.go", endpointCollection) files := make(map[string][]byte) fileMap.Range(func(key, value interface{}) bool { files[key.(string)] = value.([]byte) return true }) return &BuildResult{ Files: files, Spec: endpointSpecs, }, nil } type endpointMetaResult struct { meta *EndpointMeta err error } func (g *EndpointGenerator) generateEndpointFile(e *EndpointSpec, instance *ModuleInstance, out *sync.Map) (*EndpointMeta, error) { m := e.ModuleSpec methodName := e.ThriftMethodName thriftServiceName := e.ThriftServiceName if len(m.Services) == 0 { return nil, nil } endpointDirectory := filepath.Join( g.packageHelper.CodeGenTargetPath(), instance.Directory, ) var err error if e.EndpointType == "http" { structFilePath, err := filepath.Rel(endpointDirectory, e.GoStructsFileName) if err != nil { structFilePath = e.GoStructsFileName } if _, ok := out.Load(structFilePath); !ok { meta := &StructMeta{ Instance: instance, Spec: m, } structs, err := ExecuteDefaultOrCustomTemplate( "structs.tmpl", g.templates, instance.CustomTemplates, e.Config, meta, g.packageHelper, ) if err != nil { return nil, err } out.Store(structFilePath, structs) } } method := findMethod(m, thriftServiceName, methodName) if method == nil { return nil, errors.Errorf( "Could not find thriftServiceName %q + methodName %q in module", thriftServiceName, methodName, ) } includedPackages := m.IncludedPackages includedPackages = append(includedPackages, GoPackageImport{ PackageName: instance.PackageInfo.GeneratedPackagePath + "/workflow", AliasName: "workflow", }) if e.WorkflowType == customWorkflow { includedPackages = append(includedPackages, GoPackageImport{ PackageName: e.WorkflowImportPath, AliasName: "custom" + strings.Title(m.PackageName), }) } workflowPkg := "workflow" if method.Downstream == nil && e.WorkflowType == "custom" { workflowPkg = "custom" + strings.Title(m.PackageName) } clientID := e.ClientID clientName := "" clientType := "clientless" if e.ClientSpec != nil { clientName = e.ClientSpec.ClientName clientType = e.ClientSpec.ClientType } // TODO: http client needs to support multiple thrift services meta := &EndpointMeta{ Instance: instance, Spec: e, GatewayPackageName: g.packageHelper.GoGatewayPackageName(), IncludedPackages: includedPackages, Method: method, ReqHeaders: e.ReqHeaders, IsClientlessEndpoint: e.IsClientlessEndpoint, ReqHeadersKeys: sortedHeaders(e.ReqHeaders, false), ReqRequiredHeadersKeys: sortedHeaders(e.ReqHeaders, true), ResHeadersKeys: sortedHeaders(e.ResHeaders, false), ResRequiredHeadersKeys: sortedHeaders(e.ResHeaders, true), ResHeaders: e.ResHeaders, ClientID: clientID, ClientName: clientName, ClientType: clientType, ClientMethodName: e.ClientMethod, WorkflowPkg: workflowPkg, TraceKey: g.packageHelper.traceKey, DeputyReqHeader: g.packageHelper.DeputyReqHeader(), DefaultHeaders: e.DefaultHeaders, } targetPath := e.TargetEndpointPath(thriftServiceName, method.Name) if e.EndpointType == "tchannel" { targetPath = strings.TrimSuffix(targetPath, ".go") + "_tchannel.go" } endpointFilePath, err := filepath.Rel(endpointDirectory, targetPath) if err != nil { endpointFilePath = targetPath } workCount := 2 runner := parallelize.NewUnboundedRunner(workCount) f := func() (interface{}, error) { var endpoint []byte if e.EndpointType == "http" { endpoint, err = ExecuteDefaultOrCustomTemplate("endpoint.tmpl", g.templates, instance.CustomTemplates, e.Config, meta, g.packageHelper) } else if e.EndpointType == "tchannel" { endpoint, err = g.templates.ExecTemplate("tchannel_endpoint.tmpl", meta, g.packageHelper) } else { err = errors.Errorf("Endpoint type '%s' is not supported", e.EndpointType) } if err != nil { return nil, errors.Wrap(err, "Error executing endpoint template") } out.Store(endpointFilePath, endpoint) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) f = func() (interface{}, error) { var tmpl string if e.IsClientlessEndpoint { tmpl = "clientless-workflow.tmpl" } else { tmpl = "workflow.tmpl" } workflow, err := ExecuteDefaultOrCustomTemplate(tmpl, g.templates, instance.CustomTemplates, e.Config, meta, g.packageHelper) if err != nil { return nil, errors.Wrap(err, "Error executing workflow template") } out.Store("workflow/"+endpointFilePath, workflow) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) if _, err := runner.GetResult(); err != nil { return nil, err } return meta, nil } func (g *EndpointGenerator) generateEndpointTestFile( e *EndpointSpec, instance *ModuleInstance, out *sync.Map, ) error { if len(e.TestFixtures) < 1 { // skip tests if testFixtures is missing return nil } m := e.ModuleSpec methodName := e.ThriftMethodName serviceName := e.ThriftServiceName if len(m.Services) == 0 { return nil } method := findMethod(m, serviceName, methodName) if method == nil { return errors.Errorf( "Could not find thriftServiceName %q + methodName %q in module", serviceName, methodName, ) } endpointDirectory := filepath.Join( g.packageHelper.CodeGenTargetPath(), instance.Directory, ) targetPath := e.TargetEndpointTestPath(serviceName, methodName) endpointTestFilePath, err := filepath.Rel(endpointDirectory, targetPath) if err != nil { endpointTestFilePath = targetPath } meta := &EndpointTestMeta{ Instance: instance, Method: method, TestFixtures: e.TestFixtures, ReqHeaders: e.ReqHeaders, ResHeaders: e.ResHeaders, ClientID: e.ClientSpec.ClientID, } relativePath, err := filepath.Rel( targetPath, g.packageHelper.CodeGenTargetPath(), ) if err != nil { return errors.Wrap(err, "Error computing relative path for endpoint test template", ) } meta.RelativePathToRoot = relativePath tempName := "endpoint_test.tmpl" if e.WorkflowType == "tchannelClient" { meta.ClientName = e.ClientSpec.ClientName meta.IncludedPackages = append( method.Downstream.IncludedPackages, GoPackageImport{ AliasName: method.Downstream.PackageName, PackageName: e.ClientSpec.ImportPackagePath, }, ) tempName = "endpoint_test_tchannel_client.tmpl" } endpointTest, err := ExecuteDefaultOrCustomTemplate(tempName, g.templates, instance.CustomTemplates, instance.Config, meta, g.packageHelper) if err != nil { return errors.Wrap(err, "Error executing endpoint test template") } out.Store(endpointTestFilePath, endpointTest) return nil } /* * Gateway Service Generator */ // GatewayServiceGenerator generates an entry point for a single service as // a main.go that bootstraps the service and its dependencies type GatewayServiceGenerator struct { templates *Template packageHelper *PackageHelper } // NewGatewayServiceGenerator creates a new gateway service generator. func NewGatewayServiceGenerator(t *Template, h *PackageHelper) *GatewayServiceGenerator { return &GatewayServiceGenerator{ templates: t, packageHelper: h, } } // ComputeSpec computes the spec for a gateway func (generator *GatewayServiceGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { return nil, nil } // Generate returns the gateway build result, which contains the service and // service test main files, and no spec func (generator *GatewayServiceGenerator) Generate(instance *ModuleInstance) (*BuildResult, error) { var fileMap sync.Map workCount := 5 runner := parallelize.NewUnboundedRunner(workCount) f := func() (interface{}, error) { service, err := ExecuteDefaultOrCustomTemplate( "service.tmpl", generator.templates, instance.CustomTemplates, instance.Config, instance, generator.packageHelper, ) if err != nil { err = errors.Wrapf( err, "Error generating service service.go for %s", instance.InstanceName, ) return nil, err } fileMap.Store("service.go", service) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) f = func() (interface{}, error) { // generate main.go main, err := ExecuteDefaultOrCustomTemplate( "main.tmpl", generator.templates, instance.CustomTemplates, instance.Config, instance, generator.packageHelper, ) if err != nil { err = errors.Wrapf( err, "Error generating service main.go for %s", instance.InstanceName, ) return nil, err } fileMap.Store("main/main.go", main) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) f = func() (interface{}, error) { // generate main_test.go mainTest, err := ExecuteDefaultOrCustomTemplate( "main_test.tmpl", generator.templates, instance.CustomTemplates, instance.Config, instance, generator.packageHelper, ) if err != nil { err = errors.Wrapf( err, "Error generating service main_test.go for %s", instance.InstanceName, ) return nil, err } fileMap.Store("main/main_test.go", mainTest) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) f = func() (interface{}, error) { dependencies, err := GenerateDependencyStruct( instance, generator.packageHelper, generator.templates, ) if err != nil { err = errors.Wrapf( err, "Error generating service dependencies for %s", instance.InstanceName, ) return nil, err } fileMap.Store("module/dependencies.go", dependencies) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) f = func() (interface{}, error) { initializer, err := GenerateInitializer( instance, generator.packageHelper, generator.templates, ) if err != nil { err = errors.Wrapf( err, "Error generating service initializer for %s", instance.InstanceName, ) return nil, err } fileMap.Store("module/init.go", initializer) return nil, nil } runner.SubmitWork(parallelize.StatelessFunc(f)) if _, err := runner.GetResult(); err != nil { return nil, err } files := make(map[string][]byte) fileMap.Range(func(key, value interface{}) bool { files[key.(string)] = value.([]byte) return true }) return &BuildResult{ Files: files, }, nil } /* * Middleware Generator */ // MiddlewareGenerator generates a group of zanzibar endpoints that proxy corresponding clients type MiddlewareGenerator struct { templates *Template packageHelper *PackageHelper } // ComputeSpec computes the spec for a middleware func (g *MiddlewareGenerator) ComputeSpec( instance *ModuleInstance, ) (interface{}, error) { return nil, nil } // Generate returns the endpoint build result, which contains a file per // endpoint handler and a list of handler specs func (g *MiddlewareGenerator) Generate( instance *ModuleInstance, ) (*BuildResult, error) { ret := map[string][]byte{} dependencies, err := GenerateDependencyStruct( instance, g.packageHelper, g.templates, ) if err != nil { return nil, errors.Wrapf( err, "Error generating service dependencies for %s %s", instance.InstanceName, instance.ClassName, ) } if dependencies != nil { ret["module/dependencies.go"] = dependencies } err = g.generateMiddlewareFile(instance, ret) if err != nil { return nil, errors.Wrapf( err, "Error generating middleware file for %s %s", instance.InstanceName, instance.ClassName, ) } return &BuildResult{ Files: ret, Spec: nil, }, nil } func (g *MiddlewareGenerator) generateMiddlewareFile(instance *ModuleInstance, out map[string][]byte) error { templateName := "middleware_http.tmpl" if instance.ClassType == "tchannel" { templateName = "middleware_tchannel.tmpl" } bytes, err := ExecuteDefaultOrCustomTemplate(templateName, g.templates, instance.CustomTemplates, instance.Config, instance, g.packageHelper) if err != nil { return err } baseName := filepath.Base(instance.Directory) out[baseName+".go"] = bytes return nil } // gets client dependencies (can we get endpoint dependencies like this) func readClientDependencySpecs(instance *ModuleInstance) []*ClientSpec { clients := []*ClientSpec{} for _, clientDep := range instance.ResolvedDependencies["client"] { clients = append(clients, clientDep.GeneratedSpec().(*ClientSpec)) } sort.Sort(sortByClientName(clients)) return clients } // GenerateDependencyStruct generates a module struct with placeholders for the // instance module based on the defined dependency configuration func GenerateDependencyStruct( instance *ModuleInstance, packageHelper *PackageHelper, template *Template, ) ([]byte, error) { genCustom, _ := instance.Config["customInterface"].(string) if genCustom != "" { instance.PackageInfo.ExportType = instance.Config["customInterface"].(string) } return ExecuteDefaultOrCustomTemplate( "dependency_struct.tmpl", template, instance.CustomTemplates, instance.Config, instance, packageHelper, ) } // GenerateInitializer generates a file that initializes a module fully // recursively, i.e. by initializing all of its dependencies in the correct // order func GenerateInitializer( instance *ModuleInstance, packageHelper *PackageHelper, template *Template, ) ([]byte, error) { return ExecuteDefaultOrCustomTemplate( "module_initializer.tmpl", template, instance.CustomTemplates, instance.Config, instance, packageHelper, ) } // ExecuteDefaultOrCustomTemplate verify and execute a default or custom template func ExecuteDefaultOrCustomTemplate( defaultTemplateName string, defaultTemplates *Template, customTemplates *Template, config map[string]interface{}, tplData interface{}, packageHelper *PackageHelper, ) (ret []byte, rErr error) { tmplName, templates := GetDefaultOrCustomTemplate(defaultTemplateName, defaultTemplates, customTemplates, config) return templates.ExecTemplate( tmplName, tplData, packageHelper, ) } /* * General client meta */ // ClientMeta ... type ClientMeta struct { Instance *ModuleInstance ExportName string ExportType string ClientID string IncludedPackages []GoPackageImport Services []*ServiceSpec ProtoServices []*ProtoService ExposedMethods map[string]string // client-method name to qps level (string) // if not qps level for method value is "default" QPSLevels map[string]string SidecarRouter string Fixture *Fixture DeputyReqHeader string } func findMethod( m *ModuleSpec, serviceName string, methodName string, ) *MethodSpec { for _, service := range m.Services { if service.Name != serviceName { continue } for _, method := range service.Methods { if method.Name == methodName { return method } } } return nil } type sortByClientName []*ClientSpec func (c sortByClientName) Len() int { return len(c) } func (c sortByClientName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c sortByClientName) Less(i, j int) bool { return c[i].ClientName < c[j].ClientName }