Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(openapi): remove spec file generation from live code path #1290

Merged
merged 2 commits into from
Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ openapi-lint: docs/api/openapi3.json
openapi lint $<

docs/api/openapi3.json:
go run . openapi
go run ./internal/openapigen $@
2 changes: 0 additions & 2 deletions docs/api/openapi3.json
Original file line number Diff line number Diff line change
Expand Up @@ -3321,11 +3321,9 @@
"operationId": "ListUsers",
"parameters": [
{
"example": "email@example.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is weird. should not have changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment below :)

It happens on main as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this and you are correct, it was removed in main and not regenerated

"in": "query",
"name": "email",
"schema": {
"example": "email@example.com",
Comment on lines -3324 to -3328
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was surprised to see these changes, but running the test on main showed the same changes.

I guess that the API definitions changed in another PR that was merged at the same time as the openapi spec generation changes.

"type": "string"
}
},
Expand Down
17 changes: 0 additions & 17 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,22 +358,6 @@ func canonicalPath(in string) (string, error) {
return abs, nil
}

func newOpenAPICmd() *cobra.Command {
cmd := &cobra.Command{
Use: "openapi",
Short: "generate the openapi spec",
Hidden: true,

RunE: func(cmd *cobra.Command, args []string) error {
s := &server.Server{}
s.GenerateRoutes()
return nil
},
}

return cmd
}

func newServerCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Expand Down Expand Up @@ -607,7 +591,6 @@ func NewRootCmd() (*cobra.Command, error) {
rootCmd.AddCommand(newTokensCmd())
rootCmd.AddCommand(newInfoCmd())
rootCmd.AddCommand(newServerCmd())
rootCmd.AddCommand(newOpenAPICmd())
rootCmd.AddCommand(newConnectorCmd())
rootCmd.AddCommand(newVersionCmd())

Expand Down
27 changes: 27 additions & 0 deletions internal/openapigen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"fmt"
"os"

"github.com/infrahq/infra/internal/server"
)

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}

func run(args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing command line argument: path to openapi spec file")
}
filename := args[0]

s := server.Server{}
s.GenerateRoutes()

return server.WriteOpenAPISpecToFile(filename)
}
100 changes: 31 additions & 69 deletions internal/server/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,16 @@ func getFuncName(i interface{}) string {
nameParts := strings.Split(name, ".")
name = nameParts[len(nameParts)-1]
name = strings.TrimSuffix(name, "-fm")

return name
}

func orderedTagNames() []string {
tagNames := []string{}
tagNames := make([]string, 0, len(funcPartialNameToTagNames))
for k := range funcPartialNameToTagNames {
tagNames = append(tagNames, k)
}

sort.Strings(tagNames)

return tagNames
}

Expand Down Expand Up @@ -218,98 +216,62 @@ func buildProperty(f reflect.StructField, t, parent reflect.Type, parentSchema *
}
}

func isDev() bool {
dirs := []string{".git", "docs"}
for _, dir := range dirs {
info, err := os.Stat(dir)
if err != nil {
return false
}

if !info.IsDir() {
return false
}
}

return true
}

