mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-11-30 10:10:21 +00:00
551 lines
17 KiB
Go
551 lines
17 KiB
Go
// Copyright 2017 Google Inc. All Rights Reserved.
|
|
//
|
|
// 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.
|
|
|
|
//go:generate ./COMPILE-PROTOS.sh
|
|
|
|
// Gnostic is a tool for building better REST APIs through knowledge.
|
|
//
|
|
// Gnostic reads declarative descriptions of REST APIs that conform
|
|
// to the OpenAPI Specification, reports errors, resolves internal
|
|
// dependencies, and puts the results in a binary form that can
|
|
// be used in any language that is supported by the Protocol Buffer
|
|
// tools.
|
|
//
|
|
// Gnostic models are validated and typed. This allows API tool
|
|
// developers to focus on their product and not worry about input
|
|
// validation and type checking.
|
|
//
|
|
// Gnostic calls plugins that implement a variety of API implementation
|
|
// and support features including generation of client and server
|
|
// support code.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/googleapis/gnostic/OpenAPIv2"
|
|
"github.com/googleapis/gnostic/OpenAPIv3"
|
|
"github.com/googleapis/gnostic/compiler"
|
|
"github.com/googleapis/gnostic/discovery"
|
|
"github.com/googleapis/gnostic/jsonwriter"
|
|
plugins "github.com/googleapis/gnostic/plugins"
|
|
surface "github.com/googleapis/gnostic/surface"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
const ( // Source Format
|
|
SourceFormatUnknown = 0
|
|
SourceFormatOpenAPI2 = 2
|
|
SourceFormatOpenAPI3 = 3
|
|
SourceFormatDiscovery = 4
|
|
)
|
|
|
|
// Determine the version of an OpenAPI description read from JSON or YAML.
|
|
func getOpenAPIVersionFromInfo(info interface{}) int {
|
|
m, ok := compiler.UnpackMap(info)
|
|
if !ok {
|
|
return SourceFormatUnknown
|
|
}
|
|
swagger, ok := compiler.MapValueForKey(m, "swagger").(string)
|
|
if ok && strings.HasPrefix(swagger, "2.0") {
|
|
return SourceFormatOpenAPI2
|
|
}
|
|
openapi, ok := compiler.MapValueForKey(m, "openapi").(string)
|
|
if ok && strings.HasPrefix(openapi, "3.0") {
|
|
return SourceFormatOpenAPI3
|
|
}
|
|
kind, ok := compiler.MapValueForKey(m, "kind").(string)
|
|
if ok && kind == "discovery#restDescription" {
|
|
return SourceFormatDiscovery
|
|
}
|
|
return SourceFormatUnknown
|
|
}
|
|
|
|
const (
|
|
pluginPrefix = "gnostic-"
|
|
extensionPrefix = "gnostic-x-"
|
|
)
|
|
|
|
type pluginCall struct {
|
|
Name string
|
|
Invocation string
|
|
}
|
|
|
|
// Invokes a plugin.
|
|
func (p *pluginCall) perform(document proto.Message, sourceFormat int, sourceName string) error {
|
|
if p.Name != "" {
|
|
request := &plugins.Request{}
|
|
|
|
// Infer the name of the executable by adding the prefix.
|
|
executableName := pluginPrefix + p.Name
|
|
|
|
// Validate invocation string with regular expression.
|
|
invocation := p.Invocation
|
|
|
|
//
|
|
// Plugin invocations must consist of
|
|
// zero or more comma-separated key=value pairs followed by a path.
|
|
// If pairs are present, a colon separates them from the path.
|
|
// Keys and values must be alphanumeric strings and may contain
|
|
// dashes, underscores, periods, or forward slashes.
|
|
// A path can contain any characters other than the separators ',', ':', and '='.
|
|
//
|
|
invocationRegex := regexp.MustCompile(`^([\w-_\/\.]+=[\w-_\/\.]+(,[\w-_\/\.]+=[\w-_\/\.]+)*:)?[^,:=]+$`)
|
|
if !invocationRegex.Match([]byte(p.Invocation)) {
|
|
return fmt.Errorf("Invalid invocation of %s: %s", executableName, invocation)
|
|
}
|
|
|
|
invocationParts := strings.Split(p.Invocation, ":")
|
|
var outputLocation string
|
|
switch len(invocationParts) {
|
|
case 1:
|
|
outputLocation = invocationParts[0]
|
|
case 2:
|
|
parameters := strings.Split(invocationParts[0], ",")
|
|
for _, keyvalue := range parameters {
|
|
pair := strings.Split(keyvalue, "=")
|
|
if len(pair) == 2 {
|
|
request.Parameters = append(request.Parameters, &plugins.Parameter{Name: pair[0], Value: pair[1]})
|
|
}
|
|
}
|
|
outputLocation = invocationParts[1]
|
|
default:
|
|
// badly-formed request
|
|
outputLocation = invocationParts[len(invocationParts)-1]
|
|
}
|
|
|
|
version := &plugins.Version{}
|
|
version.Major = 0
|
|
version.Minor = 1
|
|
version.Patch = 0
|
|
request.CompilerVersion = version
|
|
|
|
request.OutputPath = outputLocation
|
|
|
|
request.SourceName = sourceName
|
|
switch sourceFormat {
|
|
case SourceFormatOpenAPI2:
|
|
request.Openapi2 = document.(*openapi_v2.Document)
|
|
request.Surface, _ = surface.NewModelFromOpenAPI2(request.Openapi2)
|
|
case SourceFormatOpenAPI3:
|
|
request.Openapi3 = document.(*openapi_v3.Document)
|
|
request.Surface, _ = surface.NewModelFromOpenAPI3(request.Openapi3)
|
|
default:
|
|
}
|
|
|
|
requestBytes, _ := proto.Marshal(request)
|
|
|
|
cmd := exec.Command(executableName, "-plugin")
|
|
cmd.Stdin = bytes.NewReader(requestBytes)
|
|
cmd.Stderr = os.Stderr
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
response := &plugins.Response{}
|
|
err = proto.Unmarshal(output, response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
plugins.HandleResponse(response, outputLocation)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isFile(path string) bool {
|
|
fileInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return !fileInfo.IsDir()
|
|
}
|
|
|
|
func isDirectory(path string) bool {
|
|
fileInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return fileInfo.IsDir()
|
|
}
|
|
|
|
// Write bytes to a named file.
|
|
// Certain names have special meaning:
|
|
// ! writes nothing
|
|
// - writes to stdout
|
|
// = writes to stderr
|
|
// If a directory name is given, the file is written there with
|
|
// a name derived from the source and extension arguments.
|
|
func writeFile(name string, bytes []byte, source string, extension string) {
|
|
var writer io.Writer
|
|
if name == "!" {
|
|
return
|
|
} else if name == "-" {
|
|
writer = os.Stdout
|
|
} else if name == "=" {
|
|
writer = os.Stderr
|
|
} else if isDirectory(name) {
|
|
base := filepath.Base(source)
|
|
// Remove the original source extension.
|
|
base = base[0 : len(base)-len(filepath.Ext(base))]
|
|
// Build the path that puts the result in the passed-in directory.
|
|
filename := name + "/" + base + "." + extension
|
|
file, _ := os.Create(filename)
|
|
defer file.Close()
|
|
writer = file
|
|
} else {
|
|
file, _ := os.Create(name)
|
|
defer file.Close()
|
|
writer = file
|
|
}
|
|
writer.Write(bytes)
|
|
if name == "-" || name == "=" {
|
|
writer.Write([]byte("\n"))
|
|
}
|
|
}
|
|
|
|
// The Gnostic structure holds global state information for gnostic.
|
|
type Gnostic struct {
|
|
usage string
|
|
sourceName string
|
|
binaryOutputPath string
|
|
textOutputPath string
|
|
yamlOutputPath string
|
|
jsonOutputPath string
|
|
errorOutputPath string
|
|
resolveReferences bool
|
|
pluginCalls []*pluginCall
|
|
extensionHandlers []compiler.ExtensionHandler
|
|
sourceFormat int
|
|
}
|
|
|
|
// Initialize a structure to store global application state.
|
|
func newGnostic() *Gnostic {
|
|
g := &Gnostic{}
|
|
// Option fields initialize to their default values.
|
|
g.usage = `
|
|
Usage: gnostic OPENAPI_SOURCE [OPTIONS]
|
|
OPENAPI_SOURCE is the filename or URL of an OpenAPI description to read.
|
|
Options:
|
|
--pb-out=PATH Write a binary proto to the specified location.
|
|
--text-out=PATH Write a text proto to the specified location.
|
|
--json-out=PATH Write a json API description to the specified location.
|
|
--yaml-out=PATH Write a yaml API description to the specified location.
|
|
--errors-out=PATH Write compilation errors to the specified location.
|
|
--PLUGIN-out=PATH Run the plugin named gnostic_PLUGIN and write results
|
|
to the specified location.
|
|
--x-EXTENSION Use the extension named gnostic-x-EXTENSION
|
|
to process OpenAPI specification extensions.
|
|
--resolve-refs Explicitly resolve $ref references.
|
|
This could have problems with recursive definitions.
|
|
`
|
|
// Initialize internal structures.
|
|
g.pluginCalls = make([]*pluginCall, 0)
|
|
g.extensionHandlers = make([]compiler.ExtensionHandler, 0)
|
|
return g
|
|
}
|
|
|
|
// Parse command-line options.
|
|
func (g *Gnostic) readOptions() {
|
|
// plugin processing matches patterns of the form "--PLUGIN-out=PATH" and "--PLUGIN_out=PATH"
|
|
pluginRegex := regexp.MustCompile("--(.+)[-_]out=(.+)")
|
|
|
|
// extension processing matches patterns of the form "--x-EXTENSION"
|
|
extensionRegex := regexp.MustCompile("--x-(.+)")
|
|
|
|
for i, arg := range os.Args {
|
|
if i == 0 {
|
|
continue // skip the tool name
|
|
}
|
|
var m [][]byte
|
|
if m = pluginRegex.FindSubmatch([]byte(arg)); m != nil {
|
|
pluginName := string(m[1])
|
|
invocation := string(m[2])
|
|
switch pluginName {
|
|
case "pb":
|
|
g.binaryOutputPath = invocation
|
|
case "text":
|
|
g.textOutputPath = invocation
|
|
case "json":
|
|
g.jsonOutputPath = invocation
|
|
case "yaml":
|
|
g.yamlOutputPath = invocation
|
|
case "errors":
|
|
g.errorOutputPath = invocation
|
|
default:
|
|
p := &pluginCall{Name: pluginName, Invocation: invocation}
|
|
g.pluginCalls = append(g.pluginCalls, p)
|
|
}
|
|
} else if m = extensionRegex.FindSubmatch([]byte(arg)); m != nil {
|
|
extensionName := string(m[1])
|
|
extensionHandler := compiler.ExtensionHandler{Name: extensionPrefix + extensionName}
|
|
g.extensionHandlers = append(g.extensionHandlers, extensionHandler)
|
|
} else if arg == "--resolve-refs" {
|
|
g.resolveReferences = true
|
|
} else if arg[0] == '-' {
|
|
fmt.Fprintf(os.Stderr, "Unknown option: %s.\n%s\n", arg, g.usage)
|
|
os.Exit(-1)
|
|
} else {
|
|
g.sourceName = arg
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate command-line options.
|
|
func (g *Gnostic) validateOptions() {
|
|
if g.binaryOutputPath == "" &&
|
|
g.textOutputPath == "" &&
|
|
g.yamlOutputPath == "" &&
|
|
g.jsonOutputPath == "" &&
|
|
g.errorOutputPath == "" &&
|
|
len(g.pluginCalls) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Missing output directives.\n%s\n", g.usage)
|
|
os.Exit(-1)
|
|
}
|
|
if g.sourceName == "" {
|
|
fmt.Fprintf(os.Stderr, "No input specified.\n%s\n", g.usage)
|
|
os.Exit(-1)
|
|
}
|
|
// If we get here and the error output is unspecified, write errors to stderr.
|
|
if g.errorOutputPath == "" {
|
|
g.errorOutputPath = "="
|
|
}
|
|
}
|
|
|
|
// Generate an error message to be written to stderr or a file.
|
|
func (g *Gnostic) errorBytes(err error) []byte {
|
|
return []byte("Errors reading " + g.sourceName + "\n" + err.Error())
|
|
}
|
|
|
|
// Read an OpenAPI description from YAML or JSON.
|
|
func (g *Gnostic) readOpenAPIText(bytes []byte) (message proto.Message, err error) {
|
|
info, err := compiler.ReadInfoFromBytes(g.sourceName, bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Determine the OpenAPI version.
|
|
g.sourceFormat = getOpenAPIVersionFromInfo(info)
|
|
if g.sourceFormat == SourceFormatUnknown {
|
|
return nil, errors.New("unable to identify OpenAPI version")
|
|
}
|
|
// Compile to the proto model.
|
|
if g.sourceFormat == SourceFormatOpenAPI2 {
|
|
document, err := openapi_v2.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &g.extensionHandlers))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message = document
|
|
} else if g.sourceFormat == SourceFormatOpenAPI3 {
|
|
document, err := openapi_v3.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &g.extensionHandlers))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message = document
|
|
} else {
|
|
document, err := discovery_v1.NewDocument(info, compiler.NewContextWithExtensions("$root", nil, &g.extensionHandlers))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message = document
|
|
}
|
|
return message, err
|
|
}
|
|
|
|
// Read an OpenAPI binary file.
|
|
func (g *Gnostic) readOpenAPIBinary(data []byte) (message proto.Message, err error) {
|
|
// try to read an OpenAPI v3 document
|
|
documentV3 := &openapi_v3.Document{}
|
|
err = proto.Unmarshal(data, documentV3)
|
|
if err == nil && strings.HasPrefix(documentV3.Openapi, "3.0") {
|
|
g.sourceFormat = SourceFormatOpenAPI3
|
|
return documentV3, nil
|
|
}
|
|
// if that failed, try to read an OpenAPI v2 document
|
|
documentV2 := &openapi_v2.Document{}
|
|
err = proto.Unmarshal(data, documentV2)
|
|
if err == nil && strings.HasPrefix(documentV2.Swagger, "2.0") {
|
|
g.sourceFormat = SourceFormatOpenAPI2
|
|
return documentV2, nil
|
|
}
|
|
// if that failed, try to read a Discovery Format document
|
|
discoveryDocument := &discovery_v1.Document{}
|
|
err = proto.Unmarshal(data, discoveryDocument)
|
|
if err == nil { // && strings.HasPrefix(documentV2.Swagger, "2.0") {
|
|
g.sourceFormat = SourceFormatDiscovery
|
|
return discoveryDocument, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Write a binary pb representation.
|
|
func (g *Gnostic) writeBinaryOutput(message proto.Message) {
|
|
protoBytes, err := proto.Marshal(message)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
defer os.Exit(-1)
|
|
} else {
|
|
writeFile(g.binaryOutputPath, protoBytes, g.sourceName, "pb")
|
|
}
|
|
}
|
|
|
|
// Write a text pb representation.
|
|
func (g *Gnostic) writeTextOutput(message proto.Message) {
|
|
bytes := []byte(proto.MarshalTextString(message))
|
|
writeFile(g.textOutputPath, bytes, g.sourceName, "text")
|
|
}
|
|
|
|
// Write JSON/YAML OpenAPI representations.
|
|
func (g *Gnostic) writeJSONYAMLOutput(message proto.Message) {
|
|
// Convert the OpenAPI document into an exportable MapSlice.
|
|
var rawInfo yaml.MapSlice
|
|
var ok bool
|
|
var err error
|
|
if g.sourceFormat == SourceFormatOpenAPI2 {
|
|
document := message.(*openapi_v2.Document)
|
|
rawInfo, ok = document.ToRawInfo().(yaml.MapSlice)
|
|
if !ok {
|
|
rawInfo = nil
|
|
}
|
|
} else if g.sourceFormat == SourceFormatOpenAPI3 {
|
|
document := message.(*openapi_v3.Document)
|
|
rawInfo, ok = document.ToRawInfo().(yaml.MapSlice)
|
|
if !ok {
|
|
rawInfo = nil
|
|
}
|
|
} else if g.sourceFormat == SourceFormatDiscovery {
|
|
document := message.(*discovery_v1.Document)
|
|
rawInfo, ok = document.ToRawInfo().(yaml.MapSlice)
|
|
if !ok {
|
|
rawInfo = nil
|
|
}
|
|
}
|
|
// Optionally write description in yaml format.
|
|
if g.yamlOutputPath != "" {
|
|
var bytes []byte
|
|
if rawInfo != nil {
|
|
bytes, err = yaml.Marshal(rawInfo)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error generating yaml output %s\n", err.Error())
|
|
}
|
|
writeFile(g.yamlOutputPath, bytes, g.sourceName, "yaml")
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "No yaml output available.\n")
|
|
}
|
|
}
|
|
// Optionally write description in json format.
|
|
if g.jsonOutputPath != "" {
|
|
var bytes []byte
|
|
if rawInfo != nil {
|
|
bytes, _ = jsonwriter.Marshal(rawInfo)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error generating json output %s\n", err.Error())
|
|
}
|
|
writeFile(g.jsonOutputPath, bytes, g.sourceName, "json")
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "No json output available.\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Perform all actions specified in the command-line options.
|
|
func (g *Gnostic) performActions(message proto.Message) (err error) {
|
|
// Optionally resolve internal references.
|
|
if g.resolveReferences {
|
|
if g.sourceFormat == SourceFormatOpenAPI2 {
|
|
document := message.(*openapi_v2.Document)
|
|
_, err = document.ResolveReferences(g.sourceName)
|
|
} else if g.sourceFormat == SourceFormatOpenAPI3 {
|
|
document := message.(*openapi_v3.Document)
|
|
_, err = document.ResolveReferences(g.sourceName)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Optionally write proto in binary format.
|
|
if g.binaryOutputPath != "" {
|
|
g.writeBinaryOutput(message)
|
|
}
|
|
// Optionally write proto in text format.
|
|
if g.textOutputPath != "" {
|
|
g.writeTextOutput(message)
|
|
}
|
|
// Optionaly write document in yaml and/or json formats.
|
|
if g.yamlOutputPath != "" || g.jsonOutputPath != "" {
|
|
g.writeJSONYAMLOutput(message)
|
|
}
|
|
// Call all specified plugins.
|
|
for _, p := range g.pluginCalls {
|
|
err := p.perform(message, g.sourceFormat, g.sourceName)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
defer os.Exit(-1) // run all plugins, even when some have errors
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Gnostic) main() {
|
|
var err error
|
|
g.readOptions()
|
|
g.validateOptions()
|
|
// Read the OpenAPI source.
|
|
bytes, err := compiler.ReadBytesForFile(g.sourceName)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
os.Exit(-1)
|
|
}
|
|
extension := strings.ToLower(filepath.Ext(g.sourceName))
|
|
var message proto.Message
|
|
if extension == ".json" || extension == ".yaml" {
|
|
// Try to read the source as JSON/YAML.
|
|
message, err = g.readOpenAPIText(bytes)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
os.Exit(-1)
|
|
}
|
|
} else if extension == ".pb" {
|
|
// Try to read the source as a binary protocol buffer.
|
|
message, err = g.readOpenAPIBinary(bytes)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
os.Exit(-1)
|
|
}
|
|
} else {
|
|
err = errors.New("unknown file extension. 'json', 'yaml', and 'pb' are accepted")
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
os.Exit(-1)
|
|
}
|
|
// Perform actions specified by command options.
|
|
err = g.performActions(message)
|
|
if err != nil {
|
|
writeFile(g.errorOutputPath, g.errorBytes(err), g.sourceName, "errors")
|
|
os.Exit(-1)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
g := newGnostic()
|
|
g.main()
|
|
}
|