Skip to content

Commit

Permalink
OpenAPI Parser and Options (#2)
Browse files Browse the repository at this point in the history
* add openapi spec parser and options
  • Loading branch information
Kyle Hodgetts authored Oct 5, 2021
1 parent 5a5c56e commit 9163747
Show file tree
Hide file tree
Showing 14 changed files with 753 additions and 0 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ go 1.16

require (
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021
github.com/getkin/kin-openapi v0.76.0
github.com/ghodss/yaml v1.0.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang/protobuf v1.4.3
github.com/onsi/ginkgo v1.14.1
github.com/onsi/gomega v1.10.2
github.com/stretchr/testify v1.7.0
google.golang.org/grpc v1.36.0
k8s.io/apimachinery v0.20.2
k8s.io/client-go v0.20.2
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down Expand Up @@ -118,7 +120,10 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getkin/kin-openapi v0.76.0 h1:j77zg3Ec+k+r+GA3d8hBoXpAc6KX9TbBPrwQGBIy2sY=
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand All @@ -135,11 +140,16 @@ github.com/go-logr/zapr v0.2.0 h1:v6Ji8yBW77pva6NkJKQdHLAJKrIJKRHz0RXwPqCHSR4=
github.com/go-logr/zapr v0.2.0/go.mod h1:qhKdvif7YF5GI9NWEpyxTSSBdGmzkNguibrdCNVPunU=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
Expand Down Expand Up @@ -202,6 +212,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i
github.com/googleapis/gnostic v0.5.1 h1:A8Yhf6EtqTv9RMsU6MQTyrtV1TjWlR6xU9BsZIwuTCM=
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
Expand Down Expand Up @@ -264,6 +275,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
Expand Down
16 changes: 16 additions & 0 deletions options/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package options

import (
validation "github.com/go-ozzo/ozzo-validation/v4"
)

type ClusterOptions struct {
// ClusterDomain is the base DNS domain for the cluster. Default value is "cluster.local".
ClusterDomain string `yaml:"cluster_domain,omitempty" json:"cluster_domain,omitempty"`
}

func (o *ClusterOptions) Validate() error {
return validation.ValidateStruct(o,
validation.Field(&o.ClusterDomain, validation.Required.Error("cluster_domain is required")),
)
}
42 changes: 42 additions & 0 deletions options/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package options

import "reflect"

type CORSOptions struct {
Origins []string `yaml:"origins,omitempty" json:"origins,omitempty"`
Methods []string `yaml:"methods,omitempty" json:"methods,omitempty"`
Headers []string `yaml:"headers,omitempty" json:"headers,omitempty"`
ExposeHeaders []string `yaml:"expose_headers,omitempty" json:"expose_headers,omitempty"`

// Pointer because default value of bool is false which could have unintended side effects
// Check if not nil to ensure it's been set by user
Credentials *bool `yaml:"credentials,omitempty" json:"credentials,omitempty"`
MaxAge int `yaml:"max_age,omitempty" json:"max_age,omitempty"`
}

func (o *Options) GetCORSOpts(path, method string) CORSOptions {
// take global CORS options
corsOpts := o.CORS

// if non-zero path-level CORS options are different, override with them
if pathSubOpts, ok := o.PathSubOptions[path]; ok {
if !reflect.DeepEqual(CORSOptions{}, pathSubOpts.CORS) &&
!reflect.DeepEqual(corsOpts, pathSubOpts.CORS) {
corsOpts = pathSubOpts.CORS
}
}

// if non-zero operation-level CORS options are different, override them
if opSubOpts, ok := o.OperationSubOptions[path]; ok {
if !reflect.DeepEqual(CORSOptions{}, opSubOpts.CORS) &&
!reflect.DeepEqual(corsOpts, opSubOpts.CORS) {
corsOpts = opSubOpts.CORS
}
}

return corsOpts
}

func (o *CORSOptions) Validate() error {
return nil
}
11 changes: 11 additions & 0 deletions options/nginx_ingress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package options

type NGINXIngressOptions struct {
// RewriteTarget is a custom rewrite target for ingress-nginx.
// See https://kubernetes.github.io/ingress-nginx/examples/rewrite/ for additional documentation.
RewriteTarget string `yaml:"rewrite_target,omitempty" json:"rewrite_target,omitempty"`
}

func (o *NGINXIngressOptions) Validate() error {
return nil
}
122 changes: 122 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package options

import (
v "github.com/go-ozzo/ozzo-validation/v4"
)

// SubOptions allow user to overwrite certain options at path/operation level
// using x-kusk extension
type SubOptions struct {
Disabled *bool `yaml:"disabled,omitempty" json:"disabled,omitempty"`

Host string `yaml:"host,omitempty" json:"host,omitempty"`
CORS CORSOptions `yaml:"cors,omitempty" json:"cors,omitempty"`
RateLimits RateLimitOptions `yaml:"rate_limits,omitempty" json:"rate_limits,omitempty"`
Timeouts TimeoutOptions `yaml:"timeouts,omitempty" json:"timeouts,omitempty"`
}

type Options struct {
Disabled bool `yaml:"disabled,omitempty" json:"disabled,omitempty"`

// Namespace for the generated resource. Default value is "default".
Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`

// Service is a set of options of a target service to receive traffic.
Service ServiceOptions `yaml:"service,omitempty" json:"service,omitempty"`

// Path is a set of options to configure service endpoints paths.
Path PathOptions `yaml:"path,omitempty" json:"path,omitempty"`

// Cluster is a set of cluster-wide options.
Cluster ClusterOptions `yaml:"cluster,omitempty" json:"cluster,omitempty"`

// Host is an ingress host rule.
// See https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules for additional documentation.
Host string `yaml:"host,omitempty" json:"host,omitempty"`

CORS CORSOptions `yaml:"cors,omitempty" json:"cors,omitempty"`

// NGINXIngress is a set of custom nginx-ingress options.
NGINXIngress NGINXIngressOptions `yaml:"nginx_ingress,omitempty" json:"nginx_ingress,omitempty"`

// PathSubOptions allow to overwrite specific subset of Options for a given path.
// They are filled during extension parsing, the map key is path.
PathSubOptions map[string]SubOptions `yaml:"-" json:"-"`

// OperationSubOptions allow to overwrite specific subset of Options for a given operation.
// They are filled during extension parsing, the map key is method+path.
OperationSubOptions map[string]SubOptions `yaml:"-" json:"-"`

RateLimits RateLimitOptions `yaml:"rate_limits,omitempty" json:"rate_limits,omitempty"`

Timeouts TimeoutOptions `yaml:"timeouts,omitempty" json:"timeouts,omitempty"`
}

func (o *Options) fillDefaults() {
if o.Namespace == "" {
o.Namespace = "default"
}

if o.Path.Base == "" {
o.Path.Base = "/"
}

if o.Cluster.ClusterDomain == "" {
o.Cluster.ClusterDomain = "cluster.local"
}

if o.Service.Port == 0 {
o.Service.Port = 80
}
}

func (o *Options) Validate() error {
err := v.ValidateStruct(o,
v.Field(&o.Namespace, v.Required.Error("Target namespace is required")),
)

if err != nil {
return err
}

return nil
}

func (o *Options) FillDefaultsAndValidate() error {
o.fillDefaults()

return v.Validate([]v.Validatable{
o,
&o.Service,
&o.Path,
&o.Cluster,
&o.CORS,
&o.NGINXIngress,
&o.RateLimits,
&o.Timeouts,
})

}

func (o *Options) IsOperationDisabled(path, method string) bool {
opSubOptions, ok := o.OperationSubOptions[method+path]

// If the operation has an explicit value set, return that (takes precedence over the path level setting)
if ok && opSubOptions.Disabled != nil {
return *opSubOptions.Disabled
}

// No explicit value set for `Disabled` at the operation level, check the path level
return o.IsPathDisabled(path)
}

func (o *Options) IsPathDisabled(path string) bool {
pathSubOptions, ok := o.PathSubOptions[path]

// If the path has an explicit value set, return that (takes precedence over the global level setting)
if ok && pathSubOptions.Disabled != nil {
return *pathSubOptions.Disabled
}

return o.Disabled
}
26 changes: 26 additions & 0 deletions options/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package options

import (
"github.com/go-ozzo/ozzo-validation/v4"
)

type PathOptions struct {
// Base is the preceding prefix for the route (i.e. /your-prefix/here/rest/of/the/route).
// Default value is "/".
Base string `yaml:"base,omitempty" json:"base,omitempty"`

// TrimPrefix is the prefix that would be omitted from the URL when request is being forwarded
// to the upstream service, i.e. given that Base is set to "/petstore/api/v3", TrimPrefix is set to "/petstore",
// path that would be generated is "/petstore/api/v3/pets", URL that the upstream service would receive
// is "/api/v3/pets".
TrimPrefix string `yaml:"trim_prefix,omitempty" json:"trim_prefix,omitempty"`

// Split forces Kusk to generate a separate resource for each Path or Operation, where appropriate.
Split bool `yaml:"split,omitempty" json:"split,omitempty"`
}

func (o *PathOptions) Validate() error {
return validation.ValidateStruct(o,
validation.Field(&o.Base, validation.Required.Error("Base path required")),
)
}
36 changes: 36 additions & 0 deletions options/ratelimit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package options

import "reflect"

type RateLimitOptions struct {
RPS uint32 `json:"rps,omitempty" yaml:"rps,omitempty"`
Burst uint32 `json:"burst,omitempty" yaml:"burst,omitempty"`
Group string `json:"group,omitempty" yaml:"group,omitempty"`
}

func (o *Options) GetRateLimitOpts(path, method string) RateLimitOptions {
// take global rate limit options
rateLimitOpts := o.RateLimits

// if non-zero path-level rate limit options are different, override with them
if pathSubOpts, ok := o.PathSubOptions[path]; ok &&
pathSubOpts.RateLimits.ShouldOverride(rateLimitOpts) {
rateLimitOpts = pathSubOpts.RateLimits
}

// if non-zero operation-level rate limit options are different, override them
if opSubOpts, ok := o.OperationSubOptions[path]; ok &&
opSubOpts.RateLimits.ShouldOverride(rateLimitOpts) {
rateLimitOpts = opSubOpts.RateLimits
}

return rateLimitOpts
}

func (o *RateLimitOptions) ShouldOverride(opts RateLimitOptions) bool {
return !reflect.DeepEqual(CORSOptions{}, o) && !reflect.DeepEqual(opts, o)
}

func (o *RateLimitOptions) Validate() error {
return nil
}
24 changes: 24 additions & 0 deletions options/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package options

import (
v "github.com/go-ozzo/ozzo-validation/v4"
)

type ServiceOptions struct {
// Namespace is the namespace containing the upstream Service.
Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`

// Name is the upstream Service's name.
Name string `yaml:"name,omitempty" json:"name,omitempty"`

// Port is the upstream Service's port. Default value is 80.
Port int32 `yaml:"port,omitempty" json:"port,omitempty"`
}

func (o *ServiceOptions) Validate() error {
return v.ValidateStruct(o,
v.Field(&o.Namespace, v.Required.Error("service.namespace is required")),
v.Field(&o.Name, v.Required.Error("service.name is required")),
v.Field(&o.Port, v.Required.Error("service.port is required"), v.Min(1), v.Max(65535)),
)
}
37 changes: 37 additions & 0 deletions options/timeout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package options

import "reflect"

type TimeoutOptions struct {
// RequestTimeout is total request timeout
RequestTimeout uint32 `yaml:"request_timeout,omitempty" json:"request_timeout,omitempty"`
// IdleTimeout is timeout for idle connection
IdleTimeout uint32 `yaml:"idle_timeout,omitempty" json:"idle_timeout,omitempty"`
}

func (o *Options) GetTimeoutOpts(path, method string) TimeoutOptions {
// take global timeout options
timeoutOpts := o.Timeouts

// if non-zero path-level timeout options are different, override them
if pathSubOpts, ok := o.PathSubOptions[path]; ok {
if !reflect.DeepEqual(TimeoutOptions{}, pathSubOpts.Timeouts) &&
!reflect.DeepEqual(timeoutOpts, pathSubOpts.Timeouts) {
timeoutOpts = pathSubOpts.Timeouts
}
}

// if non-zero operation-level timeout options are different, override them
if opSubOpts, ok := o.OperationSubOptions[path]; ok {
if !reflect.DeepEqual(TimeoutOptions{}, opSubOpts.Timeouts) &&
!reflect.DeepEqual(timeoutOpts, opSubOpts.Timeouts) {
timeoutOpts = opSubOpts.Timeouts
}
}

return timeoutOpts
}

func (o *TimeoutOptions) Validate() error {
return nil
}
Loading

0 comments on commit 9163747

Please sign in to comment.