mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Change OpenAPI code generator to extract request objects (#14217)
This commit is contained in:
committed by
GitHub
parent
ef8ce03e70
commit
dcb5942bd1
3
changelog/14217.txt
Normal file
3
changelog/14217.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
sdk: Change OpenAPI code generator to extract request objects into /components/schemas and reference them by name.
|
||||||
|
```
|
||||||
@@ -198,7 +198,7 @@ func (b *Backend) HandleRequest(ctx context.Context, req *logical.Request) (*log
|
|||||||
|
|
||||||
// If the path is empty and it is a help operation, handle that.
|
// If the path is empty and it is a help operation, handle that.
|
||||||
if req.Path == "" && req.Operation == logical.HelpOperation {
|
if req.Path == "" && req.Operation == logical.HelpOperation {
|
||||||
return b.handleRootHelp()
|
return b.handleRootHelp(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the matching route
|
// Find the matching route
|
||||||
@@ -457,7 +457,7 @@ func (b *Backend) route(path string) (*Path, map[string]string) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Backend) handleRootHelp() (*logical.Response, error) {
|
func (b *Backend) handleRootHelp(req *logical.Request) (*logical.Response, error) {
|
||||||
// Build a mapping of the paths and get the paths alphabetized to
|
// Build a mapping of the paths and get the paths alphabetized to
|
||||||
// make the output prettier.
|
// make the output prettier.
|
||||||
pathsMap := make(map[string]*Path)
|
pathsMap := make(map[string]*Path)
|
||||||
@@ -486,9 +486,18 @@ func (b *Backend) handleRootHelp() (*logical.Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugins currently don't have a direct knowledge of their own "type"
|
||||||
|
// (e.g. "kv", "cubbyhole"). It defaults to the name of the executable but
|
||||||
|
// can be overridden when the plugin is mounted. Since we need this type to
|
||||||
|
// form the request & response full names, we are passing it as an optional
|
||||||
|
// request parameter to the plugin's root help endpoint. If specified in
|
||||||
|
// the request, the type will be used as part of the request/response body
|
||||||
|
// names in the OAS document.
|
||||||
|
requestResponsePrefix := req.GetString("requestResponsePrefix")
|
||||||
|
|
||||||
// Build OpenAPI response for the entire backend
|
// Build OpenAPI response for the entire backend
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPaths(b, doc); err != nil {
|
if err := documentPaths(b, requestResponsePrefix, doc); err != nil {
|
||||||
b.Logger().Warn("error generating OpenAPI", "error", err)
|
b.Logger().Warn("error generating OpenAPI", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ func NewOASDocument() *OASDocument {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Paths: make(map[string]*OASPathItem),
|
Paths: make(map[string]*OASPathItem),
|
||||||
|
Components: OASComponents{
|
||||||
|
Schemas: make(map[string]*OASSchema),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +81,14 @@ func NewOASDocumentFromMap(input map[string]interface{}) (*OASDocument, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OASDocument struct {
|
type OASDocument struct {
|
||||||
Version string `json:"openapi" mapstructure:"openapi"`
|
Version string `json:"openapi" mapstructure:"openapi"`
|
||||||
Info OASInfo `json:"info"`
|
Info OASInfo `json:"info"`
|
||||||
Paths map[string]*OASPathItem `json:"paths"`
|
Paths map[string]*OASPathItem `json:"paths"`
|
||||||
|
Components OASComponents `json:"components"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OASComponents struct {
|
||||||
|
Schemas map[string]*OASSchema `json:"schemas"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OASInfo struct {
|
type OASInfo struct {
|
||||||
@@ -148,6 +156,7 @@ type OASMediaTypeObject struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OASSchema struct {
|
type OASSchema struct {
|
||||||
|
Ref string `json:"$ref,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Properties map[string]*OASSchema `json:"properties,omitempty"`
|
Properties map[string]*OASSchema `json:"properties,omitempty"`
|
||||||
@@ -204,9 +213,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// documentPaths parses all paths in a framework.Backend into OpenAPI paths.
|
// documentPaths parses all paths in a framework.Backend into OpenAPI paths.
|
||||||
func documentPaths(backend *Backend, doc *OASDocument) error {
|
func documentPaths(backend *Backend, requestResponsePrefix string, doc *OASDocument) error {
|
||||||
for _, p := range backend.Paths {
|
for _, p := range backend.Paths {
|
||||||
if err := documentPath(p, backend.SpecialPaths(), backend.BackendType, doc); err != nil {
|
if err := documentPath(p, backend.SpecialPaths(), requestResponsePrefix, backend.BackendType, doc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +224,7 @@ func documentPaths(backend *Backend, doc *OASDocument) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// documentPath parses a framework.Path into one or more OpenAPI paths.
|
// documentPath parses a framework.Path into one or more OpenAPI paths.
|
||||||
func documentPath(p *Path, specialPaths *logical.Paths, backendType logical.BackendType, doc *OASDocument) error {
|
func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix string, backendType logical.BackendType, doc *OASDocument) error {
|
||||||
var sudoPaths []string
|
var sudoPaths []string
|
||||||
var unauthPaths []string
|
var unauthPaths []string
|
||||||
|
|
||||||
@@ -224,7 +233,7 @@ func documentPath(p *Path, specialPaths *logical.Paths, backendType logical.Back
|
|||||||
unauthPaths = specialPaths.Unauthenticated
|
unauthPaths = specialPaths.Unauthenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert optional parameters into distinct patterns to be process independently.
|
// Convert optional parameters into distinct patterns to be processed independently.
|
||||||
paths := expandPattern(p.Pattern)
|
paths := expandPattern(p.Pattern)
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
@@ -358,10 +367,12 @@ func documentPath(p *Path, specialPaths *logical.Paths, backendType logical.Back
|
|||||||
|
|
||||||
// Set the final request body. Only JSON request data is supported.
|
// Set the final request body. Only JSON request data is supported.
|
||||||
if len(s.Properties) > 0 || s.Example != nil {
|
if len(s.Properties) > 0 || s.Example != nil {
|
||||||
|
requestName := constructRequestName(requestResponsePrefix, path)
|
||||||
|
doc.Components.Schemas[requestName] = s
|
||||||
op.RequestBody = &OASRequestBody{
|
op.RequestBody = &OASRequestBody{
|
||||||
Content: OASContent{
|
Content: OASContent{
|
||||||
"application/json": &OASMediaTypeObject{
|
"application/json": &OASMediaTypeObject{
|
||||||
Schema: s,
|
Schema: &OASSchema{Ref: fmt.Sprintf("#/components/schemas/%s", requestName)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -459,6 +470,30 @@ func documentPath(p *Path, specialPaths *logical.Paths, backendType logical.Back
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// constructRequestName joins the given prefix with the path elements into a
|
||||||
|
// CamelCaseRequest string.
|
||||||
|
//
|
||||||
|
// For example, prefix="kv" & path=/config/lease/{name} => KvConfigLeaseRequest
|
||||||
|
func constructRequestName(requestResponsePrefix string, path string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(strings.Title(requestResponsePrefix))
|
||||||
|
|
||||||
|
// split the path by / _ - separators
|
||||||
|
for _, token := range strings.FieldsFunc(path, func(r rune) bool {
|
||||||
|
return r == '/' || r == '_' || r == '-'
|
||||||
|
}) {
|
||||||
|
// exclude request fields
|
||||||
|
if !strings.ContainsAny(token, "{}") {
|
||||||
|
b.WriteString(strings.Title(token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("Request")
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func specialPathMatch(path string, specialPaths []string) bool {
|
func specialPathMatch(path string, specialPaths []string) bool {
|
||||||
// Test for exact or prefix match of special paths.
|
// Test for exact or prefix match of special paths.
|
||||||
for _, sp := range specialPaths {
|
for _, sp := range specialPaths {
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ func TestOpenAPI_SpecialPaths(t *testing.T) {
|
|||||||
Root: test.rootPaths,
|
Root: test.rootPaths,
|
||||||
Unauthenticated: test.unauthPaths,
|
Unauthenticated: test.unauthPaths,
|
||||||
}
|
}
|
||||||
err := documentPath(&path, sp, logical.TypeLogical, doc)
|
err := documentPath(&path, sp, "kv", logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -515,11 +515,11 @@ func TestOpenAPI_OperationID(t *testing.T) {
|
|||||||
|
|
||||||
for _, context := range []string{"", "bar"} {
|
for _, context := range []string{"", "bar"} {
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
err := documentPath(path1, nil, logical.TypeLogical, doc)
|
err := documentPath(path1, nil, "kv", logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = documentPath(path2, nil, logical.TypeLogical, doc)
|
err = documentPath(path2, nil, "kv", logical.TypeLogical, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -579,7 +579,7 @@ func TestOpenAPI_CustomDecoder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
docOrig := NewOASDocument()
|
docOrig := NewOASDocument()
|
||||||
err := documentPath(p, nil, logical.TypeLogical, docOrig)
|
err := documentPath(p, nil, "kv", logical.TypeLogical, docOrig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -642,7 +642,7 @@ func testPath(t *testing.T, path *Path, sp *logical.Paths, expectedJSON string)
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPath(path, sp, logical.TypeLogical, doc); err != nil {
|
if err := documentPath(path, sp, "kv", logical.TypeLogical, doc); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
doc.CreateOperationIDs("")
|
doc.CreateOperationIDs("")
|
||||||
|
|||||||
@@ -301,9 +301,17 @@ func (p *Path) helpCallback(b *Backend) OperationFunc {
|
|||||||
return nil, errwrap.Wrapf("error executing template: {{err}}", err)
|
return nil, errwrap.Wrapf("error executing template: {{err}}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The plugin type (e.g. "kv", "cubbyhole") is only assigned at the time
|
||||||
|
// the plugin is enabled (mounted). If specified in the request, the type
|
||||||
|
// will be used as part of the request/response names in the OAS document
|
||||||
|
var requestResponsePrefix string
|
||||||
|
if v, ok := req.Data["requestResponsePrefix"]; ok {
|
||||||
|
requestResponsePrefix = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
// Build OpenAPI response for this path
|
// Build OpenAPI response for this path
|
||||||
doc := NewOASDocument()
|
doc := NewOASDocument()
|
||||||
if err := documentPath(p, b.SpecialPaths(), b.BackendType, doc); err != nil {
|
if err := documentPath(p, b.SpecialPaths(), requestResponsePrefix, b.BackendType, doc); err != nil {
|
||||||
b.Logger().Warn("error generating OpenAPI", "error", err)
|
b.Logger().Warn("error generating OpenAPI", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
sdk/framework/testdata/legacy.json
vendored
21
sdk/framework/testdata/legacy.json
vendored
@@ -41,13 +41,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/KvLookupRequest"
|
||||||
"properties": {
|
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "My token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +53,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"KvLookupRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "My token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
sdk/framework/testdata/operations.json
vendored
73
sdk/framework/testdata/operations.json
vendored
@@ -66,39 +66,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/components/schemas/KvFooRequest"
|
||||||
"required": ["age"],
|
|
||||||
"properties": {
|
|
||||||
"flavors": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "the flavors",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "the age",
|
|
||||||
"enum": [1, 2, 3],
|
|
||||||
"x-vault-displayAttrs": {
|
|
||||||
"name": "Age",
|
|
||||||
"sensitive": true,
|
|
||||||
"group": "Some Group",
|
|
||||||
"value": 7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "the name",
|
|
||||||
"default": "Larry",
|
|
||||||
"pattern": "\\w([\\w-.]*\\w)?"
|
|
||||||
},
|
|
||||||
"x-abc-token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "a header value",
|
|
||||||
"enum": ["a", "b", "c"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,5 +78,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"KvFooRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["age"],
|
||||||
|
"properties": {
|
||||||
|
"flavors": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "the flavors",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the age",
|
||||||
|
"enum": [1, 2, 3],
|
||||||
|
"x-vault-displayAttrs": {
|
||||||
|
"name": "Age",
|
||||||
|
"sensitive": true,
|
||||||
|
"group": "Some Group",
|
||||||
|
"value": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "the name",
|
||||||
|
"default": "Larry",
|
||||||
|
"pattern": "\\w([\\w-.]*\\w)?"
|
||||||
|
},
|
||||||
|
"x-abc-token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "a header value",
|
||||||
|
"enum": ["a", "b", "c"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
sdk/framework/testdata/operations_list.json
vendored
4
sdk/framework/testdata/operations_list.json
vendored
@@ -59,5 +59,9 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
sdk/framework/testdata/responses.json
vendored
4
sdk/framework/testdata/responses.json
vendored
@@ -46,6 +46,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4027,7 +4027,13 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
|||||||
doc := framework.NewOASDocument()
|
doc := framework.NewOASDocument()
|
||||||
|
|
||||||
procMountGroup := func(group, mountPrefix string) error {
|
procMountGroup := func(group, mountPrefix string) error {
|
||||||
for mount := range resp.Data[group].(map[string]interface{}) {
|
for mount, entry := range resp.Data[group].(map[string]interface{}) {
|
||||||
|
|
||||||
|
var pluginType string
|
||||||
|
if t, ok := entry.(map[string]interface{})["type"]; ok {
|
||||||
|
pluginType = t.(string)
|
||||||
|
}
|
||||||
|
|
||||||
backend := b.Core.router.MatchingBackend(ctx, mountPrefix+mount)
|
backend := b.Core.router.MatchingBackend(ctx, mountPrefix+mount)
|
||||||
|
|
||||||
if backend == nil {
|
if backend == nil {
|
||||||
@@ -4037,6 +4043,7 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
|||||||
req := &logical.Request{
|
req := &logical.Request{
|
||||||
Operation: logical.HelpOperation,
|
Operation: logical.HelpOperation,
|
||||||
Storage: req.Storage,
|
Storage: req.Storage,
|
||||||
|
Data: map[string]interface{}{"requestResponsePrefix": pluginType},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := backend.HandleRequest(ctx, req)
|
resp, err := backend.HandleRequest(ctx, req)
|
||||||
@@ -4092,6 +4099,11 @@ func (b *SystemBackend) pathInternalOpenAPI(ctx context.Context, req *logical.Re
|
|||||||
|
|
||||||
doc.Paths["/"+mountPrefix+mount+path] = obj
|
doc.Paths["/"+mountPrefix+mount+path] = obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge backend schema components
|
||||||
|
for e, schema := range backendDoc.Components.Schemas {
|
||||||
|
doc.Components.Schemas[e] = schema
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3404,6 +3404,9 @@ func TestSystemBackend_OpenAPI(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"paths": map[string]interface{}{},
|
"paths": map[string]interface{}{},
|
||||||
|
"components": map[string]interface{}{
|
||||||
|
"schemas": map[string]interface{}{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := deep.Equal(oapi, exp); diff != nil {
|
if diff := deep.Equal(oapi, exp); diff != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user