1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-01-05 10:20:53 +02:00

Generate Gomu files after the fact (#2258)

* Move file generation to new package

* Use text/template instead of html/template

* Make config variables more consistent

* Combine generate files and print comments there

* Add gomu generate command

* Refactor project templating to file library

* Determine client earlier
This commit is contained in:
Niek den Breeje 2021-09-10 14:20:57 +02:00 committed by GitHub
parent 01b7b4409b
commit e23006b1a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 300 additions and 124 deletions

View File

@ -3,6 +3,7 @@ package cli
import (
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/call"
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/describe"
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/generate"
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/new"
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/run"
_ "github.com/asim/go-micro/cmd/gomu/cmd/cli/services"

View File

@ -0,0 +1,98 @@
package generate
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/asim/go-micro/cmd/gomu/cmd"
"github.com/asim/go-micro/cmd/gomu/file"
"github.com/asim/go-micro/cmd/gomu/file/generator"
tmpl "github.com/asim/go-micro/cmd/gomu/file/template"
"github.com/urfave/cli/v2"
)
func init() {
cmd.Register(&cli.Command{
Name: "generate",
Usage: "Generate project template files after the fact",
Subcommands: []*cli.Command{
{
Name: "skaffold",
Usage: "Generate Skaffold project template files",
Action: Skaffold,
},
},
})
}
// Skaffold generates Skaffold project template files in the current directory.
// Exits on error.
func Skaffold(ctx *cli.Context) error {
dir, err := os.Getwd()
if err != nil {
return err
}
service := dir[strings.LastIndex(dir, "/")+1:]
vendor, err := getServiceVendor(service)
if err != nil {
return err
}
g := generator.New(
generator.Service(service),
generator.Vendor(vendor),
generator.Directory("."),
generator.Client(strings.HasSuffix(service, "-client")),
generator.Skaffold(true),
)
files := []file.File{
{".dockerignore", tmpl.DockerIgnore},
{"go.mod", tmpl.Module},
{"plugins.go", tmpl.Plugins},
{"resources/clusterrole.yaml", tmpl.KubernetesClusterRole},
{"resources/configmap.yaml", tmpl.KubernetesEnv},
{"resources/deployment.yaml", tmpl.KubernetesDeployment},
{"resources/rolebinding.yaml", tmpl.KubernetesRoleBinding},
{"skaffold.yaml", tmpl.SkaffoldCFG},
}
if err := g.Generate(files); err != nil {
return err
}
fmt.Println("skaffold project template files generated")
return nil
}
func getServiceVendor(s string) (string, error) {
f, err := os.Open("go.mod")
if err != nil {
return "", err
}
defer f.Close()
line := ""
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "module ") {
line = scanner.Text()
break
}
}
if line == "" {
return "", nil
}
module := line[strings.LastIndex(line, " ")+1:]
if module == s {
return "", nil
}
return module[:strings.LastIndex(module, "/")] + "/", nil
}

View File

