...

Source file src/github.com/linkerd/linkerd2/pkg/profiles/openapi.go

Documentation: github.com/linkerd/linkerd2/pkg/profiles

     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  // RenderOpenAPI reads an OpenAPI spec file and renders the corresponding
    22  // ServiceProfile to a buffer, given a namespace, service, and control plane
    23  // namespace.
    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  // MkRouteSpec makes a service profile route from an OpenAPI operation.
   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