linkerd2/pkg/profiles/openapi.go

153 lines
3.8 KiB
Go

package profiles
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"path"
"regexp"
"sort"
"github.com/go-openapi/spec"
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)
// RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
// ServiceProfile to a buffer, given a namespace, service, and control plane
// namespace.
func RenderOpenAPI(fileName, namespace, name string, w io.Writer) error {
input, err := readFile(fileName)
if err != nil {
return err
}
bytes, err := ioutil.ReadAll(input)
if err != nil {
return fmt.Errorf("Error reading file: %s", err)
}
json, err := yaml.YAMLToJSON(bytes)
if err != nil {
return fmt.Errorf("Error parsing yaml: %s", err)
}
swagger := spec.Swagger{}
err = swagger.UnmarshalJSON(json)
if err != nil {
return fmt.Errorf("Error parsing OpenAPI spec: %s", err)
}
profile := swaggerToServiceProfile(swagger, namespace, name)
return writeProfile(profile, w)
}
func swaggerToServiceProfile(swagger spec.Swagger, namespace, name string) sp.ServiceProfile {
profile := sp.ServiceProfile{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s.%s.svc.cluster.local", name, namespace),
Namespace: namespace,
},
TypeMeta: serviceProfileMeta,
}
routes := make([]*sp.RouteSpec, 0)
paths := make([]string, 0)
if swagger.Paths != nil {
for path := range swagger.Paths.Paths {
paths = append(paths, path)
}
sort.Strings(paths)
}
for _, relPath := range paths {
item := swagger.Paths.Paths[relPath]
path := path.Join(swagger.BasePath, relPath)
pathRegex := pathToRegex(path)
if item.Delete != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodDelete, item.Delete.Responses)
routes = append(routes, spec)
}
if item.Get != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodGet, item.Get.Responses)
routes = append(routes, spec)
}
if item.Head != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodHead, item.Head.Responses)
routes = append(routes, spec)
}
if item.Options != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodOptions, item.Options.Responses)
routes = append(routes, spec)
}
if item.Patch != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPatch, item.Patch.Responses)
routes = append(routes, spec)
}
if item.Post != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPost, item.Post.Responses)
routes = append(routes, spec)
}
if item.Put != nil {
spec := mkRouteSpec(path, pathRegex, http.MethodPut, item.Put.Responses)
routes = append(routes, spec)
}
}
profile.Spec.Routes = routes
return profile
}
func mkRouteSpec(path, pathRegex string, method string, responses *spec.Responses) *sp.RouteSpec {
return &sp.RouteSpec{
Name: fmt.Sprintf("%s %s", method, path),
Condition: toReqMatch(pathRegex, method),
ResponseClasses: toRspClasses(responses),
}
}
func pathToRegex(path string) string {
escaped := regexp.QuoteMeta(path)
return pathParamRegex.ReplaceAllLiteralString(escaped, "[^/]*")
}
func toReqMatch(path string, method string) *sp.RequestMatch {
return &sp.RequestMatch{
PathRegex: path,
Method: method,
}
}
func toRspClasses(responses *spec.Responses) []*sp.ResponseClass {
if responses == nil {
return nil
}
classes := make([]*sp.ResponseClass, 0)
statuses := make([]int, 0)
for status := range responses.StatusCodeResponses {
statuses = append(statuses, status)
}
sort.Ints(statuses)
for _, status := range statuses {
cond := &sp.ResponseMatch{
Status: &sp.Range{
Min: uint32(status),
Max: uint32(status),
},
}
classes = append(classes, &sp.ResponseClass{
Condition: cond,
IsFailure: status >= 500,
})
}
return classes
}