1 package profiles
2
3 import (
4 "fmt"
5 "io"
6 "net/http"
7 "sort"
8 "strings"
9
10 "github.com/go-openapi/spec"
11 sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 "sigs.k8s.io/yaml"
14 )
15
16 const (
17 xLinkerdRetryable = "x-linkerd-retryable"
18 xLinkerdTimeout = "x-linkerd-timeout"
19 )
20
21
22
23
24 func RenderOpenAPI(fileName, namespace, name, clusterDomain string) (*sp.ServiceProfile, error) {
25
26 input, err := readFile(fileName)
27 if err != nil {
28 return nil, err
29 }
30
31 bytes, err := io.ReadAll(input)
32 if err != nil {
33 return nil, fmt.Errorf("Error reading file: %w", err)
34 }
35 json, err := yaml.YAMLToJSON(bytes)
36 if err != nil {
37 return nil, fmt.Errorf("Error parsing yaml: %w", err)
38 }
39
40 swagger := spec.Swagger{}
41 err = swagger.UnmarshalJSON(json)
42 if err != nil {
43 return nil, fmt.Errorf("Error parsing OpenAPI spec: %w", err)
44 }
45
46 profile := swaggerToServiceProfile(swagger, namespace, name, clusterDomain)
47
48 return &profile, nil
49 }
50
51 func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomain string) sp.ServiceProfile {
52 profile := sp.ServiceProfile{
53 ObjectMeta: metav1.ObjectMeta{
54 Name: fmt.Sprintf("%s.%s.svc.%s", name, namespace, clusterDomain),
55 Namespace: namespace,
56 },
57 TypeMeta: ServiceProfileMeta,
58 }
59
60 routes := make([]*sp.RouteSpec, 0)
61
62 paths := make([]string, 0)
63 if swagger.Paths != nil {
64 for path := range swagger.Paths.Paths {
65 paths = append(paths, path)
66 }
67 sort.Strings(paths)
68 }
69
70 base := strings.TrimRight(swagger.BasePath, "/")
71 for _, relPath := range paths {
72 item := swagger.Paths.Paths[relPath]
73 path := base + "/" + strings.TrimLeft(relPath, "/")
74 pathRegex := PathToRegex(path)
75 if item.Delete != nil {
76 spec := MkRouteSpec(path, pathRegex, http.MethodDelete, item.Delete)
77 routes = append(routes, spec)
78 }
79 if item.Get != nil {
80 spec := MkRouteSpec(path, pathRegex, http.MethodGet, item.Get)
81 routes = append(routes, spec)
82 }
83 if item.Head != nil {
84 spec := MkRouteSpec(path, pathRegex, http.MethodHead, item.Head)
85 routes = append(routes, spec)
86 }
87 if item.Options != nil {
88 spec := MkRouteSpec(path, pathRegex, http.MethodOptions, item.Options)
89 routes = append(routes, spec)
90 }
91 if item.Patch != nil {
92 spec := MkRouteSpec(path, pathRegex, http.MethodPatch, item.Patch)
93 routes = append(routes, spec)
94 }
95 if item.Post != nil {
96 spec := MkRouteSpec(path, pathRegex, http.MethodPost, item.Post)
97 routes = append(routes, spec)
98 }
99 if item.Put != nil {
100 spec := MkRouteSpec(path, pathRegex, http.MethodPut, item.Put)
101 routes = append(routes, spec)
102 }
103 }
104
105 profile.Spec.Routes = routes
106 return profile
107 }
108
109
110 func MkRouteSpec(path, pathRegex string, method string, operation *spec.Operation) *sp.RouteSpec {
111 retryable := false
112 timeout := ""
113 var responses *spec.Responses
114 if operation != nil {
115 retryable, _ = operation.VendorExtensible.Extensions.GetBool(xLinkerdRetryable)
116 timeout, _ = operation.VendorExtensible.Extensions.GetString(xLinkerdTimeout)
117 responses = operation.Responses
118 }
119 return &sp.RouteSpec{
120 Name: fmt.Sprintf("%s %s", method, path),
121 Condition: toReqMatch(pathRegex, method),
122 ResponseClasses: toRspClasses(responses),
123 IsRetryable: retryable,
124 Timeout: timeout,
125 }
126 }
127
128 func toReqMatch(path string, method string) *sp.RequestMatch {
129 return &sp.RequestMatch{
130 PathRegex: path,
131 Method: method,
132 }
133 }
134
135 func toRspClasses(responses *spec.Responses) []*sp.ResponseClass {
136 if responses == nil {
137 return nil
138 }
139 classes := make([]*sp.ResponseClass, 0)
140
141 statuses := make([]int, 0)
142 for status := range responses.StatusCodeResponses {
143 statuses = append(statuses, status)
144 }
145 sort.Ints(statuses)
146
147 for _, status := range statuses {
148 cond := &sp.ResponseMatch{
149 Status: &sp.Range{
150 Min: uint32(status),
151 Max: uint32(status),
152 },
153 }
154 classes = append(classes, &sp.ResponseClass{
155 Condition: cond,
156 IsFailure: status >= 500,
157 })
158 }
159 return classes
160 }
161
View as plain text