func generateOpenAPI() {
if !isDev() {
return
}

func writeOpenAPISpec(version string, out io.Writer) error {
openAPISchema.OpenAPI = "3.0.0"
openAPISchema.Info = &openapi3.Info{
Title: "Infra API",
Version: time.Now().Format("2006-01-02"),
Version: version,
Description: "Infra API",
License: &openapi3.License{Name: "Apache 2.0", URL: "https://www.apache.org/licenses/LICENSE-2.0.html"},
}
openAPISchema.Servers = append(openAPISchema.Servers, &openapi3.Server{
URL: "https://api.infrahq.com",
})
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")

if changedSinceLastRun() {
f2, err := os.Create("docs/api/openapi3.json")
if err != nil {
panic(err)
}
defer f2.Close()
enc2 := json.NewEncoder(f2)
enc2.SetIndent("", " ")

if err := enc2.Encode(openAPISchema); err != nil {
panic(err)
}
if err := encoder.Encode(openAPISchema); err != nil {
return fmt.Errorf("failed to write schema: %w", err)
}
return nil
}

func changedSinceLastRun() bool {
origVersion := openAPISchema.Info.Version
defer func() {
openAPISchema.Info.Version = origVersion
}()

// read last file
f, err := os.Open("docs/api/openapi3.json")
func WriteOpenAPISpecToFile(filename string) error {
old, err := readOpenAPISpec(filename)
if err != nil {
panic("expected docs/api/openapi3.json to exist but didn't find it: " + err.Error())
return err
}
defer f.Close()

oldOpenAPISchema := openapi3.T{}
version := time.Now().Format("2006-01-02")
old.Info.Version = version

err = json.NewDecoder(f).Decode(&oldOpenAPISchema)
if err != nil {
panic("couldn't parse last openapi schema: " + err.Error())
var buf bytes.Buffer
if err := writeOpenAPISpec(version, &buf); err != nil {
return err
}

oldDate := oldOpenAPISchema.Info.Version

openAPISchema.Info.Version = oldDate

_, err = f.Seek(0, 0)
if err != nil {
panic(err)
if reflect.DeepEqual(openAPISchema, old) {
// no changes to the schema
return nil
}

bufLast := &bytes.Buffer{}
// nolint: gosec // 0644 is the right mode
return os.WriteFile(filename, buf.Bytes(), 0o644)
}

func readOpenAPISpec(filename string) (openapi3.T, error) {
spec := openapi3.T{}

_, err = io.Copy(bufLast, f)
fh, err := os.Open(filename)
if err != nil {
panic("couldn't read file contents: " + err.Error())
return spec, fmt.Errorf("failed to create file: %w", err)
}
defer fh.Close()

bufTmp := &bytes.Buffer{}
enc2 := json.NewEncoder(bufTmp)
enc2.SetIndent("", " ")

if err := enc2.Encode(openAPISchema); err != nil {
panic(err)
if err := json.NewDecoder(fh).Decode(&spec); err != nil {
return spec, fmt.Errorf("failed to parse last openapi schema from %s: %w", filename, err)
}

return !bytes.Equal(bufLast.Bytes(), bufTmp.Bytes())
return spec, nil
}

func setTagInfo(f reflect.StructField, t, parent reflect.Type, schema, parentSchema *openapi3.Schema) {
Expand Down
25 changes: 10 additions & 15 deletions internal/server/openapi_test.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
package server

import (
"os"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestOpenAPIGen(t *testing.T) {
// must run from infra root dir
wd, err := os.Getwd()
require.NoError(t, err)

parts := strings.Split(wd, string(os.PathSeparator))

if parts[len(parts)-1] != "infra" {
err := os.Chdir("../..")
Comment on lines -13 to -19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go test always runs tests with the package directory as the working directory. So we can use a specific path instead of a conditional.

require.NoError(t, err)
}

s := &Server{}
// TestWriteOpenAPISpec is not really a test. It's a way of ensuring the openapi
// spec is updated.
// TODO: replace this with a test that uses golden, and a CI check to make sure the
// file in git matches the source code.
func TestWriteOpenAPISpec(t *testing.T) {
s := Server{}
s.GenerateRoutes()

filename := "../../docs/api/openapi3.json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can't assume the test will always run from this directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment, you can! (although I can't find a good docs link right now, I'll keep looking)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm mistaken. I tested different run methods and couldn't find a problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure where it is documented, but I've always seen it work this way. Somewhere in go test it checked to the directory for the package before running the test binary.

err := WriteOpenAPISpecToFile(filename)
require.NoError(t, err)
}
2 changes: 0 additions & 2 deletions internal/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ func (a *API) registerRoutes(router *gin.RouterGroup) {

get(unauthorized, "/version", a.Version)
}

generateOpenAPI()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the main goal of this PR, to remove this from the production code path.

}

func get[Req, Res any](r *gin.RouterGroup, path string, handler ReqResHandlerFunc[Req, Res]) {
Expand Down