mirror of https://github.com/pulumi/pulumi.git
371 lines
11 KiB
Go
371 lines
11 KiB
Go
// Copyright 2016-2023, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"unicode"
|
|
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
|
|
codegenrpc "github.com/pulumi/pulumi/sdk/v3/proto/go/codegen"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
)
|
|
|
|
func format(fullname string) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Fatalf("failed to get current working directory: %v", err)
|
|
}
|
|
|
|
abs, err := filepath.Abs(fullname)
|
|
if err != nil {
|
|
log.Fatalf("failed to get absolute path for %q: %v", fullname, err)
|
|
}
|
|
|
|
parent := filepath.Dir(cwd)
|
|
rel, err := filepath.Rel(parent, abs)
|
|
if err != nil {
|
|
log.Fatalf("failed to get relative path for %q from %q: %v", abs, parent, err)
|
|
}
|
|
|
|
gofmt := exec.Command("gofumpt", "-w", rel)
|
|
gofmt.Dir = parent
|
|
|
|
stderr, err := gofmt.StderrPipe()
|
|
if err != nil {
|
|
log.Fatalf("failed to pipe stderr from gofmt: %v", err)
|
|
}
|
|
go func() {
|
|
_, err := io.Copy(os.Stderr, stderr)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("unexpected error running gofmt: %v", err))
|
|
}
|
|
}()
|
|
if err := gofmt.Run(); err != nil {
|
|
log.Printf("failed to gofmt %v: %v", fullname, err)
|
|
}
|
|
}
|
|
|
|
func allUpper(name string) bool {
|
|
for _, c := range name {
|
|
if c >= 'a' && c <= 'z' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func protobufNameToGoName(name string) string {
|
|
// A name looks like "separate_word" or "separateWord", either way this should become "SeparateWord".
|
|
|
|
nameParts := strings.Split(name, "_")
|
|
goName := ""
|
|
for _, part := range nameParts {
|
|
isFirst := true
|
|
goName += strings.Map(func(r rune) rune {
|
|
if isFirst {
|
|
isFirst = false
|
|
return unicode.ToUpper(r)
|
|
}
|
|
return r
|
|
}, part)
|
|
}
|
|
return goName
|
|
}
|
|
|
|
func protobufTypeToGoType(name string) string {
|
|
// A protobuf type might look like pulumirpc.codegen.Mapper, in this case we want to take the last part as
|
|
// the type name, but the first part needs mapping to the go module name.
|
|
parts := strings.Split(name, ".")
|
|
module := strings.Join(parts[:len(parts)-1], ".")
|
|
switch module {
|
|
case "pulumirpc":
|
|
return "pulumirpc." + parts[len(parts)-1]
|
|
case "pulumirpc.codegen":
|
|
return "codegenrpc." + parts[len(parts)-1]
|
|
default:
|
|
panic(fmt.Sprintf("unexpected protobuf module: %s", module))
|
|
}
|
|
}
|
|
|
|
func pulumiNameToGoName(name string) string {
|
|
// A name looks like "separate_word_AKA", that is each part is separated by "_" and acronyms are
|
|
// uppercase.
|
|
|
|
nameParts := strings.Split(name, "_")
|
|
goName := ""
|
|
titleCaser := cases.Title(language.English)
|
|
for _, part := range nameParts {
|
|
if allUpper(part) {
|
|
goName += part
|
|
continue
|
|
}
|
|
goName += titleCaser.String(part)
|
|
}
|
|
return goName
|
|
}
|
|
|
|
func refToGoType(ref string) string {
|
|
parts := strings.Split(ref, ".")
|
|
return pulumiNameToGoName(parts[len(parts)-1])
|
|
}
|
|
|
|
func pulumiTypeToGoType(typ *codegenrpc.TypeReference) string {
|
|
switch e := typ.Element.(type) {
|
|
case *codegenrpc.TypeReference_Primitive:
|
|
switch e.Primitive {
|
|
case codegenrpc.PrimitiveType_BOOL:
|
|
return "bool"
|
|
case codegenrpc.PrimitiveType_BYTE:
|
|
return "byte"
|
|
case codegenrpc.PrimitiveType_INT:
|
|
return "int"
|
|
case codegenrpc.PrimitiveType_STRING:
|
|
return "string"
|
|
case codegenrpc.PrimitiveType_DURATION:
|
|
return "time.Duration"
|
|
case codegenrpc.PrimitiveType_PROPERTY_VALUE:
|
|
return "resource.PropertyValue"
|
|
}
|
|
case *codegenrpc.TypeReference_Ref:
|
|
return refToGoType(e.Ref)
|
|
case *codegenrpc.TypeReference_Map:
|
|
// Special case for PropertyMap.
|
|
if e.Map.GetPrimitive() == codegenrpc.PrimitiveType_PROPERTY_VALUE {
|
|
return "resource.PropertyMap"
|
|
}
|
|
return "map[string]" + pulumiTypeToGoType(e.Map)
|
|
case *codegenrpc.TypeReference_Array:
|
|
return "[]" + pulumiTypeToGoType(e.Array)
|
|
}
|
|
|
|
log.Fatalf("unhandled type: %v", typ)
|
|
return ""
|
|
}
|
|
|
|
func needsMarshal(typ *codegenrpc.TypeReference) bool {
|
|
switch e := typ.Element.(type) {
|
|
case *codegenrpc.TypeReference_Primitive:
|
|
switch e.Primitive {
|
|
case codegenrpc.PrimitiveType_PROPERTY_VALUE:
|
|
return true
|
|
}
|
|
case *codegenrpc.TypeReference_Ref:
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func toTemplateData(typeName string, data map[string]interface{}) string {
|
|
parts := strings.Split(typeName, ".")
|
|
|
|
data["Name"] = pulumiNameToGoName(parts[len(parts)-1])
|
|
if len(parts) > 1 {
|
|
data["Package"] = parts[len(parts)-2]
|
|
} else {
|
|
data["Package"] = "pulumi"
|
|
}
|
|
|
|
path := filepath.Join(parts[:len(parts)-1]...)
|
|
fullname := filepath.Join("..", path, parts[len(parts)-1]+".go")
|
|
return fullname
|
|
}
|
|
|
|
func executeTemplate(templates *template.Template, templateName, templatePath string, data map[string]interface{}) {
|
|
log.Printf("Writing %v with template %s using data %v", templatePath, templateName, data)
|
|
if err := os.MkdirAll(filepath.Dir(templatePath), 0755); err != nil {
|
|
log.Fatalf("create directory %q: %v", filepath.Dir(templatePath), err)
|
|
}
|
|
|
|
f, err := os.Create(templatePath)
|
|
if err != nil {
|
|
log.Fatalf("create %q: %v", templatePath, err)
|
|
}
|
|
if err := templates.ExecuteTemplate(f, templateName, data); err != nil {
|
|
log.Fatalf("execute %q: %v", templateName, err)
|
|
}
|
|
f.Close()
|
|
|
|
format(templatePath)
|
|
}
|
|
|
|
// Generate the code for a Pulumi package.
|
|
func main() {
|
|
coreSchemaJson, err := os.ReadFile("../../../proto/core.json")
|
|
if err != nil {
|
|
log.Fatalf("read core.json: %v", err)
|
|
}
|
|
|
|
var core codegenrpc.Core
|
|
if err := protojson.Unmarshal(coreSchemaJson, &core); err != nil {
|
|
log.Fatalf("parse core schema: %v", err)
|
|
}
|
|
|
|
templates := template.New("templates")
|
|
templates.Funcs(template.FuncMap{
|
|
"lower": strings.ToLower,
|
|
"split": strings.Split,
|
|
})
|
|
|
|
templates, err = templates.ParseGlob("./templates/*")
|
|
if err != nil {
|
|
log.Fatalf("failed to parse templates: %v", err)
|
|
}
|
|
|
|
for _, typ := range core.Sdk.TypeDeclarations {
|
|
log.Printf("Generating %v", typ)
|
|
|
|
switch e := typ.Element.(type) {
|
|
case *codegenrpc.TypeDeclaration_Record:
|
|
properties := make([]interface{}, 0)
|
|
for _, prop := range e.Record.Properties {
|
|
goName := pulumiNameToGoName(prop.Name)
|
|
protobufGoName := protobufNameToGoName(prop.ProtobufField)
|
|
|
|
var marshalCode, unmarshalCode string
|
|
if protobufGoName == "" {
|
|
// This must have a custom mapping
|
|
switch prop.ProtobufMapping {
|
|
case codegenrpc.CustomPropertyMapping_URN_NAME:
|
|
unmarshalCode = fmt.Sprintf("s.%s = string(resource.URN(data.Urn).Name())", goName)
|
|
case codegenrpc.CustomPropertyMapping_URN_TYPE:
|
|
unmarshalCode = fmt.Sprintf("s.%s = string(resource.URN(data.Urn).Type())", goName)
|
|
}
|
|
} else {
|
|
if needsMarshal(prop.Type) {
|
|
marshalCode = fmt.Sprintf("%s: s.%s.marshal()", protobufGoName, goName)
|
|
unmarshalCode = fmt.Sprintf("s.%s.unmarshal(data.%s)", goName, protobufGoName)
|
|
} else {
|
|
marshalCode = fmt.Sprintf("%s: s.%s", protobufGoName, goName)
|
|
unmarshalCode = fmt.Sprintf("s.%s = data.%s", goName, protobufGoName)
|
|
}
|
|
}
|
|
templateProp := map[string]interface{}{
|
|
"Name": goName,
|
|
"Description": strings.Split(prop.Description, "\n"),
|
|
"Type": pulumiTypeToGoType(prop.Type),
|
|
"Marshal": marshalCode,
|
|
"Unmarshal": unmarshalCode,
|
|
"ProtobufPresenceField": protobufNameToGoName(prop.ProtobufPresenceField),
|
|
}
|
|
|
|
properties = append(properties, templateProp)
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Description": strings.Split(e.Record.Description, "\n"),
|
|
"Properties": properties,
|
|
"ProtobufMessage": protobufTypeToGoType(e.Record.ProtobufMessage),
|
|
}
|
|
|
|
templatePath := toTemplateData(e.Record.Name, data)
|
|
executeTemplate(templates, "record.go.template", templatePath, data)
|
|
|
|
case *codegenrpc.TypeDeclaration_Enumeration:
|
|
values := make([]interface{}, 0)
|
|
for _, value := range e.Enumeration.Values {
|
|
templateValue := map[string]interface{}{
|
|
"Name": pulumiNameToGoName(value.Name),
|
|
"Description": strings.Split(value.Description, "\n"),
|
|
"ProtobufValue": value.ProtobufValue,
|
|
}
|
|
|
|
values = append(values, templateValue)
|
|
}
|
|
|
|
// e.Enumeration.ProtobufEnum may be nested e.g. "pulumirpc.DiffResponse.Kind", for such a name the Go type
|
|
// is "pulumirpc.DiffResponse_Kind" and the namespace before the enum values is "pulumirpc.DiffResponse". For a root
|
|
// type e.g. "pulumirpc.LogSeverity" the type and namespace are the same.
|
|
parts := strings.Split(e.Enumeration.ProtobufEnum, ".")
|
|
var protobufType, protobufNamespace string
|
|
if len(parts) == 2 {
|
|
protobufType = fmt.Sprintf("%s.%s", parts[0], parts[1])
|
|
protobufNamespace = parts[1]
|
|
} else if len(parts) == 3 {
|
|
protobufType = fmt.Sprintf("%s.%s_%s", parts[0], parts[1], parts[2])
|
|
protobufNamespace = parts[1]
|
|
} else {
|
|
panic("unexpected protobuf enum name: " + e.Enumeration.ProtobufEnum)
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Description": strings.Split(e.Enumeration.Description, "\n"),
|
|
"Values": values,
|
|
"ProtobufType": protobufType,
|
|
"ProtobufNamespace": protobufNamespace,
|
|
}
|
|
|
|
templatePath := toTemplateData(e.Enumeration.Name, data)
|
|
executeTemplate(templates, "enum.go.template", templatePath, data)
|
|
|
|
case *codegenrpc.TypeDeclaration_Interface:
|
|
methods := make([]interface{}, 0)
|
|
for _, method := range e.Interface.Methods {
|
|
param := map[string]interface{}{
|
|
"Name": method.Request.Name,
|
|
"Description": strings.Split(method.Request.Description, "\n"),
|
|
"Type": refToGoType(method.Request.Type),
|
|
}
|
|
|
|
templateMethod := map[string]interface{}{
|
|
"Name": pulumiNameToGoName(method.Name),
|
|
"Description": strings.Split(method.Description, "\n"),
|
|
"Parameter": param,
|
|
"GrpcMethod": method.GrpcMethod,
|
|
}
|
|
if method.ResponseType != "" {
|
|
templateMethod["ReturnType"] = refToGoType(method.ResponseType)
|
|
}
|
|
|
|
methods = append(methods, templateMethod)
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Description": strings.Split(e.Interface.Description, "\n"),
|
|
"Methods": methods,
|
|
"GrpcService": protobufTypeToGoType(e.Interface.GrpcService),
|
|
}
|
|
|
|
templatePath := toTemplateData(e.Interface.Name, data)
|
|
executeTemplate(templates, "interface.go.template", templatePath, data)
|
|
|
|
// If we need grpc server/client add them
|
|
if e.Interface.GrpcKind == codegenrpc.GrpcKind_KIND_BOTH ||
|
|
e.Interface.GrpcKind == codegenrpc.GrpcKind_KIND_SERVER {
|
|
templatePath := filepath.Join(filepath.Dir(templatePath), "server_"+filepath.Base(templatePath))
|
|
executeTemplate(templates, "grpc_server.go.template", templatePath, data)
|
|
}
|
|
if e.Interface.GrpcKind == codegenrpc.GrpcKind_KIND_BOTH ||
|
|
e.Interface.GrpcKind == codegenrpc.GrpcKind_KIND_CLIENT {
|
|
templatePath := filepath.Join(filepath.Dir(templatePath), "client_"+filepath.Base(templatePath))
|
|
executeTemplate(templates, "grpc_client.go.template", templatePath, data)
|
|
}
|
|
|
|
default:
|
|
log.Fatalf("unexpected type declaration: %v", typ.Element)
|
|
}
|
|
}
|
|
}
|