diff --git a/.github/workflows/code-style.yaml b/.github/workflows/code-style.yaml index 091a7bf..5e5e226 100644 --- a/.github/workflows/code-style.yaml +++ b/.github/workflows/code-style.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.16.x - uses: actions/setup-go@v2 + - name: Set up Go 1.21.x + uses: actions/setup-go@v4 with: - go-version: 1.16.x + go-version: 1.21.x id: go - name: Check out code diff --git a/.github/workflows/commands.yaml b/.github/workflows/commands.yaml index 4d7b1c1..272ff58 100644 --- a/.github/workflows/commands.yaml +++ b/.github/workflows/commands.yaml @@ -12,16 +12,18 @@ jobs: install: name: Run kuadrantctl install runs-on: ubuntu-latest + env: + KIND_CLUSTER_NAME: kuadrantctl-local steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v3 - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.2.0 + uses: helm/kind-action@v1.8.0 with: version: v0.20.0 config: utils/kind-cluster.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..d667e88 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,29 @@ +--- + +name: Release + +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + release-matrix: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + # build and publish in parallel: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 + goos: [linux, darwin] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + goversion: 1.21.4 diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 5366111..394bb75 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -13,10 +13,10 @@ jobs: name: Build executable runs-on: ubuntu-latest steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v2 @@ -28,17 +28,17 @@ jobs: name: Run tests runs-on: ubuntu-latest env: - KIND_CLUSTER_NAME: kuadrant-local + KIND_CLUSTER_NAME: kuadrantctl-local steps: - - name: Set up Go 1.20.x + - name: Set up Go 1.21.x uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x id: go - name: Check out code uses: actions/checkout@v3 - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.2.0 + uses: helm/kind-action@v1.8.0 with: version: v0.20.0 config: utils/kind-cluster.yaml diff --git a/Makefile b/Makefile index 0f84025..2a23925 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,7 @@ env-setup: $(MAKE) olm-install $(MAKE) gateway-api-install $(MAKE) istio-install + $(MAKE) deploy-gateway ## local-setup: Sets up Kind cluster with GatewayAPI manifests and istio GW, nothing Kuadrant. Build and install kuadrantctl binary .PHONY: local-setup diff --git a/README.md b/README.md index ea119ba..c3af817 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Use `go install` to install the latest version of the library. This command will ``` go install github.com/kuadrant/kuadrantctl@latest ``` -> Golang 1.20+ required +> Golang 1.21+ required ## Commands * [Install Kuadrant](doc/install.md) diff --git a/cmd/generate_gatewayapi_httproute.go b/cmd/generate_gatewayapi_httproute.go index 3ec64f3..fea188d 100644 --- a/cmd/generate_gatewayapi_httproute.go +++ b/cmd/generate_gatewayapi_httproute.go @@ -13,73 +13,30 @@ import ( ) var ( - generateGatewayAPIHTTPRouteOAS string - generateGatewayAPIHTTPRouteHost string - generateGatewayAPIHTTPRouteSvcName string - generateGatewayAPIHTTPRouteSvcNamespace string - generateGatewayAPIHTTPRouteSvcPort int32 - generateGatewayAPIHTTPRouteGateways []string + generateGatewayAPIHTTPRouteOAS string ) -//kuadrantctl generate istio virtualservice --namespace myns --oas petstore.yaml --public-host www.kuadrant.io --service-name myservice --gateway kuadrant-gateway -// --namespace myns -// --service-name myservice -// --public-host www.kuadrant.io -// --gateway kuadrant-gateway -// -- service-port 80 +//kuadrantctl generate gatewayapi httproute --oas [OAS_FILE_PATH | OAS_URL | @] func generateGatewayApiHttpRouteCommand() *cobra.Command { cmd := &cobra.Command{ Use: "httproute", - Short: "Generate Gateway API HTTPRoute from OpenAPI 3.x", - Long: "Generate Gateway API HTTPRoute from OpenAPI 3.x", - RunE: func(cmd *cobra.Command, args []string) error { - return generateGatewayApiHttpRoute(cmd, args) - }, + Short: "Generate Gateway API HTTPRoute from OpenAPI 3.0.X", + Long: "Generate Gateway API HTTPRoute from OpenAPI 3.0.X", + RunE: runGenerateGatewayApiHttpRoute, } // OpenAPI ref - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR - (required)") + cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)") err := cmd.MarkFlagRequired("oas") if err != nil { panic(err) } - // service ref - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteSvcName, "service-name", "", "Service name (required)") - err = cmd.MarkFlagRequired("service-name") - if err != nil { - panic(err) - } - - // service namespace - cmd.Flags().StringVarP(&generateGatewayAPIHTTPRouteSvcNamespace, "namespace", "n", "", "Service namespace (required)") - err = cmd.MarkFlagRequired("namespace") - if err != nil { - panic(err) - } - - // service host - cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteHost, "public-host", "", "Public host (required)") - err = cmd.MarkFlagRequired("public-host") - if err != nil { - panic(err) - } - - // service port - cmd.Flags().Int32VarP(&generateGatewayAPIHTTPRouteSvcPort, "port", "p", 80, "Service Port (required)") - - // gateway - cmd.Flags().StringSliceVar(&generateGatewayAPIHTTPRouteGateways, "gateway", []string{}, "Gateways (required)") - err = cmd.MarkFlagRequired("gateway") - if err != nil { - panic(err) - } - return cmd } -func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { +func runGenerateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS) if err != nil { return err @@ -96,10 +53,7 @@ func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { return fmt.Errorf("OpenAPI validation error: %w", err) } - httpRoute, err := generateGatewayAPIHTTPRoute(cmd, doc) - if err != nil { - return err - } + httpRoute := buildHTTPRoute(doc) jsonData, err := json.Marshal(httpRoute) if err != nil { @@ -110,52 +64,19 @@ func generateGatewayApiHttpRoute(cmd *cobra.Command, args []string) error { return nil } -func generateGatewayAPIHTTPRoute(cmd *cobra.Command, doc *openapi3.T) (*gatewayapiv1beta1.HTTPRoute, error) { - - //loop through gateway - // https://github.com/getkin/kin-openapi - gatewaysRef := []gatewayapiv1beta1.ParentReference{} - for _, gateway := range generateGatewayAPIHTTPRouteGateways { - gatewaysRef = append(gatewaysRef, gatewayapiv1beta1.ParentReference{ - Name: gatewayapiv1beta1.ObjectName(gateway), - }) - } - - port := gatewayapiv1beta1.PortNumber(generateGatewayAPIHTTPRouteSvcPort) - service := fmt.Sprintf("%s.%s.svc", generateGatewayAPIHTTPRouteSvcName, generateGatewayAPIHTTPRouteSvcNamespace) - matches, err := gatewayapi.HTTPRouteMatchesFromOAS(doc) - if err != nil { - return nil, err - } - - httpRoute := gatewayapiv1beta1.HTTPRoute{ +func buildHTTPRoute(doc *openapi3.T) *gatewayapiv1beta1.HTTPRoute { + return &gatewayapiv1beta1.HTTPRoute{ TypeMeta: v1.TypeMeta{ - Kind: "HTTPRoute", APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "HTTPRoute", }, + ObjectMeta: gatewayapi.HTTPRouteObjectMetaFromOAS(doc), Spec: gatewayapiv1beta1.HTTPRouteSpec{ CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ - ParentRefs: gatewaysRef, - }, - Hostnames: []gatewayapiv1beta1.Hostname{ - gatewayapiv1beta1.Hostname(generateGatewayAPIHTTPRouteHost), - }, - Rules: []gatewayapiv1beta1.HTTPRouteRule{ - { - BackendRefs: []gatewayapiv1beta1.HTTPBackendRef{ - { - BackendRef: gatewayapiv1beta1.BackendRef{ - BackendObjectReference: gatewayapiv1beta1.BackendObjectReference{ - Name: gatewayapiv1beta1.ObjectName(service), - Port: &port, - }, - }, - }, - }, - Matches: matches, - }, + ParentRefs: gatewayapi.HTTPRouteGatewayParentRefsFromOAS(doc), }, + Hostnames: gatewayapi.HTTPRouteHostnamesFromOAS(doc), + Rules: gatewayapi.HTTPRouteRulesFromOAS(doc), }, } - return &httpRoute, nil } diff --git a/cmd/generate_kuadrant.go b/cmd/generate_kuadrant.go index 0cc73f3..3e575d6 100644 --- a/cmd/generate_kuadrant.go +++ b/cmd/generate_kuadrant.go @@ -11,5 +11,8 @@ func generateKuadrantCommand() *cobra.Command { Long: "Generate Kuadrant resources", } + cmd.AddCommand(generateKuadrantRateLimitPolicyCommand()) + cmd.AddCommand(generateKuadrantAuthPolicyCommand()) + return cmd } diff --git a/cmd/generate_kuadrant_authpolicy.go b/cmd/generate_kuadrant_authpolicy.go new file mode 100644 index 0000000..177e50a --- /dev/null +++ b/cmd/generate_kuadrant_authpolicy.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/kuadrantapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +//kuadrantctl generate kuadrant authpolicy --oas [OAS_FILE_PATH | OAS_URL | @] + +func generateKuadrantAuthPolicyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "authpolicy", + Short: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X", + Long: "Generate Kuadrant AuthPolicy from OpenAPI 3.0.X", + RunE: runGenerateKuadrantAuthPolicy, + } + + // OpenAPI ref + cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)") + err := cmd.MarkFlagRequired("oas") + if err != nil { + panic(err) + } + + return cmd +} + +func runGenerateKuadrantAuthPolicy(cmd *cobra.Command, args []string) error { + oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS) + if err != nil { + return err + } + + openapiLoader := openapi3.NewLoader() + doc, err := openapiLoader.LoadFromData(oasDataRaw) + if err != nil { + return err + } + + err = doc.Validate(openapiLoader.Context) + if err != nil { + return fmt.Errorf("OpenAPI validation error: %w", err) + } + + ap := buildAuthPolicy(doc) + + jsonData, err := json.Marshal(ap) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), string(jsonData)) + return nil +} + +func buildAuthPolicy(doc *openapi3.T) *kuadrantapiv1beta2.AuthPolicy { + routeMeta := gatewayapi.HTTPRouteObjectMetaFromOAS(doc) + + ap := &kuadrantapiv1beta2.AuthPolicy{ + TypeMeta: v1.TypeMeta{ + APIVersion: "kuadrant.io/v1beta2", + Kind: "AuthPolicy", + }, + ObjectMeta: kuadrantapi.AuthPolicyObjectMetaFromOAS(doc), + Spec: kuadrantapiv1beta2.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1beta1.Group("gateway.networking.k8s.io"), + Kind: gatewayapiv1beta1.Kind("HTTPRoute"), + Name: gatewayapiv1beta1.ObjectName(routeMeta.Name), + }, + // Currently only authentication rules enforced + AuthScheme: kuadrantapiv1beta2.AuthSchemeSpec{ + Authentication: kuadrantapi.AuthPolicyAuthenticationSchemeFromOAS(doc), + }, + RouteSelectors: kuadrantapi.AuthPolicyTopRouteSelectorsFromOAS(doc), + }, + } + + if routeMeta.Namespace != "" { + ap.Spec.TargetRef.Namespace = &[]gatewayapiv1beta1.Namespace{ + gatewayapiv1beta1.Namespace(routeMeta.Namespace), + }[0] + } + + return ap +} diff --git a/cmd/generate_kuadrant_ratelimitpolicy.go b/cmd/generate_kuadrant_ratelimitpolicy.go new file mode 100644 index 0000000..9115607 --- /dev/null +++ b/cmd/generate_kuadrant_ratelimitpolicy.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/kuadrantapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +//kuadrantctl generate kuadrant ratelimitpolicy --oas [OAS_FILE_PATH | OAS_URL | @] + +func generateKuadrantRateLimitPolicyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ratelimitpolicy", + Short: "Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X", + Long: "Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X", + RunE: runGenerateKuadrantRateLimitPolicy, + } + + // OpenAPI ref + cmd.Flags().StringVar(&generateGatewayAPIHTTPRouteOAS, "oas", "", "/path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required)") + err := cmd.MarkFlagRequired("oas") + if err != nil { + panic(err) + } + + return cmd +} + +func runGenerateKuadrantRateLimitPolicy(cmd *cobra.Command, args []string) error { + oasDataRaw, err := utils.ReadExternalResource(generateGatewayAPIHTTPRouteOAS) + if err != nil { + return err + } + + openapiLoader := openapi3.NewLoader() + doc, err := openapiLoader.LoadFromData(oasDataRaw) + if err != nil { + return err + } + + err = doc.Validate(openapiLoader.Context) + if err != nil { + return fmt.Errorf("OpenAPI validation error: %w", err) + } + + rlp := buildRateLimitPolicy(doc) + + jsonData, err := json.Marshal(rlp) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), string(jsonData)) + return nil +} + +func buildRateLimitPolicy(doc *openapi3.T) *kuadrantapiv1beta2.RateLimitPolicy { + routeMeta := gatewayapi.HTTPRouteObjectMetaFromOAS(doc) + + rlp := &kuadrantapiv1beta2.RateLimitPolicy{ + TypeMeta: v1.TypeMeta{ + APIVersion: "kuadrant.io/v1beta2", + Kind: "RateLimitPolicy", + }, + ObjectMeta: kuadrantapi.RateLimitPolicyObjectMetaFromOAS(doc), + Spec: kuadrantapiv1beta2.RateLimitPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1beta1.Group("gateway.networking.k8s.io"), + Kind: gatewayapiv1beta1.Kind("HTTPRoute"), + Name: gatewayapiv1beta1.ObjectName(routeMeta.Name), + }, + Limits: kuadrantapi.RateLimitPolicyLimitsFromOAS(doc), + }, + } + + if routeMeta.Namespace != "" { + rlp.Spec.TargetRef.Namespace = &[]gatewayapiv1beta1.Namespace{ + gatewayapiv1beta1.Namespace(routeMeta.Namespace), + }[0] + } + + return rlp +} diff --git a/cmd/install.go b/cmd/install.go index 9d1741b..c36eaa0 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -101,8 +101,9 @@ func waitForDeployments(k8sClient client.Client) error { } for _, key := range deploymentKeys { - err := wait.Poll(retryInterval, timeout, func() (bool, error) { - return utils.CheckDeploymentAvailable(k8sClient, key) + immediate := true + err := wait.PollUntilContextTimeout(context.Background(), retryInterval, timeout, immediate, func(ctx context.Context) (bool, error) { + return utils.CheckDeploymentAvailable(ctx, k8sClient, key) }) if err != nil { @@ -147,10 +148,11 @@ func deployKuadrantOperator(k8sClient client.Client) error { var installPlanKey client.ObjectKey // Wait for the install process to be completed + immediate := true logf.Log.Info("Waiting for the kuadrant operator installation") - err = wait.Poll(time.Second*2, time.Second*20, func() (bool, error) { + err = wait.PollUntilContextTimeout(context.Background(), time.Second*5, time.Minute*2, immediate, func(ctx context.Context) (bool, error) { existingSubs := &operators.Subscription{} - err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(subs), existingSubs) + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(subs), existingSubs) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Subscription not available", "name", client.ObjectKeyFromObject(subs)) @@ -176,9 +178,9 @@ func deployKuadrantOperator(k8sClient client.Client) error { } logf.Log.Info("Waiting for the install plan", "name", installPlanKey) - err = wait.Poll(time.Second*5, time.Minute*2, func() (bool, error) { + err = wait.PollUntilContextTimeout(context.Background(), time.Second*5, time.Minute*2, immediate, func(ctx context.Context) (bool, error) { ip := &operators.InstallPlan{} - err := k8sClient.Get(context.Background(), installPlanKey, ip) + err := k8sClient.Get(ctx, installPlanKey, ip) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Install plan not available", "name", installPlanKey) @@ -230,8 +232,9 @@ func createNamespace(k8sClient client.Client) error { retryInterval := time.Second * 2 timeout := time.Second * 20 - return wait.Poll(retryInterval, timeout, func() (bool, error) { - err := k8sClient.Get(context.Background(), types.NamespacedName{Name: installNamespace}, &corev1.Namespace{}) + immediate := true + return wait.PollUntilContextTimeout(context.Background(), retryInterval, timeout, immediate, func(ctx context.Context) (bool, error) { + err := k8sClient.Get(ctx, types.NamespacedName{Name: installNamespace}, &corev1.Namespace{}) if err != nil && apierrors.IsNotFound(err) { return false, nil } diff --git a/config/gateway-api/gateway/gateway.yaml b/config/gateway-api/gateway/gateway.yaml new file mode 100644 index 0000000..7886c7a --- /dev/null +++ b/config/gateway-api/gateway/gateway.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + labels: + istio: ingressgateway + name: istio-ingressgateway +spec: + gatewayClassName: istio + listeners: + - name: default + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All + addresses: + - value: istio-ingressgateway.istio-system.svc.cluster.local + type: Hostname diff --git a/config/gateway-api/gateway/kustomization.yaml b/config/gateway-api/gateway/kustomization.yaml new file mode 100644 index 0000000..b489ae5 --- /dev/null +++ b/config/gateway-api/gateway/kustomization.yaml @@ -0,0 +1,5 @@ +--- +# Adds namespace to all resources. +namespace: istio-system +resources: +- gateway.yaml diff --git a/doc/development.md b/doc/development.md index 4c39802..f02e2b1 100644 --- a/doc/development.md +++ b/doc/development.md @@ -3,7 +3,7 @@ ## Technology stack required for development * [git][git_tool] -* [go] version 1.20+ +* [go] version 1.21+ ## Build the CLI ``` diff --git a/doc/generate-gateway-api-httproute.md b/doc/generate-gateway-api-httproute.md index be15a4d..5385332 100644 --- a/doc/generate-gateway-api-httproute.md +++ b/doc/generate-gateway-api-httproute.md @@ -1,35 +1,32 @@ ## Generate Gateway API HTTPRoute object from OpenAPI 3 The `kuadrantctl generate gatewayapi httproute` command generates an [Gateway API HTTPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/guides/http-routing/) -from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) and kubernetes service information. +from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) powered with [kuadrant extensions](openapi-kuadrant-extensions.md). ### OpenAPI specification +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + OpenAPI document resource can be provided by one of the following channels: * Filename in the available path. * URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. * Read from stdin standard input stream. -### Usage : +### Usage ```shell $ kuadrantctl generate gatewayapi httproute -h -Generate Gateway API HTTPRoute from OpenAPI 3.x +Generate Gateway API HTTPRoute from OpenAPI 3.0.X Usage: kuadrantctl generate gatewayapi httproute [flags] Flags: - --gateway strings Gateways (required) - -h, --help help for httproute - -n, --namespace string Service namespace (required) - --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR - (required) - -p, --port int32 Service Port (required) (default 80) - --public-host string Public host (required) - --service-name string Service name (required) + -h, --help help for httproute + --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required) Global Flags: -v, --verbose verbose output ``` -> Under the example folder there are examples of OAS 3 that can be used to generate the resources +> Under the example folder there are examples of OAS 3 that can be used to generate the resources diff --git a/doc/generate-kuadrant-auth-policy.md b/doc/generate-kuadrant-auth-policy.md new file mode 100644 index 0000000..732bb5d --- /dev/null +++ b/doc/generate-kuadrant-auth-policy.md @@ -0,0 +1,499 @@ +## Generate Kuadrant AuthPolicy object from OpenAPI 3 + +The `kuadrantctl generate kuadrant authpolicy` command generates an [Kuadrant AuthPolicy](https://github.com/Kuadrant/kuadrant-operator/blob/v0.4.1/doc/auth.md) +from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) powered with [kuadrant extensions](openapi-kuadrant-extensions.md). + +### OpenAPI specification + +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + +OpenAPI document resource can be provided by one of the following channels: +* Filename in the available path. +* URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. +* Read from stdin standard input stream. + +OpenAPI [Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) types + +| Types | Implemented | +| --- | --- | +| `openIdConnect` | **YES** | +| `apiKey` | **YES** | +| `http` | NO | +| `oauth2` | NO | + +### `openIdConnect` Type Description + +The following OAS example has one protected endpoint `GET /dog` with `openIdConnect` security scheme type. + +```yaml +paths: + /dog: + get: + operationId: "getDog" + security: + - securedDog: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + securedDog: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration +``` + +Running the command + +``` +kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P +``` + +The generated authpolicy (only relevan fields shown here): + +```yaml +kind: AuthPolicy +apiVersion: kuadrant.io/v1beta2 +metadata: + name: petstore + namespace: petstore + creationTimestamp: null +spec: + routeSelectors: + - matches: + - path: + type: Exact + value: /api/v1/dog + method: GET + rules: + authentication: + getDog_securedDog: + credentials: {} + jwt: + issuerUrl: https://example.com/.well-known/openid-configuration + routeSelectors: + - matches: + - path: + type: Exact + value: /api/v1/dog + method: GET +``` + +### `apiKey` Type Description + +The following OAS example has one protected endpoint `GET /dog` with `apiKey` security scheme type. + +```yaml +paths: + /dog: + get: + operationId: "getDog" + security: + - securedDog: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + securedDog: + type: apiKey + name: dog_token + in: query +``` + +Running the command + +``` +kuadrantctl generate kuadrant authpolicy --oas ./petstore-openapi.yaml | yq -P +``` + +The generated authpolicy (only relevan fields shown here): + +```yaml +kind: AuthPolicy +apiVersion: kuadrant.io/v1beta2 +metadata: + name: petstore + namespace: petstore + creationTimestamp: null +spec: + routeSelectors: + - matches: + - path: + type: Exact + value: /dog + method: GET + rules: + authentication: + getDog_securedDog: + credentials: + queryString: + name: dog_token + apiKey: + selector: + matchLabels: + kuadrant.io/apikeys-by: securedDog + routeSelectors: + - matches: + - path: + type: Exact + value: /dog + method: GET +``` + +In this particular example, the endpoint `GET /dog` will be protected. +The token needs to be in the query string of the request included in a parameter named `dog_token`. +Kuadrant will validate received tokens against tokens found in kubernetes secrets with label `kuadrant.io/apikeys-by: ${sec scheme name}`. +In this particular example the label selector will be: `kuadrant.io/apikeys-by: securedDog`. + +Like the following example: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: api-key-1 + labels: + authorino.kuadrant.io/managed-by: authorino + kuadrant.io/apikeys-by: securedDog +stringData: + api_key: MYSECRETTOKENVALUE +type: Opaque +``` +> **Note**: Kuadrant validates tokens against api keys found in secrets. The label selector format `kuadrant.io/apikeys-by: ${sec scheme name}` is arbitrary and designed for this CLI command. + +For more information about Kuadrant auth based on api key: https://docs.kuadrant.io/authorino/docs/user-guides/api-key-authentication/ + +### Usage + +```shell +Generate Kuadrant AuthPolicy from OpenAPI 3.0.X + +Usage: + kuadrantctl generate kuadrant authpolicy [flags] + +Flags: + -h, --help help for authpolicy + --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required) + +Global Flags: + -v, --verbose verbose output +``` + +> Under the example folder there are examples of OAS 3 that can be used to generate the resources + +### User Guide + +The verification steps will lead you to the process of deploying and testing the following api with +endpoints protected using different security schemes: + +| Operation | Security Scheme | +| --- | --- | +| `GET /api/v1/cat` | public (not auth) | +| `POST /api/v1/cat` | ApiKey in header | +| `GET /api/v1/dog` | OpenIdConnect | +| `GET /api/v1/snake` | OpenIdConnect **OR** ApiKey in query string | + +* [Optional] Setup SSO service supporting OIDC. For this example, we will be using [keycloak](https://www.keycloak.org). + * Create a new realm `petstore` + * Create a client `petstore`. In the Client Protocol field, select `openid-connect`. + * Configure client settings. Access Type to public. Direct Access Grants Enabled to ON (for this example password will be used directly to generate the token). + * Add a user to the realm + * Click the Users menu on the left side of the window. Click Add user. + * Type the username `bob`, set the Email Verified switch to ON, and click Save. + * On the Credentials tab, set the password `p`. Enter the password in both the fields, set the Temporary switch to OFF to avoid the password reset at the next login, and click `Set Password`. + +Now, let's run local cluster to test the kuadrantctl new command to generate authpolicy. + +* Clone the repo + +```bash +git clone https://github.com/Kuadrant/kuadrantctl.git +cd kuadrantctl +``` + +* Setup cluster, istio and Gateway API CRDs + +```bash +make local-setup +``` + +* Build and install CLI in `bin/kuadrantctl` path + +```bash +make install +``` + +* Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1 + +```bash +bin/kuadrantctl install +``` + +* Deploy petstore backend API + +```bash +kubectl create namespace petstore +kubectl apply -n petstore -f examples/petstore/petstore.yaml +``` + +* Let's create Petstore's OpenAPI spec + +
+ +```yaml +cat <petstore-openapi.yaml +--- +openapi: "3.1.0" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/api/v1 +paths: + /cat: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # No sec requirements + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # API key + operationId: "postCat" + security: + - cat_api_key: [] + responses: + 405: + description: "invalid input" + /dog: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # OIDC + operationId: "getDog" + security: + - oidc: + - read:dogs + responses: + 405: + description: "invalid input" + /snake: + x-kuadrant: + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # OIDC or API key + operationId: "getSnake" + security: + - oidc: ["read:snakes"] + - snakes_api_key: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + cat_api_key: + type: apiKey + name: api_key + in: header + oidc: + type: openIdConnect + openIdConnectUrl: https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore + snakes_api_key: + type: apiKey + name: snake_token + in: query +EOF +``` + +
+ +> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain + +* Create an API key only valid for `POST /api/v1/cat` endpoint +```yaml +kubectl apply -f -< **Note**: the label's value of `kuadrant.io/apikeys-by: cat_api_key` is the name of the sec scheme of the OpenAPI spec. + +* Create an API key only valid for `GET /api/v1/snake` endpoint + +```yaml +kubectl apply -f -< **Note**: the label's value of `kuadrant.io/apikeys-by: snakes_api_key` is the name of the sec scheme of the OpenAPI spec. + +* Create the HTTPRoute using the CLI + +```bash +bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Create Kuadrant's Auth Policy + +```bash +bin/kuadrantctl generate kuadrant authpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +Now, we are ready to test OpenAPI endpoints :exclamation: + +- `GET /api/v1/cat` -> It's a public endpoint, hence should return 200 Ok + +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/cat" +``` + +- `POST /api/v1/cat` -> It's a protected endpoint with apikey + +Without any credentials, it should return `401 Unauthorized` + +```bash +curl -H "Host: example.com" -X POST -i "http://127.0.0.1:9080/api/v1/cat" +``` + +``` +HTTP/1.1 401 Unauthorized +www-authenticate: Bearer realm="getDog_oidc" +www-authenticate: Bearer realm="getSnake_oidc" +www-authenticate: snake_token realm="getSnake_snakes_api_key" +www-authenticate: api_key realm="postCat_cat_api_key" +x-ext-auth-reason: {"postCat_cat_api_key":"credential not found"} +date: Tue, 28 Nov 2023 22:28:44 GMT +server: istio-envoy +content-length: 0 +``` + +The *reason* headers tell that `credential not found`. +Credentials satisfying `postCat_cat_api_key` authentication is needed. + +According to the OpenAPI spec, it should be a header named `api_key`. +What if we try a wrong token? one token assigned to other endpoint, +i.e. `I_LIKE_SNAKES` instead of the valid one `I_LIKE_CATS`. It should return `401 Unauthorized`. + +```bash +curl -H "Host: example.com" -H "api_key: I_LIKE_SNAKES" -X POST -i "http://127.0.0.1:9080/api/v1/cat" +``` + +``` +TTP/1.1 401 Unauthorized +www-authenticate: Bearer realm="getDog_oidc" +www-authenticate: Bearer realm="getSnake_oidc" +www-authenticate: snake_token realm="getSnake_snakes_api_key" +www-authenticate: api_key realm="postCat_cat_api_key" +x-ext-auth-reason: {"postCat_cat_api_key":"the API Key provided is invalid"} +date: Tue, 28 Nov 2023 22:32:55 GMT +server: istio-envoy +content-length: 0 +``` + +The *reason* headers tell that `the API Key provided is invalid`. +Using valid token (from the secret `cat-api-key-1` assigned to `POST /api/v1/cats`) +in the `api_key` header should return 200 Ok + +``` +curl -H "Host: example.com" -H "api_key: I_LIKE_CATS" -X POST -i "http://127.0.0.1:9080/api/v1/cat" +``` + +- `GET /api/v1/dog` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm) + +without credentials, it should return `401 Unauthorized` + +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/dog" +``` + +To get the authentication token, this example is using Direct Access Grants oauth2 grant type +(also known as Client Credentials grant type). When configuring the Keycloak (OIDC provider) client +settings, we enabled Direct Access Grants to enable this procedure. +We will be authenticating as `bob` user with `p` password. +We previously created `bob` user in Keycloak in the `petstore` realm. + +``` +export ACCESS_TOKEN=$(curl -k -H "Content-Type: application/x-www-form-urlencoded" \ + -d 'grant_type=password' \ + -d 'client_id=petstore' \ + -d 'scope=openid' \ + -d 'username=bob' \ + -d 'password=p' "https://${KEYCLOAK_PUBLIC_DOMAIN}/auth/realms/petstore/protocol/openid-connect/token" | jq -r '.access_token') +``` + +> Replace `${KEYCLOAK_PUBLIC_DOMAIN}` with your SSO instance domain + + +With the access token in place, let's try to get those puppies + +```bash +curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/dog -i +``` + +it should return 200 OK + +- `GET /api/v1/snake` -> It's a protected endpoint with oidc (assigned to our keycloak instance and `petstore` realm) **OR** with apiKey + +This example is to show that multiple security requirements (with *OR* semantics) can be specified +for an OpenAPI operation. + +Without credentials, it should return `401 Unauthorized` + +```bash +curl -H "Host: example.com" -i "http://127.0.0.1:9080/api/v1/snake" +``` + +With the access token in place, it should return 200 OK (unless the token has expired). + +```bash +curl -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Host: example.com' http://127.0.0.1:9080/api/v1/snake -i +``` + +With apiKey it should also work. According to the OpenAPI spec security scheme, +it should be a query string named `snake_token` and the token needs to be valid token +(from the secret `snake-api-key-1` assigned to `GET /api/v1/snake`) + +```bash +curl -H 'Host: example.com' -i "http://127.0.0.1:9080/api/v1/snake?snake_token=I_LIKE_SNAKES" +``` + +* Clean environment +```bash +make local-cleanup +``` diff --git a/doc/generate-kuadrant-rate-limit-policy.md b/doc/generate-kuadrant-rate-limit-policy.md new file mode 100644 index 0000000..7a5dbee --- /dev/null +++ b/doc/generate-kuadrant-rate-limit-policy.md @@ -0,0 +1,183 @@ +## Generate Kuadrant RateLimitPolicy object from OpenAPI 3 + +The `kuadrantctl generate kuadrant ratelimitpolicy` command generates an [Kuadrant RateLimitPolicy](https://github.com/Kuadrant/kuadrant-operator/blob/v0.4.1/doc/rate-limiting.md) +from your [OpenAPI Specification (OAS) 3.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md) powered with [kuadrant extensions](openapi-kuadrant-extensions.md). + +### OpenAPI specification + +[OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) + +OpenAPI document resource can be provided by one of the following channels: +* Filename in the available path. +* URL format (supported schemes are HTTP and HTTPS). The CLI will try to download from the given address. +* Read from stdin standard input stream. + +### Usage + +```shell +Generate Kuadrant RateLimitPolicy from OpenAPI 3.0.X + +Usage: + kuadrantctl generate kuadrant ratelimitpolicy [flags] + +Flags: + -h, --help help for ratelimitpolicy + --oas string /path/to/file.[json|yaml|yml] OR http[s]://domain/resource/path.[json|yaml|yml] OR @ (required) + +Global Flags: + -v, --verbose verbose output +``` + +> Under the example folder there are examples of OAS 3 that can be used to generate the resources + +### User Guide + +* Clone the repo +```bash +git clone https://github.com/Kuadrant/kuadrantctl.git +cd kuadrantctl +``` +* Setup cluster, istio and Gateway API CRDs +```bash +make local-setup +``` +* Build and install CLI in `bin/kuadrantctl` path +```bash +make install +``` +* Install Kuadrant service protection. The CLI can be used to install kuadrant v0.4.1 +```bash +bin/kuadrantctl install +``` +* Deploy petstore backend API +```bash +kubectl create namespace petstore +kubectl apply -n petstore -f examples/petstore/petstore.yaml +``` +* Let's create Petstore's OpenAPI spec + +
+ +```yaml +cat <petstore-openapi.yaml +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/v1 +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 1 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + get: # Added to the route and rate limited + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # NOT added to the route + x-kuadrant: + disable: true + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: # Added to the route and rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 3 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + operationId: "getDog" + responses: + 405: + description: "invalid input" + post: # Added to the route and NOT rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + operationId: "postDog" + responses: + 405: + description: "invalid input" +EOF +``` + +
+ +> **NOTE**: `servers` base path not included. WIP in following up PRs. + +| Operation | Applied config | +| --- | --- | +| `GET /cat` | It should return 200 Ok and be rate limited (1 req / 10 seconds) | +| `POST /cat` | Not added to the HTTPRoute. It should return 404 Not Found | +| `GET /dog` | It should return 200 Ok and be rate limited (3 req / 10 seconds) | +| `POST /dog` | It should return 200 Ok and NOT rate limited | + + +* Create the HTTPRoute using the CLI +```bash +bin/kuadrantctl generate gatewayapi httproute --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Create the Rate Limit Policy +```bash +bin/kuadrantctl generate kuadrant ratelimitpolicy --oas petstore-openapi.yaml | kubectl apply -n petstore -f - +``` + +* Test OpenAPI endpoints + * `GET /cat` -> It should return 200 Ok and be rate limited (1 req / 10 seconds) + +```bash +curl --resolve example.com:9080:127.0.0.1 -v "http://example.com:9080/cat" +``` + * `POST /cat` -> Not added to the HTTPRoute. It should return 404 Not Found +```bash +curl --resolve example.com:9080:127.0.0.1 -v -X POST "http://example.com:9080/cat" +``` + * `GET /dog` -> It should return 200 Ok and be rate limited (3 req / 10 seconds) + +```bash +curl --resolve example.com:9080:127.0.0.1 -v "http://example.com:9080/dog" +``` + + * `POST /dog` -> It should return 200 Ok and NOT rate limited + +```bash +curl --resolve example.com:9080:127.0.0.1 -v -X POST "http://example.com:9080/dog" +``` + +* Clean environment +```bash +make local-cleanup +``` diff --git a/doc/openapi-kuadrant-extensions.md b/doc/openapi-kuadrant-extensions.md new file mode 100644 index 0000000..3f2ca70 --- /dev/null +++ b/doc/openapi-kuadrant-extensions.md @@ -0,0 +1,78 @@ +## OpenAPI 3.0.X Kuadrant Extensions + +### Info level kuadrant extension + +Kuadrant extension that can be added at the info level of the OpenAPI spec. + +```yaml +info: + x-kuadrant: + route: ## HTTPRoute metadata + name: "petstore" + namespace: "petstore" + labels: ## map[string]string + deployment: petstore + hostnames: ## []gateway.networking.k8s.io/v1beta1.Hostname + - example.com + parentRefs: ## []gateway.networking.k8s.io/v1beta1.ParentReference + - name: apiGateway + namespace: gateways +``` + +### Path level kuadrant extension + +Kuadrant extension that can be added at the path level of the OpenAPI spec. +This configuration at the path level +is the default when there is no operation level configuration. + +```yaml +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + disable: true ## Remove from the HTTPRoute. Optional. Default: false + pathMatchType: Exact ## Specifies how to match against the path Value. Valid values: [Exact;PathPrefix]. Optional. Default: Exact + backendRefs: ## Backend references to be included in the HTTPRoute. []gateway.networking.k8s.io/v1beta1.HTTPBackendRef. Optional. + - name: petstore + port: 80 + namespace: petstore + rate_limit: ## Rate limit config. Optional. + rates: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate + - limit: 1 + duration: 10 + unit: second + counters: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.CountextSelector + - auth.identity.username + when: ## Kuadrant API []github.com/kuadrant/kuadrant-operator/api/v1beta2.WhenCondition + - selector: metadata.filter_metadata.envoy\.filters\.http\.ext_authz.identity.userid + operator: eq + value: alice +``` + +### Operation level kuadrant extension + +Kuadrant extension that can be added at the operation level of the OpenAPI spec. +Same schema as path level kuadrant extension. + +```yaml +paths: + /cat: + get: + x-kuadrant: ## Path level Kuadrant Extension + disable: true ## Remove from the HTTPRoute. Optional. Default: path level "disable" value + pathMatchType: Exact ## Specifies how to match against the path Value. Valid values: [Exact;PathPrefix]. Optional. Default: Exact + backendRefs: ## Backend references to be included in the HTTPRoute. Optional. + - name: petstore + port: 80 + namespace: petstore + rate_limit: ## Rate limit config. Optional. + rates: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.Rate + - limit: 1 + duration: 10 + unit: second + counters: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.CountextSelector + - auth.identity.username + when: ## Kuadrant API github.com/kuadrant/kuadrant-operator/api/v1beta2.WhenCondition + - selector: metadata.filter_metadata.envoy\.filters\.http\.ext_authz.identity.userid + operator: eq + value: alice +``` diff --git a/examples/oas3/petstore-multiple-sec-requirements.yaml b/examples/oas3/petstore-multiple-sec-requirements.yaml index 7afe0ff..91b2184 100644 --- a/examples/oas3/petstore-multiple-sec-requirements.yaml +++ b/examples/oas3/petstore-multiple-sec-requirements.yaml @@ -3,6 +3,15 @@ openapi: "3.1.0" info: title: "Pet Store API" version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system servers: - url: https://toplevel.example.io/v1 paths: @@ -15,7 +24,7 @@ paths: post: # API key operationId: "postCat" security: - - petstore_api_key: [] + - cat_api_key: [] responses: 405: description: "invalid input" @@ -23,17 +32,30 @@ paths: get: # OIDC operationId: "getDog" security: - - petstore_oidc: + - oidc: - read:dogs responses: 405: description: "invalid input" + /snake: + get: # OIDC or API key + operationId: "getSnake" + security: + - oidc: ["read:snakes"] + - snakes_api_key: [] + responses: + 405: + description: "invalid input" components: securitySchemes: - petstore_api_key: + cat_api_key: type: apiKey name: api_key in: header - petstore_oidc: + oidc: type: openIdConnect - openIdConnectUrl: http://example.org/auth/realms/myrealm + openIdConnectUrl: https://example.com/.well-known/openid-configuration + snakes_api_key: + type: apiKey + name: snake_token + in: query diff --git a/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml new file mode 100644 index 0000000..cdcc0b8 --- /dev/null +++ b/examples/oas3/petstore-with-oidc-kuadrant-extensions.yaml @@ -0,0 +1,53 @@ +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/api/v1 +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + get: # Added to the route and public (not auth) + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # NOT added to the route + x-kuadrant: ## Operation level Kuadrant Extension + enable: false + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: # Added to the route and authenticated + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + operationId: "getDog" + security: + - openIdConnect: [] + responses: + 405: + description: "invalid input" +components: + securitySchemes: + openIdConnect: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration diff --git a/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml b/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml new file mode 100644 index 0000000..f6e5548 --- /dev/null +++ b/examples/oas3/petstore-with-rate-limit-kuadrant-extensions.yaml @@ -0,0 +1,70 @@ +--- +openapi: "3.0.3" +info: + title: "Pet Store API" + version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system +servers: + - url: https://example.io/api/v1 +paths: + /cat: + x-kuadrant: ## Path level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 1 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + get: # Added to the route and rate limited + operationId: "getCat" + responses: + 405: + description: "invalid input" + post: # NOT added to the route + x-kuadrant: ## Operation level Kuadrant Extension + disable: true + operationId: "postCat" + responses: + 405: + description: "invalid input" + /dog: + get: # Added to the route and rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + rate_limit: + rates: + - limit: 3 + duration: 10 + unit: second + counters: + - request.headers.x-forwarded-for + operationId: "getDog" + responses: + 405: + description: "invalid input" + post: # Added to the route and NOT rate limited + x-kuadrant: ## Operation level Kuadrant Extension + backendRefs: + - name: petstore + port: 80 + namespace: petstore + operationId: "postDog" + responses: + 405: + description: "invalid input" diff --git a/examples/oas3/petstore.yaml b/examples/oas3/petstore.yaml index d141f96..1d5e3e2 100644 --- a/examples/oas3/petstore.yaml +++ b/examples/oas3/petstore.yaml @@ -1,5 +1,5 @@ --- -openapi: "3.0.0" +openapi: "3.0.2" info: title: "Pet Store API" version: "1.0.0" diff --git a/examples/petstore/petstore.yaml b/examples/petstore/petstore.yaml new file mode 100644 index 0000000..fd733a5 --- /dev/null +++ b/examples/petstore/petstore.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: petstore + labels: + app: petstore +spec: + selector: + matchLabels: + app: petstore + template: + metadata: + labels: + app: petstore + spec: + containers: + - name: petstore + image: quay.io/3scale/authorino:echo-api + env: + - name: PORT + value: "3000" + ports: + - containerPort: 3000 + name: http + replicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: petstore +spec: + selector: + app: petstore + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 3000 diff --git a/go.mod b/go.mod index b4c9f3d..fa79738 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/kuadrant/kuadrantctl -go 1.20 +go 1.21 require ( - github.com/getkin/kin-openapi v0.76.0 + github.com/getkin/kin-openapi v0.120.0 github.com/kuadrant/kuadrant-operator v0.4.1 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 @@ -13,17 +13,20 @@ require ( k8s.io/apiextensions-apiserver v0.28.3 k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/gateway-api v0.6.2 ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect @@ -38,17 +41,29 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kuadrant/authorino v0.15.0 // indirect github.com/kuadrant/authorino-operator v0.9.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/sirupsen/logrus v1.9.2 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect @@ -58,6 +73,7 @@ require ( golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect @@ -67,9 +83,9 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect istio.io/api v0.0.0-20230712174848-a2b2de508c88 // indirect + k8s.io/component-base v0.28.3 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 75f2101..661e0eb 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,47 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk= +github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -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 v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= +github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -66,14 +70,16 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93 h1:D5iJJZKAi0rU4e/5E58BkrnN+xeCDjAIqcm1GGxAGSI= +github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -83,25 +89,30 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kuadrant/authorino v0.15.0 h1:Xw/buh/wTINdL+IpLSxhlpet4hpleMxZzfx39c4VQng= +github.com/kuadrant/authorino v0.15.0/go.mod h1:vXkHKrntn8DR7kt8a8Ohxq+2lgAD0jWivThoP+7ASew= github.com/kuadrant/authorino-operator v0.9.0 h1:EV7zrYBNcd53HPQMivvTwe/+DIATTK7O4znJzh4xON8= github.com/kuadrant/authorino-operator v0.9.0/go.mod h1:VkUqS4CHNiaHMrjSFQ5V71DN829kPnqT3FQxqlOntEI= github.com/kuadrant/kuadrant-operator v0.4.1 h1:nGk7786goNzItxbIifmGWj6/Al8S7U+eT0fTcgEZphU= github.com/kuadrant/kuadrant-operator v0.4.1/go.mod h1:iD+CMYKOfcpSts2JxscTlkeBgsusBwEhVsuJw832EAY= github.com/kuadrant/limitador-operator v0.4.0 h1:HgJi7LuOsenCUMs2ACCfKMKsKpfHcqmmwVmqpci0hw4= -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/kuadrant/limitador-operator v0.4.0/go.mod h1:5fQo2XwxPr7bDObut9sK5sHCnK4hwAmTsTptaYvGfuc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -112,22 +123,30 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/operator-framework/api v0.19.0 h1:QU1CTJU+CufoeneA5rsNlP/uP96s8vDHWUYDFZTauzA= github.com/operator-framework/api v0.19.0/go.mod h1:SCCslqke6AVOJ5JM+NqNE1CHuAgJLScsL66pnPaSMXs= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -145,6 +164,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -152,6 +180,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -184,6 +213,7 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -228,11 +258,13 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= @@ -265,9 +297,11 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= istio.io/api v0.0.0-20230712174848-a2b2de508c88 h1:w7lSk+XcYNzGC5xtHBFvaV7QTBMQC5nM4l8xxFslGgk= istio.io/api v0.0.0-20230712174848-a2b2de508c88/go.mod h1:owGDRg9uqMob8CN1gxaOzk6nJxnbT8wrP7PmggpJHHY= k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= @@ -279,6 +313,7 @@ k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= diff --git a/make/gateway-api.mk b/make/gateway-api.mk index 81bd84e..e96f660 100644 --- a/make/gateway-api.mk +++ b/make/gateway-api.mk @@ -1,5 +1,9 @@ ##@ Gateway API resources +.PHONY: deploy-gateway +deploy-gateway: kustomize ## Deploy Gateway API gateway + $(KUSTOMIZE) build config/gateway-api/gateway | kubectl apply -f - + .PHONY: gateway-api-install gateway-api-install: kustomize ## Install Gateway API CRDs $(KUSTOMIZE) build config/gateway-api | kubectl apply -f - diff --git a/make/istio.mk b/make/istio.mk index 9644f45..a958dd9 100644 --- a/make/istio.mk +++ b/make/istio.mk @@ -18,7 +18,7 @@ istioctl: $(ISTIOCTL) ## Download istioctl locally if necessary. .PHONY: istio-install istio-install: istioctl ## Install istio. - $(ISTIOCTL) install --set profile=demo -y + $(ISTIOCTL) install -f utils/istio-operator.yaml -y .PHONY: istio-uninstall istio-uninstall: istioctl ## Uninstall istio. diff --git a/make/kind.mk b/make/kind.mk index 4c26b5b..12f9aab 100644 --- a/make/kind.mk +++ b/make/kind.mk @@ -3,12 +3,12 @@ ## Targets to help install and use kind for development https://kind.sigs.k8s.io -KIND_CLUSTER_NAME ?= kuadrant-local +KIND_CLUSTER_NAME ?= kuadrantctl-local .PHONY: kind-create-cluster -kind-create-cluster: kind ## Create the "kuadrant-local" kind cluster. +kind-create-cluster: kind ## Create the "kuadrantctl-local" kind cluster. $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config utils/kind-cluster.yaml .PHONY: kind-delete-cluster -kind-delete-cluster: kind ## Delete the "kuadrant-local" kind cluster. +kind-delete-cluster: kind ## Delete the "kuadrantctl-local" kind cluster. - $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) diff --git a/make/lint.mk b/make/lint.mk index 45842b0..c190494 100644 --- a/make/lint.mk +++ b/make/lint.mk @@ -1,7 +1,7 @@ GOLANGCI-LINT=$(PROJECT_PATH)/bin/golangci-lint $(GOLANGCI-LINT): mkdir -p $(PROJECT_PATH)/bin - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_PATH)/bin v1.41.1 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_PATH)/bin v1.55.2 .PHONY: golangci-lint golangci-lint: $(GOLANGCI-LINT) diff --git a/pkg/gatewayapi/http_route.go b/pkg/gatewayapi/http_route.go index 4558a19..68c2ea4 100644 --- a/pkg/gatewayapi/http_route.go +++ b/pkg/gatewayapi/http_route.go @@ -2,62 +2,134 @@ package gatewayapi import ( "github.com/getkin/kin-openapi/openapi3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/utils" ) -func HTTPRouteMatchesFromOAS(doc *openapi3.T) ([]gatewayapiv1beta1.HTTPRouteMatch, error) { - httpRouteMatches := []gatewayapiv1beta1.HTTPRouteMatch{} - pathMatchExactPath := gatewayapiv1beta1.PathMatchExact +func HTTPRouteObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + if doc.Info == nil { + return metav1.ObjectMeta{} + } - for path, pathItem := range doc.Paths { + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } - headers := []gatewayapiv1beta1.HTTPHeaderMatch{} - queryParams := []gatewayapiv1beta1.HTTPQueryParamMatch{} - headers, queryParams = addRuleMatcherFromParams(pathItem.Parameters, headers, queryParams) + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } - for verb, operation := range pathItem.Operations() { + if kuadrantInfoExtension.Route.Name == nil { + panic("info kuadrant extension route name not found") + } - headers, queryParams = addRuleMatcherFromParams(operation.Parameters, headers, queryParams) - - pathValue := path - httpMethod := gatewayapiv1beta1.HTTPMethod(verb) - httpRouteMatches = append(httpRouteMatches, gatewayapiv1beta1.HTTPRouteMatch{ - Method: &httpMethod, - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: &pathMatchExactPath, - Value: &pathValue, - }, - Headers: headers, - QueryParams: queryParams, - }) - } + om := metav1.ObjectMeta{ + Name: *kuadrantInfoExtension.Route.Name, + Labels: kuadrantInfoExtension.Route.Labels, } - return httpRouteMatches, nil + if kuadrantInfoExtension.Route.Namespace != nil { + om.Namespace = *kuadrantInfoExtension.Route.Namespace + } + + return om } -func addRuleMatcherFromParams(params openapi3.Parameters, headers []gatewayapiv1beta1.HTTPHeaderMatch, queryParams []gatewayapiv1beta1.HTTPQueryParamMatch) ([]gatewayapiv1beta1.HTTPHeaderMatch, []gatewayapiv1beta1.HTTPQueryParamMatch) { - headerMatchType := gatewayapiv1beta1.HeaderMatchExact - queryParamMatchExact := gatewayapiv1beta1.QueryParamMatchExact +func HTTPRouteGatewayParentRefsFromOAS(doc *openapi3.T) []gatewayapiv1beta1.ParentReference { + if doc.Info == nil { + return nil + } - for _, parameter := range params { - if !parameter.Value.Required { - continue - } + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } + + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } + + return kuadrantInfoExtension.Route.ParentRefs +} + +func HTTPRouteHostnamesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.Hostname { + if doc.Info == nil { + return nil + } + + kuadrantInfoExtension, err := utils.NewKuadrantOASInfoExtension(doc.Info) + if err != nil { + panic(err) + } - if parameter.Value.In == openapi3.ParameterInHeader { - headers = append(headers, gatewayapiv1beta1.HTTPHeaderMatch{ - Type: &headerMatchType, - Name: gatewayapiv1beta1.HTTPHeaderName(parameter.Value.Name), - }) + if kuadrantInfoExtension.Route == nil { + panic("info kuadrant extension route not found") + } + + return kuadrantInfoExtension.Route.Hostnames +} + +func HTTPRouteRulesFromOAS(doc *openapi3.T) []gatewayapiv1beta1.HTTPRouteRule { + // Current implementation, one rule per operation + // TODO(eguzki): consider about grouping operations as HTTPRouteMatch objects in fewer HTTPRouteRule objects + rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + // Paths + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) } - if parameter.Value.In == openapi3.ParameterInQuery { - queryParams = append(queryParams, gatewayapiv1beta1.HTTPQueryParamMatch{ - Type: &queryParamMatchExact, - Name: parameter.Value.Name, - }) + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { + // not enabled for the operation + continue + } + + // default backendrefs at the path level + backendRefs := kuadrantPathExtension.BackendRefs + if len(kuadrantOperationExtension.BackendRefs) > 0 { + backendRefs = kuadrantOperationExtension.BackendRefs + } + + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + rules = append(rules, buildHTTPRouteRule(basePath, path, pathItem, verb, operation, backendRefs, pathMatchType)) } } - return headers, queryParams + if len(rules) == 0 { + return nil + } + + return rules +} + +func buildHTTPRouteRule(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, backendRefs []gatewayapiv1beta1.HTTPBackendRef, pathMatchType gatewayapiv1beta1.PathMatchType) gatewayapiv1beta1.HTTPRouteRule { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) + + return gatewayapiv1beta1.HTTPRouteRule{ + BackendRefs: backendRefs, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + } } diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go new file mode 100644 index 0000000..0a87a46 --- /dev/null +++ b/pkg/kuadrantapi/authpolicy.go @@ -0,0 +1,254 @@ +package kuadrantapi + +import ( + "errors" + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +const ( + APIKeySecretLabel = "kuadrant.io/apikeys-by" +) + +func AuthPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) +} + +func buildAuthPolicyRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) + + return []kuadrantapiv1beta2.RouteSelector{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + }, + } +} + +func AuthPolicyTopRouteSelectorsFromOAS(doc *openapi3.T) []kuadrantapiv1beta2.RouteSelector { + routeSelectors := make([]kuadrantapiv1beta2.RouteSelector, 0) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { + // not enabled for the operation + //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) + continue + } + + // Get operation level security requirements or fallback to global security requirements + secRequirements := ptr.Deref(operation.Security, doc.Security) + + // Top RouteSelectors define the matching rules to call external auth service + // group together any routes that has at least one security requirement + if len(secRequirements) == 0 { + // no security + continue + } + + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + routeSelectors = append(routeSelectors, buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType)...) + } + } + + if len(routeSelectors) == 0 { + return nil + } + + return routeSelectors +} + +func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2.AuthenticationSpec { + authentication := make(map[string]kuadrantapiv1beta2.AuthenticationSpec) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + // Paths + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { + // not enabled for the operation + //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) + continue + } + + // Get operation level security requirements or fallback to global security requirements + secRequirements := ptr.Deref(operation.Security, doc.Security) + + if len(secRequirements) == 0 { + // no security + continue + } + + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + operationAuthentication := buildOperationAuthentication(doc, basePath, path, pathItem, verb, operation, pathMatchType, secRequirements) + + // Aggregate auth methods per operation + authentication = utils.MergeMaps(authentication, operationAuthentication) + } + } + + if len(authentication) == 0 { + return nil + } + + return authentication +} + +func buildOperationAuthentication(doc *openapi3.T, basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secRequirements openapi3.SecurityRequirements) map[string]kuadrantapiv1beta2.AuthenticationSpec { + // OpenAPI supports as security requirement to have multiple security schemes and ALL + // of the must be satisfied. + // From https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object + // Kuadrant does not support it yet: https://github.com/Kuadrant/authorino/issues/112 + // not supported (AND'ed) + // security: + // - petstore_api_key: [] + // petstore_oidc: [] + // supported (OR'ed) + // security: + // - petstore_api_key: [] + // - petstore_oidc: [] + + opAuth := make(map[string]kuadrantapiv1beta2.AuthenticationSpec, 0) + for _, secReq := range secRequirements { + if len(secReq) > 1 { + panic(errors.New("multiple schemes that require ALL must be satisfied, currently not supported")) + } + + extractSecReqItemName := func(sr openapi3.SecurityRequirement) string { + for secReqItemName := range sr { + return secReqItemName + } + + return "" + } + + secReqItemName := extractSecReqItemName(secReq) + + secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] + if !ok { + // should never happen. OpenAPI validation should detect this issue + continue + } + + if secScheme == nil || secScheme.Value == nil { + continue + } + + authName := fmt.Sprintf("%s_%s", utils.OpenAPIOperationName(path, verb, op), secReqItemName) + + // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 + switch secScheme.Value.Type { + case "openIdConnect": + opAuth[authName] = openIDAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, *secScheme.Value) + case "apiKey": + opAuth[authName] = apiKeyAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, secReqItemName, *secScheme.Value) + } + } + + if len(opAuth) == 0 { + return nil + } + + return opAuth +} + +func apiKeyAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secSchemeName string, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + // From https://github.com/Kuadrant/kuadrantctl/pull/46#issuecomment-1830278191 + // secScheme.In is required + // secScheme.Name is required + credentials := authorinoapi.Credentials{} + switch secScheme.In { + case "query": + credentials.QueryString = &authorinoapi.Named{Name: secScheme.Name} + case "header": + credentials.CustomHeader = &authorinoapi.CustomHeader{ + Named: authorinoapi.Named{Name: secScheme.Name}, + } + case "cookie": + credentials.Cookie = &authorinoapi.Named{Name: secScheme.Name} + } + + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + Credentials: credentials, + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + // label selector be like + // kuadrant.io/apikeys-by: ${SecuritySchemeName} + APIKeySecretLabel: secSchemeName, + }, + }, + }, + }, + }, + } +} + +func openIDAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + Jwt: &authorinoapi.JwtAuthenticationSpec{ + IssuerUrl: secScheme.OpenIdConnectUrl, + }, + }, + }, + } +} diff --git a/pkg/kuadrantapi/rate_limit_policy.go b/pkg/kuadrantapi/rate_limit_policy.go new file mode 100644 index 0000000..2509574 --- /dev/null +++ b/pkg/kuadrantapi/rate_limit_policy.go @@ -0,0 +1,93 @@ +package kuadrantapi + +import ( + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrantctl/pkg/gatewayapi" + "github.com/kuadrant/kuadrantctl/pkg/utils" +) + +func RateLimitPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { + return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) +} + +func RateLimitPolicyLimitsFromOAS(doc *openapi3.T) map[string]kuadrantapiv1beta2.Limit { + // Current implementation, one limit per operation + // TODO(eguzki): consider about grouping operations in fewer RLP limits + + limits := make(map[string]kuadrantapiv1beta2.Limit) + + basePath, err := utils.BasePathFromOpenAPI(doc) + if err != nil { + panic(err) + } + + // Paths + for path, pathItem := range doc.Paths { + kuadrantPathExtension, err := utils.NewKuadrantOASPathExtension(pathItem) + if err != nil { + panic(err) + } + + // Operations + for verb, operation := range pathItem.Operations() { + kuadrantOperationExtension, err := utils.NewKuadrantOASOperationExtension(operation) + if err != nil { + panic(err) + } + + if ptr.Deref(kuadrantOperationExtension.Disable, kuadrantPathExtension.IsDisabled()) { + // not enabled for the operation + //fmt.Printf("OUT not enabled: path: %s, method: %s\n", path, verb) + continue + } + + // default backendrefs at the path level + rateLimit := kuadrantPathExtension.RateLimit + if kuadrantOperationExtension.RateLimit != nil { + rateLimit = kuadrantOperationExtension.RateLimit + } + + if rateLimit == nil { + // no rate limit defined for this operation + //fmt.Printf("OUT no rate limit defined: path: %s, method: %s\n", path, verb) + continue + } + + // default pathMatchType at the path level + pathMatchType := ptr.Deref( + kuadrantOperationExtension.PathMatchType, + kuadrantPathExtension.GetPathMatchType(), + ) + + limitName := utils.OpenAPIOperationName(path, verb, operation) + + limits[limitName] = kuadrantapiv1beta2.Limit{ + RouteSelectors: buildLimitRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType), + When: rateLimit.When, + Counters: rateLimit.Counters, + Rates: rateLimit.Rates, + } + } + } + + if len(limits) == 0 { + return nil + } + + return limits +} + +func buildLimitRouteSelectors(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) []kuadrantapiv1beta2.RouteSelector { + match := utils.OpenAPIMatcherFromOASOperations(basePath, path, pathItem, verb, op, pathMatchType) + + return []kuadrantapiv1beta2.RouteSelector{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{match}, + }, + } +} diff --git a/pkg/utils/external_resource_reader.go b/pkg/utils/external_resource_reader.go index 6fdbc94..43dd6bf 100644 --- a/pkg/utils/external_resource_reader.go +++ b/pkg/utils/external_resource_reader.go @@ -16,17 +16,17 @@ limitations under the License. package utils import ( - "io/ioutil" + "io" "os" ) // ReadExternalResource reads data streams from external resources. Currently implemented: -// - '-' for STDIN +// - '@' for STDIN // - URLs (HTTP[S]) // - Files func ReadExternalResource(resource string) ([]byte, error) { - if resource == "-" { - return ioutil.ReadAll(os.Stdin) + if resource == "@" { + return io.ReadAll(os.Stdin) } if url, isURL := ParseURL(resource); isURL { @@ -34,5 +34,5 @@ func ReadExternalResource(resource string) ([]byte, error) { } // Defaulting to filepath - return ioutil.ReadFile(resource) + return os.ReadFile(resource) } diff --git a/pkg/utils/http_utils.go b/pkg/utils/http_utils.go index 4a90cd7..9d2195c 100644 --- a/pkg/utils/http_utils.go +++ b/pkg/utils/http_utils.go @@ -16,7 +16,7 @@ limitations under the License. package utils import ( - "io/ioutil" + "io" "net/http" "net/url" ) @@ -32,7 +32,7 @@ func ReadURL(location *url.URL) ([]byte, error) { if err != nil { return nil, err } - data, err := ioutil.ReadAll(resp.Body) + data, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err diff --git a/pkg/utils/k8s_utils.go b/pkg/utils/k8s_utils.go index 075b9d4..80e87d6 100644 --- a/pkg/utils/k8s_utils.go +++ b/pkg/utils/k8s_utils.go @@ -123,9 +123,9 @@ func IsDeploymentAvailable(dc *appsv1.Deployment) bool { return false } -func CheckDeploymentAvailable(k8sClient client.Client, key types.NamespacedName) (bool, error) { +func CheckDeploymentAvailable(ctx context.Context, k8sClient client.Client, key types.NamespacedName) (bool, error) { existingDeployment := &appsv1.Deployment{} - err := k8sClient.Get(context.Background(), key, existingDeployment) + err := k8sClient.Get(ctx, key, existingDeployment) if err != nil { if apierrors.IsNotFound(err) { logf.Log.Info("Deployment not available", "name", key.Name) diff --git a/pkg/utils/kuadrant_oas_extension_types.go b/pkg/utils/kuadrant_oas_extension_types.go new file mode 100644 index 0000000..699fef1 --- /dev/null +++ b/pkg/utils/kuadrant_oas_extension_types.go @@ -0,0 +1,110 @@ +package utils + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" + kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +type RouteObject struct { + Name *string `json:"name,omitempty"` + Namespace *string `json:"namespace,omitempty"` + Hostnames []gatewayapiv1beta1.Hostname `json:"hostnames,omitempty"` + ParentRefs []gatewayapiv1beta1.ParentReference `json:"parentRefs,omitempty"` + Labels map[string]string `json:"labels,omitempty"` +} + +type KuadrantOASInfoExtension struct { + Route *RouteObject `json:"route,omitempty"` +} + +func NewKuadrantOASInfoExtension(info *openapi3.Info) (*KuadrantOASInfoExtension, error) { + type KuadrantOASInfoObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASInfoExtension `json:"x-kuadrant,omitempty"` + } + + data, err := info.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASInfoObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + return x.Kuadrant, nil +} + +type KuadrantRateLimitExtension struct { + When []kuadrantapiv1beta2.WhenCondition `json:"when,omitempty"` + + Counters []kuadrantapiv1beta2.ContextSelector `json:"counters,omitempty"` + + Rates []kuadrantapiv1beta2.Rate `json:"rates,omitempty"` +} + +type KuadrantOASPathExtension struct { + Disable *bool `json:"disable,omitempty"` + PathMatchType *gatewayapiv1beta1.PathMatchType `json:"pathMatchType,omitempty"` + BackendRefs []gatewayapiv1beta1.HTTPBackendRef `json:"backendRefs,omitempty"` + RateLimit *KuadrantRateLimitExtension `json:"rate_limit,omitempty"` +} + +func (k *KuadrantOASPathExtension) IsDisabled() bool { + // Set default + return ptr.Deref(k.Disable, false) +} + +func (k *KuadrantOASPathExtension) GetPathMatchType() gatewayapiv1beta1.PathMatchType { + // Set default + return ptr.Deref(k.PathMatchType, gatewayapiv1beta1.PathMatchExact) +} + +func NewKuadrantOASPathExtension(pathItem *openapi3.PathItem) (*KuadrantOASPathExtension, error) { + type KuadrantOASPathObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASPathExtension `json:"x-kuadrant,omitempty"` + } + + data, err := pathItem.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASPathObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + kuadrantExtension := ptr.Deref(x.Kuadrant, KuadrantOASPathExtension{}) + + return &kuadrantExtension, nil +} + +type KuadrantOASOperationExtension KuadrantOASPathExtension + +func NewKuadrantOASOperationExtension(operation *openapi3.Operation) (*KuadrantOASOperationExtension, error) { + type KuadrantOASOperationObject struct { + // Kuadrant extension + Kuadrant *KuadrantOASOperationExtension `json:"x-kuadrant,omitempty"` + } + + data, err := operation.MarshalJSON() + if err != nil { + return nil, err + } + + var x KuadrantOASOperationObject + if err := json.Unmarshal(data, &x); err != nil { + return nil, err + } + + kuadrantExtension := ptr.Deref(x.Kuadrant, KuadrantOASOperationExtension{}) + + return &kuadrantExtension, nil +} diff --git a/pkg/utils/maps.go b/pkg/utils/maps.go new file mode 100644 index 0000000..c423dc4 --- /dev/null +++ b/pkg/utils/maps.go @@ -0,0 +1,12 @@ +package utils + +func MergeMaps[K comparable, V any](MyMap1 map[K]V, MyMap2 map[K]V) map[K]V { + merged := make(map[K]V) + for key, val := range MyMap1 { + merged[key] = val + } + for key, val := range MyMap2 { + merged[key] = val + } + return merged +} diff --git a/pkg/utils/oas3.go b/pkg/utils/oas3.go deleted file mode 100644 index a9d0fa2..0000000 --- a/pkg/utils/oas3.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "fmt" - "regexp" - "strings" - - "github.com/getkin/kin-openapi/openapi3" - "k8s.io/apimachinery/pkg/util/validation" -) - -var ( - // NonAlphanumRegexp not alphanumeric - NonAlphanumRegexp = regexp.MustCompile(`[^0-9A-Za-z]`) -) - -func K8sNameFromOpenAPITitle(obj *openapi3.T) (string, error) { - openapiTitle := obj.Info.Title - openapiTitleToLower := strings.ToLower(openapiTitle) - objName := NonAlphanumRegexp.ReplaceAllString(openapiTitleToLower, "") - - // DNS Subdomain Names - // If the name would be part of some label, validation would be DNS Label Names (validation.IsDNS1123Label) - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ - errStrings := validation.IsDNS1123Subdomain(objName) - if len(errStrings) > 0 { - errStr := strings.Join(errStrings, ",") - return "", fmt.Errorf("k8s name from OAS not valid: %s", errStr) - } - return objName, nil -} - -func ValidateOAS3(docRaw []byte) error { - openapiLoader := openapi3.NewLoader() - doc, err := openapiLoader.LoadFromData(docRaw) - if err != nil { - return err - } - - err = doc.Validate(openapiLoader.Context) - if err != nil { - return fmt.Errorf("OpenAPI validation error: %w", err) - } - - return nil -} - -func OpenAPIOperationSecRequirements(oasDoc *openapi3.T, operation *openapi3.Operation) *openapi3.SecurityRequirements { - if operation.Security == nil { - return &oasDoc.Security - } - return operation.Security -} diff --git a/pkg/utils/oas_utils.go b/pkg/utils/oas_utils.go new file mode 100644 index 0000000..46c55c1 --- /dev/null +++ b/pkg/utils/oas_utils.go @@ -0,0 +1,193 @@ +package utils + +import ( + "bytes" + "fmt" + "html/template" + "net/url" + "regexp" + + "github.com/getkin/kin-openapi/openapi3" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var ( + // NonWordCharRegexp not word characters (== [^0-9A-Za-z_]) + NonWordCharRegexp = regexp.MustCompile(`\W`) + // TemplateRegexp used to render openapi server URLs + TemplateRegexp = regexp.MustCompile(`{([\w]+)}`) + // LastSlashRegexp matches the last slash + LastSlashRegexp = regexp.MustCompile(`/$`) +) + +func FirstServerFromOpenAPI(obj *openapi3.T) *openapi3.Server { + if obj == nil { + return nil + } + + // take only first server + // From https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md + // If the servers property is not provided, or is an empty array, the default value would be a Server Object with a url value of /. + server := &openapi3.Server{ + URL: `/`, + Variables: map[string]*openapi3.ServerVariable{}, + } + + // Current constraint: only read the first item when there are multiple servers + // Maybe this should be user provided setting + if len(obj.Servers) > 0 { + server = obj.Servers[0] + } + + return server +} + +func RenderOpenAPIServerURLStr(server *openapi3.Server) (string, error) { + if server == nil { + return "", nil + } + + data := &struct { + Data map[string]string + }{ + map[string]string{}, + } + + for variableName, variable := range server.Variables { + data.Data[variableName] = variable.Default + } + + urlTemplate := TemplateRegexp.ReplaceAllString(server.URL, `{{ index .Data "$1" }}`) + + tObj, err := template.New(server.URL).Parse(urlTemplate) + if err != nil { + return "", err + } + + var tpl bytes.Buffer + err = tObj.Execute(&tpl, data) + if err != nil { + return "", err + } + + return tpl.String(), nil +} + +func RenderOpenAPIServerURL(server *openapi3.Server) (*url.URL, error) { + serverURLStr, err := RenderOpenAPIServerURLStr(server) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(serverURLStr) + if err != nil { + return nil, err + } + + return serverURL, nil +} + +func BasePathFromOpenAPI(obj *openapi3.T) (string, error) { + server := FirstServerFromOpenAPI(obj) + serverURL, err := RenderOpenAPIServerURL(server) + if err != nil { + return "", err + } + + return serverURL.Path, nil +} + +func OpenAPIMatcherFromOASOperations(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType) gatewayapiv1beta1.HTTPRouteMatch { + // remove the last slash of the Base Path + sanitizedBasePath := LastSlashRegexp.ReplaceAllString(basePath, "") + + // According OAS 3.0: path MUST begin with a slash + matchPath := fmt.Sprintf("%s%s", sanitizedBasePath, path) + + pathHeadersMatch := headersMatchFromParams(pathItem.Parameters) + operationHeadersMatch := headersMatchFromParams(op.Parameters) + + // default headersMatch at the path level + headersMatch := pathHeadersMatch + if len(operationHeadersMatch) > 0 { + headersMatch = operationHeadersMatch + } + + pathQueryParamsMatch := queryParamsMatchFromParams(pathItem.Parameters) + operationQueryParamsMatch := queryParamsMatchFromParams(op.Parameters) + + // default queryParams at the path level + queryParams := pathQueryParamsMatch + if len(operationQueryParamsMatch) > 0 { + queryParams = operationQueryParamsMatch + } + + return gatewayapiv1beta1.HTTPRouteMatch{ + Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod(verb)}[0], + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: &pathMatchType, + Value: &[]string{matchPath}[0], + }, + Headers: headersMatch, + QueryParams: queryParams, + } +} + +func headersMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPHeaderMatch { + matches := make([]gatewayapiv1beta1.HTTPHeaderMatch, 0) + + for _, parameter := range params { + if !parameter.Value.Required { + continue + } + + if parameter.Value.In == openapi3.ParameterInHeader { + matches = append(matches, gatewayapiv1beta1.HTTPHeaderMatch{ + Type: &[]gatewayapiv1beta1.HeaderMatchType{gatewayapiv1beta1.HeaderMatchExact}[0], + Name: gatewayapiv1beta1.HTTPHeaderName(parameter.Value.Name), + }) + } + } + + if len(matches) == 0 { + return nil + } + + return matches +} + +func queryParamsMatchFromParams(params openapi3.Parameters) []gatewayapiv1beta1.HTTPQueryParamMatch { + matches := make([]gatewayapiv1beta1.HTTPQueryParamMatch, 0) + + for _, parameter := range params { + if !parameter.Value.Required { + continue + } + + if parameter.Value.In == openapi3.ParameterInQuery { + matches = append(matches, gatewayapiv1beta1.HTTPQueryParamMatch{ + Type: &[]gatewayapiv1beta1.QueryParamMatchType{gatewayapiv1beta1.QueryParamMatchExact}[0], + Name: parameter.Value.Name, + }) + } + } + + if len(matches) == 0 { + return nil + } + + return matches + +} + +func OpenAPIOperationName(path, opVerb string, op *openapi3.Operation) string { + sanitizedPath := NonWordCharRegexp.ReplaceAllString(path, "") + + name := fmt.Sprintf("%s%s", opVerb, sanitizedPath) + + if op.OperationID != "" { + name = op.OperationID + } + + return name +} diff --git a/pkg/utils/utils.coverprofile b/pkg/utils/utils.coverprofile deleted file mode 100644 index dbcac2a..0000000 --- a/pkg/utils/utils.coverprofile +++ /dev/null @@ -1,68 +0,0 @@ -mode: atomic -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:27.60,28.21 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:28.21,30.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:32.2,32.45 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:32.45,34.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/external_resource_reader.go:37.2,37.34 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:25.44,28.2 2 9 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:30.49,32.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:32.16,34.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:35.2,37.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:37.16,39.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/http_utils.go:40.2,40.18 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:32.81,34.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:34.9,36.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:38.2,40.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:40.16,42.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:44.2,44.37 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:44.37,46.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:49.2,51.9 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:51.9,53.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:54.2,55.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:55.16,57.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:59.2,62.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:62.16,64.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:66.2,69.9 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:69.9,71.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:73.2,75.12 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:78.77,80.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:80.9,82.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:83.2,87.16 4 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:87.16,88.37 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:88.37,91.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:91.9,93.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:95.2,95.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:98.73,100.9 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:100.9,102.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:103.2,107.46 4 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:107.46,110.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:111.2,111.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:116.56,118.43 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:118.43,119.99 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:119.99,121.4 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:123.2,123.14 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:126.96,129.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:129.16,130.32 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:130.32,133.4 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:135.3,135.20 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:138.2,138.48 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:138.48,143.3 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/k8s_utils.go:145.2,146.18 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:17.63,26.25 5 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:26.25,29.3 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:30.2,30.21 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:33.40,36.16 3 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:36.16,38.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:40.2,41.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:41.16,43.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:45.2,45.12 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:48.120,49.31 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:49.31,51.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/oas3.go:52.2,52.27 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:10.26,12.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:12.16,14.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:16.2,17.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:17.16,19.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:21.2,22.16 2 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:22.16,24.3 1 0 -github.com/kuadrant/kuadrantctl/pkg/utils/scheme.go:26.2,26.12 1 0 diff --git a/utils/istio-operator.yaml b/utils/istio-operator.yaml new file mode 100644 index 0000000..05db21c --- /dev/null +++ b/utils/istio-operator.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + profile: default + namespace: istio-system + components: + base: + enabled: true + cni: + enabled: false + egressGateways: + - enabled: false + name: istio-egressgateway + ingressGateways: + - enabled: true + name: istio-ingressgateway + k8s: + service: + type: NodePort + ports: + - name: status-port + port: 15021 + protocol: TCP + targetPort: 15021 + - name: http2 + port: 80 + protocol: TCP + targetPort: 8080 + nodePort: 30950 + - name: https + port: 443 + protocol: TCP + targetPort: 8443 + nodePort: 30951 + resources: + requests: + cpu: "0" + pilot: + enabled: true + k8s: + resources: + requests: + cpu: "0" + values: + pilot: + autoscaleEnabled: false + gateways: + istio-ingressgateway: + type: ClusterIP + autoscaleEnabled: false + global: + istioNamespace: istio-system diff --git a/utils/kind-cluster.yaml b/utils/kind-cluster.yaml index 8e16566..a395098 100644 --- a/utils/kind-cluster.yaml +++ b/utils/kind-cluster.yaml @@ -5,3 +5,9 @@ nodes: - role: control-plane # port forward 80 on the host to 80 on this node image: kindest/node:v1.27.3 + # port forward 80 on the host to 80 on this node + extraPortMappings: + - containerPort: 30950 + hostPort: 9080 + - containerPort: 30951 + hostPort: 9443