From 65add2a61dc6e912ebc93c806af72f320bcbb01e Mon Sep 17 00:00:00 2001 From: Matthieu 'JP' DERASSE Date: Sun, 1 Jan 2023 16:59:41 +0000 Subject: [PATCH] feat(go-swagger): Refactoring to use go-swagger templating system for most of the work --- .golangci.yml | 2 +- go.mod | 4 +- helpers/api_types/go_swagger/constants.go | 7 + helpers/api_types/go_swagger/generate_api.go | 67 +++++- .../go_swagger/generate_dockerfile.go | 2 +- .../api_types/go_swagger/generate_launcher.go | 2 +- .../api_types/go_swagger/generate_makefile.go | 2 +- .../api_types/go_swagger/generate_readme.go | 2 +- helpers/api_types/go_swagger/go_swagger.go | 15 +- helpers/api_types/go_swagger/processes.go | 2 + helpers/api_types/go_swagger/yaml.go | 9 +- helpers/file.go | 6 +- helpers/golang.go | 4 +- templates/go-swagger/app/config.yaml | 60 +++++ .../app/templates/api/handler.gotmpl | 33 +++ .../go-swagger/app/templates/api/tag.gotmpl | 6 + .../app/templates/server/configureapi.gotmpl | 222 ++++++++++++++++++ .../app/templates/server/main.gotmpl | 92 ++++++++ .../app/templates/server/server.gotmpl | 9 + .../app/templates/server/tag.gotmpl | 13 + templates/go-swagger/cmd/main.go.tmpl | 0 .../go-swagger/cmd/operation_group.go.tmpl | 0 .../go-swagger/{ => custom}/Dockerfile.tmpl | 0 .../go-swagger/{ => custom}/Makefile.tmpl | 0 .../go-swagger/{ => custom}/Readme.md.tmpl | 0 .../go-swagger/{ => custom}/api.yaml.tmpl | 0 .../{ => custom}/api/001-general.yaml.tmpl | 0 .../{ => custom}/api/002-monitoring.yaml.tmpl | 0 .../go-swagger/{ => custom}/launcher.sh.tmpl | 0 29 files changed, 538 insertions(+), 21 deletions(-) create mode 100644 templates/go-swagger/app/config.yaml create mode 100644 templates/go-swagger/app/templates/api/handler.gotmpl create mode 100644 templates/go-swagger/app/templates/api/tag.gotmpl create mode 100644 templates/go-swagger/app/templates/server/configureapi.gotmpl create mode 100644 templates/go-swagger/app/templates/server/main.gotmpl create mode 100644 templates/go-swagger/app/templates/server/server.gotmpl create mode 100644 templates/go-swagger/app/templates/server/tag.gotmpl create mode 100644 templates/go-swagger/cmd/main.go.tmpl create mode 100644 templates/go-swagger/cmd/operation_group.go.tmpl rename templates/go-swagger/{ => custom}/Dockerfile.tmpl (100%) rename templates/go-swagger/{ => custom}/Makefile.tmpl (100%) rename templates/go-swagger/{ => custom}/Readme.md.tmpl (100%) rename templates/go-swagger/{ => custom}/api.yaml.tmpl (100%) rename templates/go-swagger/{ => custom}/api/001-general.yaml.tmpl (100%) rename templates/go-swagger/{ => custom}/api/002-monitoring.yaml.tmpl (100%) rename templates/go-swagger/{ => custom}/launcher.sh.tmpl (100%) diff --git a/.golangci.yml b/.golangci.yml index a063672..8d76a56 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -155,7 +155,7 @@ linters-settings: # Select the Go version to target. The default is '1.13'. go: "1.18.5" - misspell: + misspell: {} # Correct spellings using locale preferences for US or UK. # Default is to use a neutral variety of English. # Setting locale to US will correct the British spelling of 'colour' to 'color'. diff --git a/go.mod b/go.mod index 77e14f0..94ae328 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.19 require ( github.com/blang/semver v3.5.1+incompatible + github.com/go-openapi/analysis v0.21.4 + github.com/go-openapi/loads v0.21.2 github.com/go-swagger/go-swagger v0.30.3 github.com/jessevdk/go-flags v1.5.0 github.com/juju/errors v1.0.0 @@ -19,12 +21,10 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/loads v0.21.2 // indirect github.com/go-openapi/runtime v0.24.2 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect diff --git a/helpers/api_types/go_swagger/constants.go b/helpers/api_types/go_swagger/constants.go index f8d82b7..b9a0af2 100644 --- a/helpers/api_types/go_swagger/constants.go +++ b/helpers/api_types/go_swagger/constants.go @@ -6,3 +6,10 @@ const templateDirectory = "templates/go-swagger" // mergeYamlFileName is the name of the file that will contain the final yaml spec of the API. // that file will always be deleted after generation. const mergeYamlFileName = "api-temp.yaml" + +// regeneratedFiles is a list of files / directory that are automatically regenerated by go-swagger. +// They will be automatically deleted by the generate api process. +var regeneratedFiles = []string{ + "restapi", + "models", +} diff --git a/helpers/api_types/go_swagger/generate_api.go b/helpers/api_types/go_swagger/generate_api.go index 137726b..34b7086 100644 --- a/helpers/api_types/go_swagger/generate_api.go +++ b/helpers/api_types/go_swagger/generate_api.go @@ -1,6 +1,9 @@ package go_swagger import ( + "os" + "path/filepath" + "github.com/juju/errors" "git.dev.m-and-m.ovh/mderasse/gouick/helpers" @@ -13,13 +16,34 @@ import ( func (a APIType) GenerateAPI(path string, config *models.Config) error { log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) + // delete file that need to be regenerated + err := a.deleteRegeneratedFiles(path) + if err != nil { + log.Error("Fail to delete Files that will be regenerated by Go Swagger") + return errors.Trace(err) + } + + err = a.createDirectories(path) + if err != nil { + log.Error("Fail to create project directories") + return errors.Trace(err) + } + // Launch Go Swagger - err := a.executeGoSwagger(path) + err = a.executeGoSwagger(path) if err != nil { log.Error("Fail to Execute Go Swagger") return errors.Trace(err) } + // No use atm + // Generate files from spec + // err = a.generateFromSpec(path) + // if err != nil { + // log.Error("Fail to generate files from spec") + // return errors.Trace(err) + // } + // Delete api yaml temporary file err = a.deleteTempApiYaml(path) if err != nil { @@ -29,3 +53,44 @@ func (a APIType) GenerateAPI(path string, config *models.Config) error { return nil } + +// deleteRegeneratedFiles will delete files that will be regenerated by go swagger. +func (a APIType) deleteRegeneratedFiles(path string) error { + log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) + + for _, file := range regeneratedFiles { + tempFilePath := filepath.Join(path, file) + + exist, err := helpers.FileExists(tempFilePath, true) + if err != nil { + return errors.Trace(err) + } + + if !exist { + log.Debug("File/Directory doesn't exist already") + return nil + } + + err = os.RemoveAll(tempFilePath) + if err != nil { + log.Errorf("fail to delete file/dir in %s", tempFilePath) + return errors.Trace(err) + } + } + + return nil +} + +// // generateFromSpec will read the swagger spec and create cmd server, handlers, test ... +// func (a APIType) generateFromSpec(path string) error { +// log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) + +// tempFilePath := filepath.Join(path, mergeYamlFileName) +// specData, err := spec.NewSpec(tempFilePath) +// if err != nil { +// return errors.Trace(err) +// } +// fmt.Printf("%#v", specData.GetOperationGroups()) + +// return nil +// } diff --git a/helpers/api_types/go_swagger/generate_dockerfile.go b/helpers/api_types/go_swagger/generate_dockerfile.go index 74ebf88..97a4fb2 100644 --- a/helpers/api_types/go_swagger/generate_dockerfile.go +++ b/helpers/api_types/go_swagger/generate_dockerfile.go @@ -15,7 +15,7 @@ import ( func (a APIType) GenerateDockerfile(path string, config *models.Config) error { log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) - templatePath := filepath.Join(templateDirectory, "Dockerfile.tmpl") + templatePath := filepath.Join(templateDirectory, "custom/Dockerfile.tmpl") savePath := filepath.Join(path, "Dockerfile") data, err := a.getStdTemplate(path, config) diff --git a/helpers/api_types/go_swagger/generate_launcher.go b/helpers/api_types/go_swagger/generate_launcher.go index 44ec6cd..c6a1d5a 100644 --- a/helpers/api_types/go_swagger/generate_launcher.go +++ b/helpers/api_types/go_swagger/generate_launcher.go @@ -15,7 +15,7 @@ import ( func (a APIType) GenerateLauncher(path string, config *models.Config) error { log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) - templatePath := filepath.Join(templateDirectory, "launcher.sh.tmpl") + templatePath := filepath.Join(templateDirectory, "custom/launcher.sh.tmpl") savePath := filepath.Join(path, "launcher.sh") data, err := a.getStdTemplate(path, config) diff --git a/helpers/api_types/go_swagger/generate_makefile.go b/helpers/api_types/go_swagger/generate_makefile.go index bc94ae3..e9cac32 100644 --- a/helpers/api_types/go_swagger/generate_makefile.go +++ b/helpers/api_types/go_swagger/generate_makefile.go @@ -16,7 +16,7 @@ import ( func (a APIType) GenerateMakefile(path string, config *models.Config) error { log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) - templatePath := filepath.Join(templateDirectory, "Makefile.tmpl") + templatePath := filepath.Join(templateDirectory, "custom/Makefile.tmpl") savePath := filepath.Join(path, "Makefile") data, err := a.getStdTemplate(path, config) diff --git a/helpers/api_types/go_swagger/generate_readme.go b/helpers/api_types/go_swagger/generate_readme.go index 378a596..2e15687 100644 --- a/helpers/api_types/go_swagger/generate_readme.go +++ b/helpers/api_types/go_swagger/generate_readme.go @@ -16,7 +16,7 @@ import ( func (a APIType) GenerateReadme(path string, config *models.Config) error { log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) - templatePath := filepath.Join(templateDirectory, "Readme.md.tmpl") + templatePath := filepath.Join(templateDirectory, "custom/Readme.md.tmpl") savePath := filepath.Join(path, "Readme.md") data, err := a.getStdTemplate(path, config) diff --git a/helpers/api_types/go_swagger/go_swagger.go b/helpers/api_types/go_swagger/go_swagger.go index 0bd67cd..86eba7d 100644 --- a/helpers/api_types/go_swagger/go_swagger.go +++ b/helpers/api_types/go_swagger/go_swagger.go @@ -72,6 +72,7 @@ func (a APIType) executeGoSwagger(path string) error { // goSwaggerMixin will call go-swagger code to merge api yaml files. func (a APIType) goSwaggerMixin(path string, primaryFile string, mixinFiles []string) ([]string, error) { + log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) mixinSpec := commands.MixinSpec{ Compact: false, @@ -86,16 +87,24 @@ func (a APIType) goSwaggerMixin(path string, primaryFile string, mixinFiles []st // goSwaggerGenerate will generate all app files as go-swagger binary will. func (a APIType) goSwaggerGenerate(path string) error { + log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) + + execDirectory, err := helpers.GetExecutableDirectory() + if err != nil { + log.Error("Fail to get Gouick directory") + return errors.Trace(err) + } + generateCmd := commands.Generate{ Server: &generate.Server{}, } generateCmd.Server.Shared.Spec = flags.Filename(filepath.Join(path, mergeYamlFileName)) - //generateCmd.Server.Shared.Target = flags.Filename(path) generateCmd.Server.ServerPackage = "restapi" generateCmd.Server.Models.ModelPackage = "models" generateCmd.Server.Operations.APIPackage = "operations" - generateCmd.Server.Shared.StrictResponders = true - generateCmd.Server.Shared.Template = "stratoscale" + generateCmd.Server.Shared.StrictResponders = false + generateCmd.Server.Shared.TemplateDir = flags.Filename(filepath.Join(execDirectory, "templates/go-swagger/app/templates/")) + generateCmd.Server.Shared.ConfigFile = flags.Filename(filepath.Join(execDirectory, "templates/go-swagger/app/config.yaml")) return generateCmd.Server.Execute(nil) } diff --git a/helpers/api_types/go_swagger/processes.go b/helpers/api_types/go_swagger/processes.go index 323d607..623683b 100644 --- a/helpers/api_types/go_swagger/processes.go +++ b/helpers/api_types/go_swagger/processes.go @@ -7,6 +7,7 @@ import ( "github.com/juju/errors" + "git.dev.m-and-m.ovh/mderasse/gouick/helpers" "git.dev.m-and-m.ovh/mderasse/gouick/models" log "github.com/sirupsen/logrus" @@ -14,6 +15,7 @@ import ( // getProcesses will read cmd directory and guess the app processes. func (a APIType) getProcesses(path string) ([]*models.Process, error) { + log.Debugf("Starting %s - %s", a.GetName(), helpers.GetCurrentFuncName()) cmdPath := filepath.Join(path, "cmd") cmdDirectories, err := filepath.Glob(filepath.Join(cmdPath, "*")) diff --git a/helpers/api_types/go_swagger/yaml.go b/helpers/api_types/go_swagger/yaml.go index df533e7..f2aabf8 100644 --- a/helpers/api_types/go_swagger/yaml.go +++ b/helpers/api_types/go_swagger/yaml.go @@ -25,15 +25,15 @@ func (a APIType) createDefaultAPIYamls(path string, config *models.Config) error templateFileList := []templateFileStruct{ { savePath: filepath.Join(path, "api.yaml"), - templatePath: filepath.Join(templateDirectory, "api.yaml.tmpl"), + templatePath: filepath.Join(templateDirectory, "custom/api.yaml.tmpl"), }, { savePath: filepath.Join(path, "api/001-general.yaml"), - templatePath: filepath.Join(templateDirectory, "api/001-general.yaml.tmpl"), + templatePath: filepath.Join(templateDirectory, "custom/api/001-general.yaml.tmpl"), }, { savePath: filepath.Join(path, "api/001-monitoring.yaml"), - templatePath: filepath.Join(templateDirectory, "api/002-monitoring.yaml.tmpl"), + templatePath: filepath.Join(templateDirectory, "custom/api/002-monitoring.yaml.tmpl"), }, } @@ -60,8 +60,7 @@ func (a APIType) deleteTempApiYaml(path string) error { tempFilePath := filepath.Join(path, mergeYamlFileName) - // delete - exist, err := helpers.FileExists(tempFilePath) + exist, err := helpers.FileExists(tempFilePath, false) if err != nil { return errors.Trace(err) } diff --git a/helpers/file.go b/helpers/file.go index 98c37e1..dbbf807 100644 --- a/helpers/file.go +++ b/helpers/file.go @@ -29,7 +29,7 @@ func IsGouickDirectory(path string) (bool, error) { // IsGouickProject will check if a gouick config file exist and readeable. func IsGouickProject(path string) (bool, error) { - exist, err := FileExists(filepath.Join(path, configFile)) + exist, err := FileExists(filepath.Join(path, configFile), false) if err != nil { return false, errors.Trace(err) } @@ -42,7 +42,7 @@ func IsGouickProject(path string) (bool, error) { } // FileExists will check if a file exist and is readeable. -func FileExists(path string) (bool, error) { +func FileExists(path string, allowDirectory bool) (bool, error) { fileInfo, err := os.Stat(path) if err != nil { if errors.Is(err, fs.ErrPermission) { @@ -53,7 +53,7 @@ func FileExists(path string) (bool, error) { return false, errors.Trace(err) } } - if fileInfo.IsDir() { + if fileInfo.IsDir() && !allowDirectory { return false, errors.NotValidf("%s is actually a directory", path) } diff --git a/helpers/golang.go b/helpers/golang.go index 9bb36fd..7068c17 100644 --- a/helpers/golang.go +++ b/helpers/golang.go @@ -10,7 +10,7 @@ import ( // IsGoProject check if we are in a golang project (have go.mod). func IsGoProject(path string) (bool, error) { - exist, err := FileExists(filepath.Join(path, "go.mod")) + exist, err := FileExists(filepath.Join(path, "go.mod"), false) if err != nil { return false, errors.Trace(err) } @@ -19,7 +19,7 @@ func IsGoProject(path string) (bool, error) { return true, nil } - exist, err = FileExists(filepath.Join(path, "go.sum")) + exist, err = FileExists(filepath.Join(path, "go.sum"), false) if err != nil { return false, errors.Trace(err) } diff --git a/templates/go-swagger/app/config.yaml b/templates/go-swagger/app/config.yaml new file mode 100644 index 0000000..289627a --- /dev/null +++ b/templates/go-swagger/app/config.yaml @@ -0,0 +1,60 @@ +layout: + application: + - name: configure + source: asset:serverConfigureapi + target: "{{ joinFilePath .Target .ServerPackage }}" + file_name: "configure_{{ .Name }}.go" + - name: main + source: asset:serverMain + target: "{{ joinFilePath .Target \"cmd\" (dasherize (pascalize .Name)) }}-server" + file_name: "main.go" + skip_exists: true + - name: embedded_spec + source: asset:swaggerJsonEmbed + target: "{{ joinFilePath .Target .ServerPackage }}" + file_name: "embedded_spec.go" + - name: server + source: asset:serverServer + target: "{{ joinFilePath .Target .ServerPackage }}" + file_name: "server.go" + - name: builder + source: asset:serverBuilder + target: "{{ joinFilePath .Target .ServerPackage .Package }}" + file_name: "{{ snakize (pascalize .Name) }}_api.go" + - name: doc + source: asset:serverDoc + target: "{{ joinFilePath .Target .ServerPackage }}" + file_name: "doc.go" + models: + - name: definition + source: asset:model + target: "{{ joinFilePath .Target .ModelPackage }}" + file_name: "{{ (snakize (pascalize .Name)) }}.go" + operations: + - name: parameters + source: asset:serverParameter + target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}" + file_name: "{{ (snakize (pascalize .Name)) }}_parameters.go" + - name: responses + source: asset:serverResponses + target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}" + file_name: "{{ (snakize (pascalize .Name)) }}_responses.go" + - name: handler + source: asset:serverOperation + target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}" + file_name: "{{ (snakize (pascalize .Name)) }}.go" + - name: apihandler + source: asset:apiHandler + target: "{{ joinFilePath .Target \"internal\" \"api\" .Package }}" + file_name: "{{ (snakize (pascalize .Name)) }}.go" + skip_exists: true + operation_groups: + - name: cmdtag + source: asset:serverTag + target: "{{ joinFilePath .Target \"cmd\" .MainPackage }}" + file_name: "tag_{{ (snakize (pascalize .Name)) }}.go" + skip_exists: true + - name: apitag + source: asset:apiTag + target: "{{ joinFilePath .Target \"internal\" \"api\" (snakize (pascalize .Name)) }}" + file_name: "{{ (snakize (pascalize .Name)) }}.go" \ No newline at end of file diff --git a/templates/go-swagger/app/templates/api/handler.gotmpl b/templates/go-swagger/app/templates/api/handler.gotmpl new file mode 100644 index 0000000..0d900ca --- /dev/null +++ b/templates/go-swagger/app/templates/api/handler.gotmpl @@ -0,0 +1,33 @@ +package {{ .Package }} + +import ( + "context" + + "github.com/go-openapi/runtime/middleware" + + "{{ index .DefaultImports "models"}}" + + api "{{ index .DefaultImports .Package }}" +) + +// {{ pascalize .Name }} will {{ .Summary }}. +// +// {{ .Method }} {{ .Path }} +func (m {{ pascalize .Package }}) {{ pascalize .Name }}(ctx context.Context, params api.{{ pascalize .Name }}Params) middleware.Responder { +{{ if eq .Name "getMonPing" }} + return api.NewGetMonPingOK().WithPayload( + &models.MonPing{ + Details: &models.MonPingDetails{ + AppVersion: "", + GitHash: "", + }, + Status: &models.MonPingStatus{ + Application: true, + Database: true, + }, + }, + ) +{{ else }} + return middleware.NotImplemented("Not Implemented") +{{ end }} +} diff --git a/templates/go-swagger/app/templates/api/tag.gotmpl b/templates/go-swagger/app/templates/api/tag.gotmpl new file mode 100644 index 0000000..02ed081 --- /dev/null +++ b/templates/go-swagger/app/templates/api/tag.gotmpl @@ -0,0 +1,6 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package {{ (snakize (pascalize .Name)) }} + +// {{ pascalize .Name }} implement the {{ pascalize .Name }}API interface from restapi. +type {{ pascalize .Name }} struct{} diff --git a/templates/go-swagger/app/templates/server/configureapi.gotmpl b/templates/go-swagger/app/templates/server/configureapi.gotmpl new file mode 100644 index 0000000..01a0702 --- /dev/null +++ b/templates/go-swagger/app/templates/server/configureapi.gotmpl @@ -0,0 +1,222 @@ +// Code generated by go-swagger; DO NOT EDIT. + + +{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }} + + +package {{ .APIPackage }} + +import ( + "context" + "crypto/tls" + "net/http" + "log" + "fmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/loads" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/runtime/security" + + {{ imports .DefaultImports }} + {{ imports .Imports }} +) +{{ $package := .Package }} + +type contextKey string + +const AuthKey contextKey = "Auth" + +{{ range .OperationGroups -}} +//go:generate mockery -name {{ pascalize .Name}}API -inpkg + +/* {{ pascalize .Name }}API {{ .Description }} */ +type {{ pascalize .Name }}API interface { +{{ range .Operations -}} + {{ if .Summary -}} + /* {{ pascalize .Name }} {{ .Summary }} */ + {{ else if .Description -}} + /* {{ pascalize .Name }} {{ .Description }} */ + {{ end -}} + {{ pascalize .Name }}(ctx context.Context, params {{.Package}}.{{ pascalize .Name }}Params) middleware.Responder + +{{ end -}} +} +{{ end }} + +// Config is configuration for Handler +type Config struct { + {{ range .OperationGroups -}} + {{ pascalize .Name }}API + {{ end -}} + Logger func(string, ...interface{}) + // InnerMiddleware is for the handler executors. These do not apply to the swagger.json document. + // The middleware executes after routing but before authentication, binding and validation + InnerMiddleware func(http.Handler) http.Handler + + // Authorizer is used to authorize a request after the Auth function was called using the "Auth*" functions + // and the principal was stored in the context in the "AuthKey" context value. + Authorizer func(*http.Request) error + + {{ range .SecurityDefinitions -}} + {{ if .IsBasicAuth -}} + // Auth{{ pascalize .ID }} for basic authentication + Auth{{ pascalize .ID }} func(user string, pass string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error) + {{ end -}} + {{ if .IsAPIKeyAuth -}} + // Auth{{ pascalize .ID }} Applies when the "{{ .Name }}" {{ .Source }} is set + Auth{{ pascalize .ID }} func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error) + {{ end }} + {{ if .IsOAuth2 -}} + // Auth{{ pascalize .ID }} For OAuth2 authentication + Auth{{ pascalize .ID }} func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}, error) + {{ end -}} + {{ end -}} + + // Authenticator to use for all APIKey authentication + APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator + // Authenticator to use for all Bearer authentication + BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator + // Authenticator to use for all Basic authentication + BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator + + {{ range .Consumes -}} + {{ if .Implementation -}} + // {{ pascalize .Name }}Consumer is a {{ .Name }} consumer that will replace the default if not nil. + {{ pascalize .Name }}Consumer runtime.Consumer + {{ end -}} + {{ end -}} +} + +// Handler returns an http.Handler given the handler configuration +// It mounts all the business logic implementers in the right routing. +func Handler(c Config) (http.Handler, error) { + h, _, err := HandlerAPI(c) + return h, err +} + +// HandlerAPI returns an http.Handler given the handler configuration +// and the corresponding *{{ pascalize .Name }} instance. +// It mounts all the business logic implementers in the right routing. +func HandlerAPI(c Config) (http.Handler, *{{.Package}}.{{ pascalize .Name }}API, error) { + spec, err := loads.Analyzed(swaggerCopy(SwaggerJSON), "") + if err != nil { + return nil, nil, fmt.Errorf("analyze swagger: %v", err) + } + api := {{.Package}}.New{{ pascalize .Name }}API(spec) + api.ServeError = errors.ServeError + api.Logger = c.Logger + + if c.APIKeyAuthenticator != nil { + api.APIKeyAuthenticator = c.APIKeyAuthenticator + } + if c.BasicAuthenticator != nil { + api.BasicAuthenticator = c.BasicAuthenticator + } + if c.BearerAuthenticator != nil { + api.BearerAuthenticator = c.BearerAuthenticator + } + + {{ range .Consumes -}} + if c.{{ pascalize .Name }}Consumer != nil { + api.{{ pascalize .Name }}Consumer = c.{{ pascalize .Name }}Consumer + } else { + {{ if .Implementation -}} + api.{{ pascalize .Name }}Consumer = {{ .Implementation }} + {{ else }} + api.{{ pascalize .Name }}Consumer = runtime.ConsumerFunc(func(r io.Reader, target interface{}) error { + return errors.NotImplemented("{{.Name}} consumer has not yet been implemented") + }) + {{ end -}} + } + {{ end -}} + {{ range .Produces -}} + {{ if .Implementation -}} + api.{{ pascalize .Name }}Producer = {{ .Implementation }} + {{ else -}} + api.{{ pascalize .Name }}Producer = runtime.ProducerFunc(func(w io.Writer, data interface{}) error { + return errors.NotImplemented("{{.Name}} producer has not yet been implemented") + }) + {{ end -}} + {{ end -}} + + {{ range .SecurityDefinitions -}} + {{ if .IsBasicAuth -}} + api.{{ pascalize .ID }}Auth = func(user string, pass string) ({{if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) { + if c.Auth{{ pascalize .ID }} == nil { + {{- if eq .Principal "interface{}" }} + return "", nil + {{- else }} + panic("you specified a custom principal type, but did not provide the authenticator to provide this") + {{- end }} + } + return c.Auth{{ pascalize .ID }}(user, pass) + } + {{ end -}} + {{ if .IsAPIKeyAuth -}} + api.{{ pascalize .ID }}Auth = func(token string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) { + if c.Auth{{ pascalize .ID }} == nil { + {{- if eq .Principal "interface{}" }} + return token, nil + {{- else }} + panic("you specified a custom principal type, but did not provide the authenticator to provide this") + {{- end }} + } + return c.Auth{{ pascalize .ID }}(token) + } + {{ end }} + {{ if .IsOAuth2 -}} + api.{{ pascalize .ID }}Auth = func(token string, scopes []string) ({{ if .PrincipalIsNullable }}*{{ end }}{{.Principal}}, error) { + if c.Auth{{ pascalize .ID }} == nil { + {{- if eq .Principal "interface{}" }} + return token, nil + {{- else }} + panic("you specified a custom principal type, but did not provide the authenticator to provide this") + {{- end }} + } + return c.Auth{{ pascalize .ID }}(token, scopes) + } + {{ end -}} + {{ end -}} + + {{ if .SecurityDefinitions -}} + api.APIAuthorizer = authorizer(c.Authorizer) + {{ end -}} + + {{ range .Operations -}} + api.{{if ne .Package $package}}{{pascalize .Package}}{{end}}{{ pascalize .Name }}Handler = + {{- .PackageAlias }}.{{ pascalize .Name }}HandlerFunc(func(params {{.PackageAlias}}.{{ pascalize .Name }}Params{{if .Authorized}}, principal {{ if .PrincipalIsNullable }}*{{ end }}{{ .Principal }}{{end}}) middleware.Responder { + ctx := params.HTTPRequest.Context() + {{ if .Authorized -}} + ctx = storeAuth(ctx, principal) + {{ end -}} + return c.{{pascalize .Package}}API.{{pascalize .Name}}(ctx, params) + }) + {{ end -}} + + api.ServerShutdown = func() { } + return api.Serve(c.InnerMiddleware), api, nil +} + +// swaggerCopy copies the swagger json to prevent data races in runtime +func swaggerCopy(orig json.RawMessage) json.RawMessage { + c := make(json.RawMessage, len(orig)) + copy(c, orig) + return c +} + +// authorizer is a helper function to implement the runtime.Authorizer interface. +type authorizer func(*http.Request) error + +func (a authorizer) Authorize(req *http.Request, principal interface{}) error { + if a == nil { + return nil + } + ctx := storeAuth(req.Context(), principal) + return a(req.WithContext(ctx)) +} + +func storeAuth(ctx context.Context, principal interface{}) context.Context { + return context.WithValue(ctx, AuthKey, principal) +} \ No newline at end of file diff --git a/templates/go-swagger/app/templates/server/main.gotmpl b/templates/go-swagger/app/templates/server/main.gotmpl new file mode 100644 index 0000000..13bcbd7 --- /dev/null +++ b/templates/go-swagger/app/templates/server/main.gotmpl @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/sirupsen/logrus" + + "git.dev.m-and-m.ovh/mderasse/gocommon/log" + "git.dev.m-and-m.ovh/mderasse/gocommon/tracing" + + "{{ index .DefaultImports "restapi"}}" +) + +// initTag is the signature of the function that tag initialization should respect. +type initTag func(*restapi.Config) error + +// tagswill be fulfill by init function in tag_XXX.go. +var tags = map[string]initTag{} + +func main() { + + ctx := context.Background() + + // Initialize logs + logger, err := log.Init() + if err != nil { + logrus.WithError(err).Fatal("fail to initialize gocommon.log") + } + + // Initialize tracing + logger.Info("Initializing tracing") + tp, err := tracing.Init() + if err != nil { + logger.WithError(err).Fatal("fail to initialize gocommon.tracing") + } + + // Configure go-swagger api + logger.Info("Configure go-swagger API") + conf := restapi.Config{ + Logger: logger.Printf, + } + + // Initialize Tags by calling initTag function that have been stored in tags variable (with init() in tag_XXX.go) + logger.Info("Initializing Tags") + for tag, initTagFunction := range tags { + if err := initTagFunction(&conf); err != nil { + logger.WithError(err).Fatalf("Failed to initialize tag %q", tag) + } + } + + // Initialize API Handlers + logger.Info("Initializing API handlers") + h, _, err := restapi.HandlerAPI(conf) + if err != nil { + logger.WithError(err).Fatal("Failed to initialize API Handlers") + } + + srv := http.Server{ + Addr: ":8081", + Handler: h, + IdleTimeout: 30 * time.Second, + ReadHeaderTimeout: 2 * time.Second, + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + logger.WithError(err).Fatalf("HTTP server error: %v", err) + } + logger.Info("Stopped serving new connections.") + }() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second) + defer shutdownRelease() + + if err := srv.Shutdown(shutdownCtx); err != nil { + logger.WithError(err).Fatalf("HTTP shutdown error: %v", err) + } + tp.Shutdown(ctx) + logger.Info("Graceful shutdown complete.") +} diff --git a/templates/go-swagger/app/templates/server/server.gotmpl b/templates/go-swagger/app/templates/server/server.gotmpl new file mode 100644 index 0000000..7c5d7fb --- /dev/null +++ b/templates/go-swagger/app/templates/server/server.gotmpl @@ -0,0 +1,9 @@ +// Code generated by go-swagger; DO NOT EDIT. + + +{{ if .Copyright -}}// {{ comment .Copyright -}}{{ end }} + + +package {{ .APIPackage }} + +// this file is intentionally empty. Otherwise go-swagger will generate a server which we don't want \ No newline at end of file diff --git a/templates/go-swagger/app/templates/server/tag.gotmpl b/templates/go-swagger/app/templates/server/tag.gotmpl new file mode 100644 index 0000000..becbebc --- /dev/null +++ b/templates/go-swagger/app/templates/server/tag.gotmpl @@ -0,0 +1,13 @@ +package main + +import ( + "{{ .GenCommon.TargetImportPath }}/internal/api/{{ (dasherize (pascalize .Name)) }}" + "{{ .GenCommon.TargetImportPath }}/restapi" +) + +func init() { + tags["{{ pascalize .Name }}"] = func(c *restapi.Config) error { + c.{{ pascalize .Name }}API = {{ (snakize (pascalize .Name)) }}.{{ pascalize .Name }}{} + return nil + } +} \ No newline at end of file diff --git a/templates/go-swagger/cmd/main.go.tmpl b/templates/go-swagger/cmd/main.go.tmpl new file mode 100644 index 0000000..e69de29 diff --git a/templates/go-swagger/cmd/operation_group.go.tmpl b/templates/go-swagger/cmd/operation_group.go.tmpl new file mode 100644 index 0000000..e69de29 diff --git a/templates/go-swagger/Dockerfile.tmpl b/templates/go-swagger/custom/Dockerfile.tmpl similarity index 100% rename from templates/go-swagger/Dockerfile.tmpl rename to templates/go-swagger/custom/Dockerfile.tmpl diff --git a/templates/go-swagger/Makefile.tmpl b/templates/go-swagger/custom/Makefile.tmpl similarity index 100% rename from templates/go-swagger/Makefile.tmpl rename to templates/go-swagger/custom/Makefile.tmpl diff --git a/templates/go-swagger/Readme.md.tmpl b/templates/go-swagger/custom/Readme.md.tmpl similarity index 100% rename from templates/go-swagger/Readme.md.tmpl rename to templates/go-swagger/custom/Readme.md.tmpl diff --git a/templates/go-swagger/api.yaml.tmpl b/templates/go-swagger/custom/api.yaml.tmpl similarity index 100% rename from templates/go-swagger/api.yaml.tmpl rename to templates/go-swagger/custom/api.yaml.tmpl diff --git a/templates/go-swagger/api/001-general.yaml.tmpl b/templates/go-swagger/custom/api/001-general.yaml.tmpl similarity index 100% rename from templates/go-swagger/api/001-general.yaml.tmpl rename to templates/go-swagger/custom/api/001-general.yaml.tmpl diff --git a/templates/go-swagger/api/002-monitoring.yaml.tmpl b/templates/go-swagger/custom/api/002-monitoring.yaml.tmpl similarity index 100% rename from templates/go-swagger/api/002-monitoring.yaml.tmpl rename to templates/go-swagger/custom/api/002-monitoring.yaml.tmpl diff --git a/templates/go-swagger/launcher.sh.tmpl b/templates/go-swagger/custom/launcher.sh.tmpl similarity index 100% rename from templates/go-swagger/launcher.sh.tmpl rename to templates/go-swagger/custom/launcher.sh.tmpl