@ -4,12 +4,12 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/asim/go-micro/cmd/gomu/cmd"
tmpl "github.com/asim/go-micro/cmd/gomu/cmd/cli/new/template"
"github.com/asim/go-micro/cmd/gomu/file"
"github.com/asim/go-micro/cmd/gomu/file/generator"
tmpl "github.com/asim/go-micro/cmd/gomu/file/template"
"github.com/urfave/cli/v2"
)
@ -24,21 +24,6 @@ var flags []cli.Flag = []cli.Flag{
},
}
type config struct {
Alias string
Comments []string
Dir string
Vendor string
Client bool
Jaeger bool
Skaffold bool
}
type file struct {
Path string
Tmpl string
}
// NewCommand returns a new new cli command.
func init() {
cmd.Register(&cli.Command{
@ -87,10 +72,11 @@ func createProject(ctx *cli.Context, pt string) error {
return cli.ShowSubcommandHelp(ctx)
}
client := pt == "client"
name, vendor := getNameAndVendor(arg)
dir := name
if pt == "client" {
if client {
dir += "-client"
}
@ -105,7 +91,16 @@ func createProject(ctx *cli.Context, pt string) error {
fmt.Printf("creating %s %s\n", pt, name)
files := []file{
g := generator.New(
generator.Service(name),
generator.Vendor(vendor),
generator.Directory(dir),
generator.Client(client),
generator.Jaeger(ctx.Bool("jaeger")),
generator.Skaffold(ctx.Bool("skaffold")),
)
files := []file.File{
{".dockerignore", tmpl.DockerIgnore},
{".gitignore", tmpl.GitIgnore},
{"Dockerfile", tmpl.Dockerfile},
@ -115,17 +110,17 @@ func createProject(ctx *cli.Context, pt string) error {
switch pt {
case "client":
files = append(files, []file{
files = append(files, []file.File{
{"main.go", tmpl.MainCLT},
}...)
case "function":
files = append(files, []file{
files = append(files, []file.File{
{"handler/" + name + ".go", tmpl.HandlerFNC},
{"main.go", tmpl.MainFNC},
{"proto/" + name + ".proto", tmpl.ProtoFNC},
}...)
case "service":
files = append(files, []file{
files = append(files, []file.File{
{"handler/" + name + ".go", tmpl.HandlerSRV},
{"main.go", tmpl.MainSRV},
{"proto/" + name + ".proto", tmpl.ProtoSRV},
@ -135,7 +130,7 @@ func createProject(ctx *cli.Context, pt string) error {
}
if ctx.Bool("skaffold") {
files = append(files, []file{
files = append(files, []file.File{
{"plugins.go", tmpl.Plugins},
{"resources/clusterrole.yaml", tmpl.KubernetesClusterRole},
{"resources/configmap.yaml", tmpl.KubernetesEnv},
@ -145,22 +140,22 @@ func createProject(ctx *cli.Context, pt string) error {
}...)
}
c := config{
Alias: name,
Dir: dir,
Vendor: vendor,
Client: pt == "client",
Jaeger: ctx.Bool("jaeger"),
Skaffold: ctx.Bool("skaffold"),
if err := g.Generate(files); err != nil {
return err
}
if pt == "client" {
c.Comments = clientComments(name, dir)
var comments []string
if client {
comments = clientComments(name, dir)
} else {
c.Comments = protoComments(name, dir)
comments = protoComments(name, dir)
}
return create(files, c)
for _, comment := range comments {
fmt.Println(comment)
}
return nil
}
func clientComments(name, dir string) []string {
@ -184,49 +179,6 @@ func protoComments(name, dir string) []string {
}
}
func create(files []file, c config) error {
for _, file := range files {
fp := filepath.Join(c.Dir, file.Path)
dir := filepath.Dir(fp)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
f, err := os.Create(fp)
if err != nil {
return err
}
fn := template.FuncMap{
"dehyphen": func(s string) string {
return strings.ReplaceAll(s, "-", "")
},
"lower": strings.ToLower,
"title": func(s string) string {
return strings.ReplaceAll(strings.Title(s), "-", "")
},
}
t, err := template.New(fp).Funcs(fn).Parse(file.Tmpl)
if err != nil {
return err
}
err = t.Execute(f, c)
if err != nil {
return err
}
}
for _, comment := range c.Comments {
fmt.Println(comment)
}
return nil
}
func getNameAndVendor(s string) (string, string) {
var n string
var v string

6
cmd/gomu/file/file.go Normal file
View File

@ -0,0 +1,6 @@
package file
type File struct {
Path string
Template string
}

View File

@ -0,0 +1,70 @@
package generator
import (
"os"
"path/filepath"
"strings"
"text/template"
"github.com/asim/go-micro/cmd/gomu/file"
)
type Generator interface {
Generate([]file.File) error
}
type generator struct {
opts Options
}
// Generate generates project template files.
func (g *generator) Generate(files []file.File) error {
for _, file := range files {
fp := filepath.Join(g.opts.Directory, file.Path)
dir := filepath.Dir(fp)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
f, err := os.Create(fp)
if err != nil {
return err
}
fn := template.FuncMap{
"dehyphen": func(s string) string {
return strings.ReplaceAll(s, "-", "")
},
"lower": strings.ToLower,
"title": func(s string) string {
return strings.ReplaceAll(strings.Title(s), "-", "")
},
}
t, err := template.New(fp).Funcs(fn).Parse(file.Template)
if err != nil {
return err
}
err = t.Execute(f, g.opts)
if err != nil {
return err
}
}
return nil
}
// New returns a new generator struct.
func New(opts ...Option) Generator {
var options Options
for _, o := range opts {
o(&options)
}
return &generator{
opts: options,
}
}

View File

@ -0,0 +1,49 @@
package generator
type Options struct {
Service string
Vendor string
Directory string
Client bool
Jaeger bool
Skaffold bool
}
type Option func(o *Options)
func Service(s string) Option {
return func(o *Options) {
o.Service = s
}
}
func Vendor(v string) Option {
return func(o *Options) {
o.Vendor = v
}
}
func Directory(d string) Option {
return func(o *Options) {
o.Directory = d
}
}
func Client(c bool) Option {
return func(o *Options) {
o.Client = c
}
}
func Jaeger(j bool) Option {
return func(o *Options) {
o.Jaeger = j
}
}
func Skaffold(s bool) Option {
return func(o *Options) {
o.Skaffold = s
}
}

View File

@ -3,7 +3,7 @@ package template
// Dockerfile is the Dockerfile template used for new projects.
var Dockerfile = `FROM golang:alpine AS builder
ENV CGO_ENABLED=0 GOOS=linux
WORKDIR /go/src/{{.Alias}}
WORKDIR /go/src/{{.Service}}{{if .Client}}-client{{end}}
RUN apk --update --no-cache add ca-certificates gcc libtool make musl-dev protoc
COPY {{if not .Client}}Makefile {{end}}go.mod go.sum ./
RUN {{if not .Client}}make init && {{end}}go mod download
@ -12,8 +12,8 @@ RUN make {{if not .Client}}proto {{end}}tidy build
FROM scratch
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
COPY --from=builder /go/src/{{.Alias}}/{{.Alias}} /{{.Alias}}
ENTRYPOINT ["/{{.Alias}}"]
COPY --from=builder /go/src/{{.Service}}{{if .Client}}-client{{end}}/{{.Service}}{{if .Client}}-client{{end}} /{{.Service}}{{if .Client}}-client{{end}}
ENTRYPOINT ["/{{.Service}}{{if .Client}}-client{{end}}"]
CMD []
`

View File

@ -2,5 +2,5 @@ package template
// GitIgnore is the .gitignore template used for new projects.
var GitIgnore = `# don't commit the service binary to vcs
{{.Alias}}
{{.Service}}{{if .Client}}-client{{end}}
`

View File

@ -8,13 +8,13 @@ import (
log "github.com/asim/go-micro/v3/logger"
pb "{{.Vendor}}{{.Dir}}/proto"
pb "{{.Vendor}}{{.Service}}/proto"
)
type {{title .Alias}} struct{}
type {{title .Service}} struct{}
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
log.Infof("Received {{title .Alias}}.Call request: %v", req)
func (e *{{title .Service}}) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
log.Infof("Received {{title .Service}}.Call request: %v", req)
rsp.Msg = "Hello " + req.Name
return nil
}
@ -30,18 +30,18 @@ import (
log "github.com/asim/go-micro/v3/logger"
pb "{{.Vendor}}{{.Dir}}/proto"
pb "{{.Vendor}}{{.Service}}/proto"
)
type {{title .Alias}} struct{}
type {{title .Service}} struct{}
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
log.Infof("Received {{title .Alias}}.Call request: %v", req)
func (e *{{title .Service}}) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
log.Infof("Received {{title .Service}}.Call request: %v", req)
rsp.Msg = "Hello " + req.Name
return nil
}
func (e *{{title .Alias}}) ClientStream(ctx context.Context, stream pb.{{title .Alias}}_ClientStreamStream) error {
func (e *{{title .Service}}) ClientStream(ctx context.Context, stream pb.{{title .Service}}_ClientStreamStream) error {
var count int64
for {
req, err := stream.Recv()
@ -57,8 +57,8 @@ func (e *{{title .Alias}}) ClientStream(ctx context.Context, stream pb.{{title .
}
}
func (e *{{title .Alias}}) ServerStream(ctx context.Context, req *pb.ServerStreamRequest, stream pb.{{title .Alias}}_ServerStreamStream) error {
log.Infof("Received {{title .Alias}}.ServerStream request: %v", req)
func (e *{{title .Service}}) ServerStream(ctx context.Context, req *pb.ServerStreamRequest, stream pb.{{title .Service}}_ServerStreamStream) error {
log.Infof("Received {{title .Service}}.ServerStream request: %v", req)
for i := 0; i < int(req.Count); i++ {
log.Infof("Sending %d", i)
if err := stream.Send(&pb.ServerStreamResponse{
@ -71,7 +71,7 @@ func (e *{{title .Alias}}) ServerStream(ctx context.Context, req *pb.ServerStrea
return nil
}
func (e *{{title .Alias}}) BidiStream(ctx context.Context, stream pb.{{title .Alias}}_BidiStreamStream) error {
func (e *{{title .Service}}) BidiStream(ctx context.Context, stream pb.{{title .Service}}_BidiStreamStream) error {
for {
req, err := stream.Recv()
if err == io.EOF {

View File

@ -7,7 +7,7 @@ var KubernetesEnv = `---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{.Dir}}-env
name: {{.Service}}{{if .Client}}-client{{end}}-env
data:
MICRO_REGISTRY: kubernetes
`
@ -56,23 +56,23 @@ var KubernetesDeployment = `---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{.Dir}}
name: {{.Service}}{{if .Client}}-client{{end}}
labels:
app: {{.Dir}}
app: {{.Service}}{{if .Client}}-client{{end}}
spec:
replicas: 1
selector:
matchLabels:
app: {{.Dir}}
app: {{.Service}}{{if .Client}}-client{{end}}
template:
metadata:
labels:
app: {{.Dir}}
app: {{.Service}}{{if .Client}}-client{{end}}
spec:
containers:
- name: {{.Dir}}
image: {{.Dir}}:latest
- name: {{.Service}}{{if .Client}}-client{{end}}
image: {{.Service}}{{if .Client}}-client{{end}}:latest
envFrom:
- configMapRef:
name: {{.Dir}}-env
name: {{.Service}}{{if .Client}}-client{{end}}-env
`

View File

@ -7,14 +7,14 @@ import (
"context"
"time"
pb "{{.Vendor}}{{lower .Alias}}/proto"
pb "{{.Vendor}}{{lower .Service}}/proto"
"github.com/asim/go-micro/v3"
log "github.com/asim/go-micro/v3/logger"
)
var (
service = "{{lower .Alias}}"
service = "{{lower .Service}}"
version = "latest"
)
@ -44,7 +44,7 @@ func main() {
var MainFNC = `package main
import (
"{{.Vendor}}{{.Dir}}/handler"
"{{.Vendor}}{{.Service}}/handler"
{{if .Jaeger}} ot "github.com/asim/go-micro/plugins/wrapper/trace/opentracing/v3"
{{end}} "github.com/asim/go-micro/v3"
@ -54,7 +54,7 @@ import (
)
var (
service = "{{lower .Alias}}"
service = "{{lower .Service}}"
version = "latest"
)
@ -82,7 +82,7 @@ func main() {
fnc.Init()
// Handle function
fnc.Handle(new(handler.{{title .Alias}}))
fnc.Handle(new(handler.{{title .Service}}))
// Run function
if err := fnc.Run(); err != nil {
@ -95,8 +95,8 @@ func main() {
var MainSRV = `package main
import (
"{{.Vendor}}{{.Dir}}/handler"
pb "{{.Vendor}}{{.Dir}}/proto"
"{{.Vendor}}{{.Service}}/handler"
pb "{{.Vendor}}{{.Service}}/proto"
{{if .Jaeger}} ot "github.com/asim/go-micro/plugins/wrapper/trace/opentracing/v3"
{{end}} "github.com/asim/go-micro/v3"
@ -106,7 +106,7 @@ import (
)
var (
service = "{{lower .Alias}}"
service = "{{lower .Service}}"
version = "latest"
)
@ -134,7 +134,7 @@ func main() {
srv.Init()
// Register handler
pb.Register{{title .Alias}}Handler(srv.Server(), new(handler.{{title .Alias}}))
pb.Register{{title .Service}}Handler(srv.Server(), new(handler.{{title .Service}}))
// Run service
if err := srv.Run(); err != nil {

View File

@ -11,7 +11,7 @@ init:
.PHONY: proto
proto:
@protoc --proto_path=. --micro_out=. --go_out=:. proto/{{.Alias}}.proto
@protoc --proto_path=. --micro_out=. --go_out=:. proto/{{.Service}}.proto
.PHONY: tidy
tidy:
@ -19,7 +19,7 @@ tidy:
.PHONY: build
build:
@go build -o {{.Alias}} *.go
@go build -o {{.Service}}{{if .Client}}-client{{end}} *.go
.PHONY: test
test:
@ -27,5 +27,5 @@ test:
.PHONY: docker
docker:
@docker build -t {{.Alias}}:latest .
@docker build -t {{.Service}}{{if .Client}}-client{{end}}:latest .
`

View File

@ -1,7 +1,7 @@
package template
// Module is the go.mod template used for new projects.
var Module = `module {{.Vendor}}{{.Dir}}
var Module = `module {{.Vendor}}{{.Service}}{{if .Client}}-client{{end}}
go 1.16
@ -13,5 +13,5 @@ require (
// see https://github.com/etcd-io/etcd/issues/11154 and https://github.com/etcd-io/etcd/issues/11931.
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0{{if .Vendor}}{{if not .Skaffold}}
replace {{.Vendor}}{{lower .Alias}} => ../{{lower .Alias}}{{end}}{{end}}
replace {{.Vendor}}{{lower .Service}} => ../{{lower .Service}}{{end}}{{end}}
`

View File

@ -3,11 +3,11 @@ package template
// ProtoFNC is the .proto file template used for new function projects.
var ProtoFNC = `syntax = "proto3";
package {{dehyphen .Alias}};
package {{dehyphen .Service}};
option go_package = "./proto;{{dehyphen .Alias}}";
option go_package = "./proto;{{dehyphen .Service}}";
service {{title .Alias}} {
service {{title .Service}} {
rpc Call(CallRequest) returns (CallResponse) {}
}
@ -23,11 +23,11 @@ message CallResponse {
// ProtoSRV is the .proto file template used for new service projects.
var ProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
package {{dehyphen .Service}};
option go_package = "./proto;{{dehyphen .Alias}}";
option go_package = "./proto;{{dehyphen .Service}}";
service {{title .Alias}} {
service {{title .Service}} {
rpc Call(CallRequest) returns (CallResponse) {}
rpc ClientStream(stream ClientStreamRequest) returns (ClientStreamResponse) {}
rpc ServerStream(ServerStreamRequest) returns (stream ServerStreamResponse) {}

View File

@ -6,10 +6,10 @@ var SkaffoldCFG = `---
apiVersion: skaffold/v2beta21
kind: Config
metadata:
name: {{.Dir}}
name: {{.Service}}{{if .Client}}-client{{end}}
build:
artifacts:
- image: {{.Dir}}
- image: {{.Service}}{{if .Client}}-client{{end}}
deploy:
kubectl:
manifests: