| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| // This program is the generator for the gRPC service wrapper types in the |
| // parent directory. It's not suitable for any other use. |
| // |
| // This makes various assumptions about how the protobuf compiler and |
| // gRPC stub generators produce code. If those significantly change in future |
| // then this will probably break. |
| package main |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/format" |
| "go/types" |
| "log" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/tools/go/packages" |
| ) |
| |
| var protobufPkgs = map[string]string{ |
| "dependencies": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies", |
| "stacks": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks", |
| "packages": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages", |
| } |
| |
| func main() { |
| for shortName, pkgName := range protobufPkgs { |
| cfg := &packages.Config{ |
| Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles, |
| } |
| pkgs, err := packages.Load(cfg, pkgName) |
| if err != nil { |
| log.Fatalf("can't load the protobuf/gRPC proxy package: %s", err) |
| } |
| if len(pkgs) != 1 { |
| log.Fatalf("wrong number of packages found") |
| } |
| pkg := pkgs[0] |
| if pkg.TypesInfo == nil { |
| log.Fatalf("types info not available") |
| } |
| if len(pkg.GoFiles) < 1 { |
| log.Fatalf("no files included in package") |
| } |
| |
| // We assume that our output directory is sibling to the directory |
| // containing the protobuf specification. |
| outDir := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "../../dynrpcserver") |
| |
| Types: |
| for _, obj := range pkg.TypesInfo.Defs { |
| typ, ok := obj.(*types.TypeName) |
| if !ok { |
| continue |
| } |
| underTyp := typ.Type().Underlying() |
| iface, ok := underTyp.(*types.Interface) |
| if !ok { |
| continue |
| } |
| if !strings.HasSuffix(typ.Name(), "Server") || typ.Name() == "SetupServer" { |
| // Doesn't look like a generated gRPC server interface |
| continue |
| } |
| |
| // The interfaces used for streaming requests/responses unfortunately |
| // also have a "Server" suffix in the generated Go code, and so |
| // we need to detect those more surgically by noticing that they |
| // have grpc.ServerStream embedded inside. |
| for i := 0; i < iface.NumEmbeddeds(); i++ { |
| emb, ok := iface.EmbeddedType(i).(*types.Named) |
| if !ok { |
| continue |
| } |
| pkg := emb.Obj().Pkg().Path() |
| name := emb.Obj().Name() |
| if pkg == "google.golang.org/grpc" && name == "ServerStream" { |
| continue Types |
| } |
| } |
| |
| // If we get here then what we're holding _seems_ to be a gRPC |
| // server interface, and so we'll generate a dynamic initialization |
| // wrapper for it. |
| |
| ifaceName := typ.Name() |
| baseName := strings.TrimSuffix(ifaceName, "Server") |
| filename := toFilenameCase(baseName) + ".go" |
| absFilename := filepath.Join(outDir, filename) |
| |
| var buf bytes.Buffer |
| |
| fmt.Fprintf(&buf, `// Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| // Code generated by ./generator. DO NOT EDIT. |
| `) |
| |
| fmt.Fprintf(&buf, `package dynrpcserver |
| |
| import ( |
| "context" |
| "sync" |
| |
| %s %q |
| ) |
| |
| `, shortName, pkg) |
| fmt.Fprintf(&buf, "type %s struct {\n", baseName) |
| fmt.Fprintf(&buf, "impl %s.%s\n", shortName, ifaceName) |
| fmt.Fprintln(&buf, "mu sync.RWMutex") |
| buf.WriteString("}\n\n") |
| |
| fmt.Fprintf(&buf, "var _ %s.%s = (*%s)(nil)\n\n", shortName, ifaceName, baseName) |
| |
| fmt.Fprintf(&buf, "func New%sStub() *%s {\n", baseName, baseName) |
| fmt.Fprintf(&buf, "return &%s{}\n", baseName) |
| fmt.Fprintf(&buf, "}\n\n") |
| |
| for i := 0; i < iface.NumMethods(); i++ { |
| method := iface.Method(i) |
| sig := method.Type().(*types.Signature) |
| |
| fmt.Fprintf(&buf, "func (s *%s) %s(", baseName, method.Name()) |
| for i := 0; i < sig.Params().Len(); i++ { |
| param := sig.Params().At(i) |
| |
| // The generated interface types don't include parameter names |
| // and so we just use synthetic parameter names here. |
| name := fmt.Sprintf("a%d", i) |
| genType := typeRef(param.Type().String(), shortName, pkgName) |
| |
| if i > 0 { |
| buf.WriteString(", ") |
| } |
| buf.WriteString(name) |
| buf.WriteString(" ") |
| buf.WriteString(genType) |
| } |
| fmt.Fprintf(&buf, ")") |
| if sig.Results().Len() > 1 { |
| buf.WriteString("(") |
| } |
| for i := 0; i < sig.Results().Len(); i++ { |
| result := sig.Results().At(i) |
| genType := typeRef(result.Type().String(), shortName, pkgName) |
| if i > 0 { |
| buf.WriteString(", ") |
| } |
| buf.WriteString(genType) |
| } |
| if sig.Results().Len() > 1 { |
| buf.WriteString(")") |
| } |
| switch n := sig.Results().Len(); n { |
| case 1: |
| fmt.Fprintf(&buf, ` { |
| impl, err := s.realRPCServer() |
| if err != nil { |
| return err |
| } |
| `) |
| case 2: |
| fmt.Fprintf(&buf, ` { |
| impl, err := s.realRPCServer() |
| if err != nil { |
| return nil, err |
| } |
| `) |
| default: |
| log.Fatalf("don't know how to make a stub for method with %d results", n) |
| } |
| fmt.Fprintf(&buf, "return impl.%s(", method.Name()) |
| for i := 0; i < sig.Params().Len(); i++ { |
| if i > 0 { |
| buf.WriteString(", ") |
| } |
| fmt.Fprintf(&buf, "a%d", i) |
| } |
| fmt.Fprintf(&buf, ")\n}\n\n") |
| } |
| |
| fmt.Fprintf(&buf, ` |
| func (s *%s) ActivateRPCServer(impl %s.%s) { |
| s.mu.Lock() |
| s.impl = impl |
| s.mu.Unlock() |
| } |
| |
| func (s *%s) realRPCServer() (%s.%s, error) { |
| s.mu.RLock() |
| impl := s.impl |
| s.mu.RUnlock() |
| if impl == nil { |
| return nil, unavailableErr |
| } |
| return impl, nil |
| } |
| `, baseName, shortName, ifaceName, baseName, shortName, ifaceName) |
| |
| src, err := format.Source(buf.Bytes()) |
| if err != nil { |
| //log.Fatalf("formatting %s: %s", filename, err) |
| src = buf.Bytes() |
| } |
| f, err := os.Create(absFilename) |
| if err != nil { |
| log.Fatal(err) |
| } |
| _, err = f.Write(src) |
| if err != nil { |
| log.Fatalf("writing %s: %s", filename, err) |
| } |
| |
| } |
| } |
| } |
| |
| func typeRef(fullType, name, pkg string) string { |
| // The following is specialized to only the parameter types |
| // we typically expect to see in a server interface. This |
| // might need extra rules if we step outside the design idiom |
| // we've used for these services so far. |
| switch { |
| case fullType == "context.Context" || fullType == "error": |
| return fullType |
| case fullType == "interface{}" || fullType == "any": |
| return "any" |
| case strings.HasPrefix(fullType, "*"+pkg+"."): |
| return "*" + name + "." + fullType[len(pkg)+2:] |
| case strings.HasPrefix(fullType, pkg+"."): |
| return name + "." + fullType[len(pkg)+1:] |
| default: |
| log.Fatalf("don't know what to do with parameter type %s", fullType) |
| return "" |
| } |
| } |
| |
| var firstCapPattern = regexp.MustCompile("(.)([A-Z][a-z]+)") |
| var otherCapPattern = regexp.MustCompile("([a-z0-9])([A-Z])") |
| |
| func toFilenameCase(typeName string) string { |
| ret := firstCapPattern.ReplaceAllString(typeName, "${1}_${2}") |
| ret = otherCapPattern.ReplaceAllString(ret, "${1}_${2}") |
| return strings.ToLower(ret) |
| } |