├── docs ├── .gitignore ├── summertime-kong.yml ├── lint │ ├── https.yaml │ └── version.yaml ├── mock-a-rena-oas.yml └── security-o2k.md ├── main.go ├── merge ├── merge_testfiles │ ├── badenvvar.yml │ ├── badversion.yml │ ├── transform_false.yml │ ├── file4.yml │ ├── file5.yml │ ├── file6.yml │ ├── file7.yml │ ├── file2.yml │ ├── file3.yml │ ├── test3_expected.json │ ├── test4_expected.json │ ├── test1_expected.json │ ├── test2_expected.json │ └── file1.yml └── merge_suite_test.go ├── openapi2kong ├── oas3_testfiles │ ├── invalid │ │ ├── no-paths.yaml │ │ ├── unsupported-security-type.yaml │ │ ├── missing-security-scheme.yaml │ │ ├── multiple-security-requirements.yaml │ │ └── multiple-security-schemes.yaml │ ├── 20-empty-paths.yaml │ ├── 03a-server-no-trailing-slash.yaml │ ├── 04a-servers-upstream.yaml │ ├── 04-servers-upstream.yaml │ ├── 23-no-servers-at-all.yaml │ ├── 21-no-doc-servers.yaml │ ├── 22-doc-path-and-ops-servers.yaml │ ├── 03-servers-defaults.yaml │ ├── 06a-upstream-defaults.yaml │ ├── 18-request-validator-plugin-path-params-outside-ops.yaml │ ├── 016a-security-oidc-generation-no-component-extension.yaml │ ├── 23-no-servers-at-all.expected.json │ ├── 03-servers-defaults.expected.json │ ├── 04a-servers-upstream.expected.json │ ├── 19-request-validator-plugin-op-params-override-path-params.yaml │ ├── 07a-service-defaults.expected.json │ ├── 03a-server-no-trailing-slash.expected.json │ ├── 20-empty-paths.expected.json │ ├── 11-references.yaml │ ├── 07a-service-defaults.yaml │ ├── 15-circular-requestBody-schema.yaml │ ├── 25-routes-with-headers.yaml │ ├── 05-service-defaults-document.yaml │ ├── 01-names-inferred.yaml │ ├── 02-names-set.yaml │ ├── 06-upstream-defaults-document.yaml │ ├── _fixup_expected.sh │ ├── 016a-security-oidc-generation-no-component-extension.expected.json │ ├── 07-service-defaults-overrides.yaml │ ├── 14-no-request-validator-plugin.yaml │ ├── 04-servers-upstream.expected.json │ ├── 20-ignore-circular-references.circular-yaml │ ├── 06a-upstream-defaults.expected.json │ ├── 11-references.expected.json │ ├── 21-nullable-path-param.expected.json │ ├── 10-generic-plugins-multi-service.yaml │ ├── 21-no-doc-servers.expected.json │ ├── 15-circular-requestBody-schema.expected.json │ ├── 08-route-defaults-overrides.yaml │ ├── 20-ignore-circular-references.expected_no_circular.json │ ├── 20-ignore-circular-references.generated_no_circular.json │ ├── 19-request-validator-plugin-op-params-override-path-params.expected.json │ ├── 14-no-request-validator-plugin.expected.json │ ├── 21-nullable-path-param.yaml │ ├── 17-request-validator-plugin-oneOf-usage.yaml │ ├── 09a-plugins-with-consumers.yaml │ ├── 18-request-validator-plugin-path-params-outside-ops.expected.json │ ├── 09-generic-plugins.yaml │ ├── 08-route-defaults-overrides.expected.json │ ├── 05-service-defaults-document.expected.json │ ├── 12-path-parameter-regex.yaml │ ├── 22-doc-path-and-ops-servers.expected.json │ ├── 13-request-validator-plugin.yaml │ ├── 09-generic-plugins.expected.json │ ├── 12-path-parameter-regex.expected_inso.json │ ├── 16-security-oidc-generation.yaml │ ├── 09a-plugins-with-consumers.expected.json │ ├── 02-names-set.expected_inso.json │ ├── 07-service-defaults-overrides.expected.json │ ├── 01-names-inferred.expected_inso.json │ └── 12-path-parameter-regex.expected.json ├── openapi2kong_suite_test.go ├── utils_test.go ├── jsonschema.go ├── service_test.go └── utils.go ├── patch ├── patch_suite_test.go ├── patch-file.yml ├── deckpatchfile.go └── patchcli.go ├── tags └── tags_suite_test.go ├── plugins ├── plugins_suite_test.go ├── plugin-file.yml ├── pluginfile_test.go └── pluginfile.go ├── namespace ├── namespace_suite_test.go ├── namespace_host_test.go └── namespace_host.go ├── deckformat ├── deckformat_suite_test.go ├── deckformat.go ├── history.go └── deckformat_test.go ├── filebasics ├── filebasics_suite_test.go └── filebasics_test.go ├── jsonbasics └── jsonbasics_suite_test.go ├── yamlbasics ├── yamlbasics_suite_test.go ├── nodeset.go ├── selectors.go └── selectors_test.go ├── .github ├── dependabot.yml └── workflows │ ├── release.yaml │ ├── test.yml │ └── golangci-lint.yml ├── .editorconfig ├── .goreleaser.yml ├── .vscode └── launch.json ├── .gitignore ├── cmd ├── version.go ├── root.go ├── merge.go ├── addtags.go └── listtags.go ├── Makefile ├── go.mod ├── .golangci.yml ├── logbasics └── logbasics.go └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | mock-a-rena-kong.yml 2 | kong-combined.yml 3 | kong.yml 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/kong/go-apiops/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /merge/merge_testfiles/badenvvar.yml: -------------------------------------------------------------------------------- 1 | # we require format versions to be prefixed with DECK_ 2 | _format_version: "${{ env "FORMAT_VERSION" }}" -------------------------------------------------------------------------------- /merge/merge_testfiles/badversion.yml: -------------------------------------------------------------------------------- 1 | # this version differs by major, and hence is incompatible 2 | _format_version: "1.0" 3 | _transform: true 4 | -------------------------------------------------------------------------------- /merge/merge_testfiles/transform_false.yml: -------------------------------------------------------------------------------- 1 | # transform 'false' cannot be merged with 'true'/null 2 | _format_version: "1.0" 3 | _transform: false 4 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/invalid/no-paths.yaml: -------------------------------------------------------------------------------- 1 | # openapi2kong.go should fail with an actionable error message when .paths is missing 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server1.com/ 9 | -------------------------------------------------------------------------------- /merge/merge_suite_test.go: -------------------------------------------------------------------------------- 1 | package merge_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestMerge(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Merge Suite") 13 | } 14 | -------------------------------------------------------------------------------- /patch/patch_suite_test.go: -------------------------------------------------------------------------------- 1 | package patch_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestPatch(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Patch Suite") 13 | } 14 | -------------------------------------------------------------------------------- /tags/tags_suite_test.go: -------------------------------------------------------------------------------- 1 | package tags_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFilebasics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Tags Suite") 13 | } 14 | -------------------------------------------------------------------------------- /plugins/plugins_suite_test.go: -------------------------------------------------------------------------------- 1 | package plugins_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFilebasics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Plugins Suite") 13 | } 14 | -------------------------------------------------------------------------------- /namespace/namespace_suite_test.go: -------------------------------------------------------------------------------- 1 | package namespace_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestNamespace(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Namespace Suite") 13 | } 14 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/20-empty-paths.yaml: -------------------------------------------------------------------------------- 1 | # openapi2kong.go should not fail when .paths is empty 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server1.com/ 9 | - url: https://server2.com/ 10 | paths: [] 11 | -------------------------------------------------------------------------------- /deckformat/deckformat_suite_test.go: -------------------------------------------------------------------------------- 1 | package deckformat_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestDeckformat(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Deckformat Suite") 13 | } 14 | -------------------------------------------------------------------------------- /filebasics/filebasics_suite_test.go: -------------------------------------------------------------------------------- 1 | package filebasics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFilebasics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Filebasics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /jsonbasics/jsonbasics_suite_test.go: -------------------------------------------------------------------------------- 1 | package jsonbasics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestJSONbasics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "JSONbasics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /yamlbasics/yamlbasics_suite_test.go: -------------------------------------------------------------------------------- 1 | package yamlbasics_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestYamlbasics(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "YamlBasics Suite") 13 | } 14 | -------------------------------------------------------------------------------- /openapi2kong/openapi2kong_suite_test.go: -------------------------------------------------------------------------------- 1 | package openapi2kong_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestOpenapi2kong(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "OpenAPI2Kong Suite") 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: daily 11 | - package-ecosystem: docker 12 | directory: / 13 | schedule: 14 | interval: daily 15 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file4.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file4 2 | 3 | _format_version: "3.0" 4 | 5 | services: 6 | - connect_timeout: ${{ env "DECK_SVC_CONNECT_TIMEOUT" }} 7 | host: mockbin.org 8 | name: svc1 9 | port: 80 10 | protocol: http 11 | read_timeout: ${{ env "DECK_SVC_READ_TIMEOUT" }} 12 | retries: ${{ env "DECK_SVC_RETRIES" }} 13 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file5.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file5 2 | 3 | _format_version: "3.0" 4 | 5 | services: 6 | - connect_timeout: ${{ env "DECK_SVC_CONNECT_TIMEOUT" }} 7 | host: mockbin.org 8 | name: svc2 9 | port: 80 10 | protocol: http 11 | read_timeout: ${{ env "DECK_SVC_READ_TIMEOUT" }} 12 | retries: ${{ env "DECK_SVC_RETRIES" }} 13 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file6.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file6 2 | 3 | _format_version: "${{ env "DECK_FORMAT_VERSION" }}" 4 | 5 | services: 6 | - connect_timeout: ${{ env "DECK_SVC_CONNECT_TIMEOUT" }} 7 | host: mockbin.org 8 | name: svc1 9 | port: 80 10 | protocol: http 11 | read_timeout: ${{ env "DECK_SVC_READ_TIMEOUT" }} 12 | retries: ${{ env "DECK_SVC_RETRIES" }} 13 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file7.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file7 2 | 3 | _format_version: "${{ env "DECK_FORMAT_VERSION" }}" 4 | 5 | services: 6 | - connect_timeout: ${{ env "DECK_SVC_CONNECT_TIMEOUT" }} 7 | host: mockbin.org 8 | name: svc2 9 | port: 80 10 | protocol: http 11 | read_timeout: ${{ env "DECK_SVC_READ_TIMEOUT" }} 12 | retries: ${{ env "DECK_SVC_RETRIES" }} 13 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file2.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file2 2 | 3 | # different minor version 4 | _format_version: "3.1" 5 | #_transform: true this should default to true if omitted 6 | 7 | services: 8 | - name: file2-service-1 9 | url: http://example.com 10 | - name: file2-service-2 11 | url: https://example.org 12 | 13 | routes: 14 | - name: file2-route-1 15 | hosts: ["hello.com"] 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.go] 10 | indent_style = tab 11 | indent_size = 2 12 | 13 | [*.md] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.yaml] 22 | indent_style = space 23 | 24 | [Makefile] 25 | indent_style = tab 26 | indent_size = 4 27 | -------------------------------------------------------------------------------- /merge/merge_testfiles/file3.yml: -------------------------------------------------------------------------------- 1 | _comment: this is file3 2 | 3 | # If omitted, defaults to "0.0" in the order, but compatible with all versions. 4 | # Officially this is a required field. 5 | #_format_version: "3.0" 6 | 7 | _transform: true 8 | 9 | services: 10 | - name: file3-service-1 11 | url: http://example.com 12 | - name: file3-service-2 13 | url: https://example.org 14 | 15 | routes: 16 | - name: file3-route-1 17 | hosts: ["hello.com"] 18 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/03a-server-no-trailing-slash.yaml: -------------------------------------------------------------------------------- 1 | # servers[0].url doesn't have a trailing slash 2 | # if service.url is only a hostname, with no path nor 3 | # trailing / 4 | 5 | openapi: 3.0.3 6 | 7 | info: 8 | title: Server with no trailing slash 9 | version: v1 10 | servers: 11 | - url: "https://example.com" 12 | 13 | paths: 14 | /path: 15 | get: 16 | operationId: myId 17 | responses: 18 | "200": 19 | description: OK 20 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/04a-servers-upstream.yaml: -------------------------------------------------------------------------------- 1 | # properly parses ports, see issue #104 2 | # https://github.com/Kong/go-apiops/issues/104 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server2.com:65000/ 9 | paths: 10 | /: 11 | get: 12 | operationId: OpsId 13 | summary: List API versions 14 | responses: 15 | '200': 16 | description: |- 17 | 200 response 18 | -------------------------------------------------------------------------------- /docs/summertime-kong.yml: -------------------------------------------------------------------------------- 1 | _format_version: "3.0" 2 | services: 3 | - host: mockbin.org 4 | id: b1525aee-d304-11ed-afa1-0242ac120002 5 | name: summer-time 6 | path: /requests 7 | plugins: [] 8 | port: 443 9 | protocol: https 10 | routes: 11 | - id: b7d87736-d304-11ed-afa1-0242ac120002 12 | methods: 13 | - GET 14 | name: summer-time_get 15 | paths: 16 | - ~/summer-time$ 17 | plugins: [] 18 | regex_priority: 200 19 | strip_path: false 20 | tags: [] 21 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/04-servers-upstream.yaml: -------------------------------------------------------------------------------- 1 | # if the servers block has more than 1 url, then an upstream 2 | # is generated with a target for each sesrver 3 | 4 | openapi: '3.0.0' 5 | info: 6 | title: Simple API overview 7 | version: v2 8 | servers: 9 | - url: https://server1.com/ 10 | - url: https://server2.com/ 11 | paths: 12 | /: 13 | get: 14 | operationId: OpsId 15 | summary: List API versions 16 | responses: 17 | '200': 18 | description: |- 19 | 200 response 20 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/23-no-servers-at-all.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Simple API 4 | version: 1.0.0 5 | paths: 6 | /hello: 7 | get: 8 | summary: Say Hello 9 | responses: 10 | '200': 11 | description: Successful response 12 | content: 13 | application/json: 14 | schema: 15 | type: object 16 | properties: 17 | message: 18 | type: string 19 | example: Hello, world! -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | skip_upload: true 3 | 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | - darwin 10 | - windows 11 | - freebsd 12 | goarch: 13 | - amd64 14 | - arm64 15 | ignore: 16 | - goos: freebsd 17 | goarch: arm64 18 | - goos: windows 19 | goarch: arm64 20 | flags: 21 | - -trimpath 22 | ldflags: 23 | - -s -w -X github.com/kong/go-apiops/cmd.VERSION={{ .Tag }} -X github.com/kong/go-apiops/cmd.COMMIT={{ .ShortCommit }} 24 | snapshot: 25 | name_template: "{{ .Tag }}" 26 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/21-no-doc-servers.yaml: -------------------------------------------------------------------------------- 1 | # openapi2kong.go should not create an extra service 2 | # when there are no top-level doc servers defined. 3 | 4 | openapi: 3.0.0 5 | info: 6 | title: Multi-Service API 7 | version: 1.0.0 8 | 9 | paths: 10 | /users: 11 | get: 12 | servers: 13 | - url: https://user-service.example.com 14 | responses: 15 | '200': 16 | 17 | /orders: 18 | post: 19 | servers: 20 | - url: https://order-service.example.com 21 | responses: 22 | '201': -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/invalid/unsupported-security-type.yaml: -------------------------------------------------------------------------------- 1 | # This should fail with unsupported security scheme type when OIDC is enabled 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Invalid security - unsupported scheme type 6 | version: v1 7 | servers: 8 | - url: https://server1.com/ 9 | security: 10 | - apiKey: [] 11 | components: 12 | securitySchemes: 13 | apiKey: 14 | type: apiKey # Not openIdConnect 15 | in: header 16 | name: X-API-Key 17 | paths: 18 | /test: 19 | get: 20 | responses: 21 | "200": 22 | description: OK -------------------------------------------------------------------------------- /plugins/plugin-file.yml: -------------------------------------------------------------------------------- 1 | # This file describes the possible format of the plugin operations by 2 | # the CLI command `deck add-plugin` etc. 3 | 4 | _format_version: "1.0" 5 | 6 | # plugins is an array of plugin operations, to be applied in order 7 | # for now only "add" is supported 8 | add-plugins: 9 | - selectors: # defaults to ["$"] 10 | - "$.services[*]" 11 | overwrite: false # defaults to false 12 | plugins: 13 | - name: key-auth 14 | config: 15 | key_names: [api_key, apikey] 16 | key_in_body: false 17 | hide_credentials: true 18 | -------------------------------------------------------------------------------- /docs/lint/https.yaml: -------------------------------------------------------------------------------- 1 | # This example rule will validate a decK file 2 | # ensuring that all Kong Gateway services 3 | # utilize https protocol only 4 | rules: 5 | service-https-check: 6 | description: "Ensure https usage in Kong GW Services" 7 | # From the root, select all services key 8 | given: $.services[*].protocol 9 | # Fatal error if rule is violated 10 | severity: error 11 | then: 12 | # Match on a regex pattern for the value selected from 13 | # JSON Path in the given field 14 | function: pattern 15 | functionOptions: 16 | match: "^https$" 17 | -------------------------------------------------------------------------------- /docs/lint/version.yaml: -------------------------------------------------------------------------------- 1 | # This example rule will validate the decK file version 2 | # which is specified in the _format_version key at the root 3 | # of the document 4 | rules: 5 | version-check: 6 | description: "Validate version 3.1 for decK files" 7 | # From the root, select the _format_version key 8 | given: $._format_version 9 | # Fatal error if rule is violated 10 | severity: error 11 | then: 12 | # Match on a regex pattern for the value selected from 13 | # JSON Path in the given field 14 | function: pattern 15 | functionOptions: 16 | match: "^3.1$" 17 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/invalid/missing-security-scheme.yaml: -------------------------------------------------------------------------------- 1 | # This should fail with missing security scheme reference when OIDC is enabled 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Invalid security - missing scheme reference 6 | version: v1 7 | servers: 8 | - url: https://server1.com/ 9 | security: 10 | - nonExistentScheme: [] # References a scheme that doesn't exist 11 | components: 12 | securitySchemes: 13 | existingScheme: 14 | type: openIdConnect 15 | openIdConnectUrl: https://example.com/.well-known/openid_configuration 16 | paths: 17 | /test: 18 | get: 19 | responses: 20 | "200": 21 | description: OK -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/22-doc-path-and-ops-servers.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Multi-Service API 4 | version: 1.0.0 5 | 6 | # this would create a top-level 7 | # doc service as well as an upstream 8 | servers: 9 | - url: https://server1.com/ 10 | - url: https://server2.com/ 11 | 12 | paths: 13 | /users: 14 | get: 15 | # this would create another service 16 | servers: 17 | - url: https://user-service.example.com 18 | responses: 19 | '200': 20 | 21 | /orders: 22 | # this would create another service 23 | servers: 24 | - url: https://order-service.example.com 25 | post: 26 | responses: 27 | '201': -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/03-servers-defaults.yaml: -------------------------------------------------------------------------------- 1 | # if the servers block has no hostname, then the default 'localhost' 2 | # should be injected. 3 | # OAS default for the url is "/" but that is useless for Kong, since there 4 | # would be no host. Using 'localhost' is just slightly less useless :/ 5 | # 6 | # scheme default: https 7 | # hostname default: localhost 8 | # port default: based on scheme 9 | 10 | openapi: '3.0.0' 11 | info: 12 | title: Simple API overview 13 | version: v2 14 | servers: 15 | - url: / 16 | paths: 17 | /: 18 | get: 19 | operationId: OpsId 20 | summary: List API versions 21 | responses: 22 | '200': 23 | description: |- 24 | 200 response 25 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/invalid/multiple-security-requirements.yaml: -------------------------------------------------------------------------------- 1 | # This should fail with multiple security requirements when OIDC is enabled 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Invalid security - multiple requirements 6 | version: v1 7 | servers: 8 | - url: https://server1.com/ 9 | security: 10 | - oauth2: [] 11 | - apiKey: [] # Multiple security requirements 12 | components: 13 | securitySchemes: 14 | oauth2: 15 | type: openIdConnect 16 | openIdConnectUrl: https://example.com/.well-known/openid_configuration 17 | apiKey: 18 | type: apiKey 19 | in: header 20 | name: X-API-Key 21 | paths: 22 | /test: 23 | get: 24 | responses: 25 | "200": 26 | description: OK -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "main.go", 13 | "args": ["patch", 14 | "-s", 15 | "kong.yml", 16 | "--selector", 17 | "$..services[*]", 18 | "--value", 19 | "read_timeout:10000" 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/invalid/multiple-security-schemes.yaml: -------------------------------------------------------------------------------- 1 | # This should fail with multiple security schemes within a requirement when OIDC is enabled 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Invalid security - multiple schemes in requirement 6 | version: v1 7 | servers: 8 | - url: https://server1.com/ 9 | security: 10 | - oauth2: [] 11 | apiKey: [] # Multiple schemes in one requirement (AND logic) 12 | components: 13 | securitySchemes: 14 | oauth2: 15 | type: openIdConnect 16 | openIdConnectUrl: https://example.com/.well-known/openid_configuration 17 | apiKey: 18 | type: apiKey 19 | in: header 20 | name: X-API-Key 21 | paths: 22 | /test: 23 | get: 24 | responses: 25 | "200": 26 | description: OK -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # distributions created by goreleaser 18 | dist/ 19 | 20 | # generated test files 21 | openapi2kong/oas3_testfiles/*.generated.json 22 | openapi2kong/oas3_testfiles/*.generated_inso.json 23 | merge/merge_testfiles/*_generated.json 24 | 25 | # the binary 26 | go-apiops 27 | 28 | # any manually created test files 29 | kong.yaml 30 | kong.yml 31 | 32 | # environment and log files, which might come from something like kong quickstart scripts 33 | *.env 34 | *.log 35 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/06a-upstream-defaults.yaml: -------------------------------------------------------------------------------- 1 | # Some of the generated fields for upstreams should be overridable 2 | # using the defaults. 3 | # These are the order in which we should take the values: 4 | # - upstream.targets: x-kong-upstream-defaults -> list of targets gotten from servers block -> default empty array 5 | 6 | openapi: '3.0.0' 7 | info: 8 | title: Simple API overview 9 | version: v2 10 | servers: 11 | - url: https://server1.com/anything 12 | 13 | x-kong-upstream-defaults: 14 | targets: 15 | - target: 'my-server1.com:443' 16 | - target: 'my-server2.com:443' 17 | 18 | paths: 19 | /path1: 20 | get: 21 | operationId: uses-doc-service 22 | summary: List API versions 23 | responses: 24 | '200': 25 | description: |- 26 | 200 response 27 | -------------------------------------------------------------------------------- /merge/merge_testfiles/test3_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "this is file5", 3 | "_format_version": "3.0", 4 | "services": [ 5 | { 6 | "connect_timeout": "${{ env \"DECK_SVC_CONNECT_TIMEOUT\" }}", 7 | "host": "mockbin.org", 8 | "name": "svc1", 9 | "port": 80, 10 | "protocol": "http", 11 | "read_timeout": "${{ env \"DECK_SVC_READ_TIMEOUT\" }}", 12 | "retries": "${{ env \"DECK_SVC_RETRIES\" }}" 13 | }, 14 | { 15 | "connect_timeout": "${{ env \"DECK_SVC_CONNECT_TIMEOUT\" }}", 16 | "host": "mockbin.org", 17 | "name": "svc2", 18 | "port": 80, 19 | "protocol": "http", 20 | "read_timeout": "${{ env \"DECK_SVC_READ_TIMEOUT\" }}", 21 | "retries": "${{ env \"DECK_SVC_RETRIES\" }}" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /merge/merge_testfiles/test4_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "this is file7", 3 | "_format_version": "3.0", 4 | "services": [ 5 | { 6 | "connect_timeout": "${{ env \"DECK_SVC_CONNECT_TIMEOUT\" }}", 7 | "host": "mockbin.org", 8 | "name": "svc1", 9 | "port": 80, 10 | "protocol": "http", 11 | "read_timeout": "${{ env \"DECK_SVC_READ_TIMEOUT\" }}", 12 | "retries": "${{ env \"DECK_SVC_RETRIES\" }}" 13 | }, 14 | { 15 | "connect_timeout": "${{ env \"DECK_SVC_CONNECT_TIMEOUT\" }}", 16 | "host": "mockbin.org", 17 | "name": "svc2", 18 | "port": 80, 19 | "protocol": "http", 20 | "read_timeout": "${{ env \"DECK_SVC_READ_TIMEOUT\" }}", 21 | "retries": "${{ env \"DECK_SVC_RETRIES\" }}" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v*.*" 8 | 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Configure git to use a PAT when cloning 21 | run: | 22 | git config --global url."https://api:${{ secrets.PAT }}@github.com/".insteadOf "https://github.com/" 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: 1.19 28 | 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /docs/mock-a-rena-oas.yml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: 3.1.0 3 | 4 | info: 5 | description: Mock service for a popular song in the 90s 6 | version: 0.0.1 7 | title: Mock-a-rena 8 | contact: 9 | name: Los del Rio 10 | 11 | servers: 12 | - url: https://mockbin.org/requests 13 | description: Mock-a-rena service 14 | 15 | tags: 16 | - name: mock 17 | description: Hey.... mock-a-rena! 18 | - name: a-rena 19 | description: tag-arena 20 | 21 | paths: 22 | "/mock": 23 | get: 24 | summary: Mock, mock, mock, mock-a-rena 25 | description: yeah yeah 26 | operationId: mock 27 | tags: 28 | - mock 29 | responses: 30 | '200': 31 | description: Success 32 | "/a-rena": 33 | get: 34 | summary: hey hey 35 | description: a-rena-description 36 | operationId: a-rena 37 | tags: 38 | - marena 39 | responses: 40 | '200': 41 | description: success 42 | -------------------------------------------------------------------------------- /merge/merge_testfiles/test1_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "this is file3", 3 | "_format_version":"3.1", 4 | "_transform":true, 5 | "routes":[ 6 | { 7 | "hosts": ["hello.com"], 8 | "name": "file1-route-1" 9 | }, { 10 | "hosts": ["hello.com"], 11 | "name": "file2-route-1" 12 | }, { 13 | "hosts": ["hello.com"], 14 | "name": "file3-route-1" 15 | } 16 | ], 17 | "services":[ 18 | { "name":"file1-service-1", 19 | "url":"http://example.com", 20 | "routes": [{ 21 | "name": "my_route", 22 | "paths": ["/path"] 23 | }] 24 | }, 25 | {"name":"file1-service-2", "url":"https://example.org"}, 26 | {"name":"file2-service-1", "url":"http://example.com"}, 27 | {"name":"file2-service-2", "url":"https://example.org"}, 28 | {"name":"file3-service-1", "url":"http://example.com"}, 29 | {"name":"file3-service-2", "url":"https://example.org"} 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /merge/merge_testfiles/test2_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "this is file1", 3 | "_format_version":"3.1", 4 | "_transform":true, 5 | "routes":[ 6 | { 7 | "hosts": ["hello.com"], 8 | "name": "file3-route-1" 9 | }, { 10 | "hosts": ["hello.com"], 11 | "name": "file2-route-1" 12 | }, { 13 | "hosts": ["hello.com"], 14 | "name": "file1-route-1" 15 | } 16 | ], 17 | "services":[ 18 | {"name":"file3-service-1", "url":"http://example.com"}, 19 | {"name":"file3-service-2", "url":"https://example.org"}, 20 | {"name":"file2-service-1", "url":"http://example.com"}, 21 | {"name":"file2-service-2", "url":"https://example.org"}, 22 | { "name":"file1-service-1", 23 | "url":"http://example.com", 24 | "routes": [{ 25 | "name": "my_route", 26 | "paths": ["/path"] 27 | }] 28 | }, 29 | {"name":"file1-service-2", "url":"https://example.org"} 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kong/go-apiops/deckformat" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // VERSION is the current version of decK. 11 | // This should be substituted by git tag during the build process. 12 | var VERSION = "dev" 13 | 14 | // COMMIT is the short hash of the source tree. 15 | // This should be substituted by Git commit hash during the build process. 16 | var COMMIT = "unknown" 17 | 18 | var versionCmd = &cobra.Command{ 19 | Use: "version", 20 | Short: "Print the go-apiops version", 21 | Long: `The version command prints the version of go-apiops along with a Git short 22 | commit hash of the source tree.`, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | fmt.Printf("go-apiops %s (%s) \n", VERSION, COMMIT) 25 | }, 26 | } 27 | 28 | func init() { 29 | deckformat.ToolVersionSet("go-apiops", "", "") // VERSION, COMMIT) disabled for now 30 | rootCmd.AddCommand(versionCmd) 31 | } 32 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/18-request-validator-plugin-path-params-outside-ops.yaml: -------------------------------------------------------------------------------- 1 | # When the request-validator is added with path and operation parameters, 2 | # the generator should automatically generate it. 3 | 4 | openapi: 3.0.2 5 | 6 | info: 7 | title: Example 8 | version: 1.0.0 9 | 10 | servers: 11 | - url: http://backend.com/path 12 | 13 | x-kong-plugin-request-validator: {} 14 | 15 | paths: 16 | /test/common-param/{common-param}: 17 | parameters: 18 | - in: path 19 | name: common-param 20 | schema: 21 | type: integer 22 | required: true 23 | get: 24 | parameters: 25 | - in: query 26 | name: metadata 27 | schema: 28 | type: boolean 29 | required: false 30 | responses: 31 | '200': 32 | description: OK 33 | x-kong-plugin-request-validator: 34 | enabled: true 35 | config: 36 | verbose_response: true 37 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/016a-security-oidc-generation-no-component-extension.yaml: -------------------------------------------------------------------------------- 1 | # security directives can generate oidc plugin configs 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: OIDC tests 6 | version: v2 7 | servers: 8 | - url: / 9 | 10 | # defining a toplevel security spec 11 | security: 12 | - doclevel-oidc: ["top-scope"] 13 | 14 | # overriding the top level with per operation ones below 15 | paths: 16 | /path1: 17 | get: 18 | operationId: OpsId1 19 | # this should get the plugin per doclevel-oidc specs, since we do not override 20 | summary: List API versions 21 | responses: 22 | '200': 23 | description: |- 24 | 200 response 25 | 26 | 27 | components: 28 | securitySchemes: 29 | doclevel-oidc: 30 | type: openIdConnect 31 | openIdConnectUrl: https://konghq.com/top-level 32 | x-kong-security-openid-connect: 33 | config: 34 | run_on_preflight: false 35 | scopes_required: ["scope2", "scope1"] -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/23-no-servers-at-all.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "localhost", 6 | "id": "a49d53c1-c2ce-5d7c-a72c-e2c376de9e93", 7 | "name": "simple-api", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "046fbd3e-5b29-5d21-b7dd-637c5b1060df", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api_hello_get", 19 | "paths": [ 20 | "~/hello$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_23-no-servers-at-all.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_23-no-servers-at-all.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [] 38 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/03-servers-defaults.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "localhost", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "eee036de-517e-59cf-a2e0-17b3adfa31b5", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_opsid", 19 | "paths": [ 20 | "~/$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_03-servers-defaults.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_03-servers-defaults.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [] 38 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/04a-servers-upstream.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "server2.com", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 65000, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "eee036de-517e-59cf-a2e0-17b3adfa31b5", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_opsid", 19 | "paths": [ 20 | "~/$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_04a-servers-upstream.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_04a-servers-upstream.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [] 38 | } -------------------------------------------------------------------------------- /filebasics/filebasics_test.go: -------------------------------------------------------------------------------- 1 | package filebasics_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | ) 6 | 7 | var _ = Describe("filebasics", func() { 8 | Describe("ReadFile", func() { 9 | PIt("still to do", func() { 10 | }) 11 | }) 12 | 13 | Describe("MustReadFile", func() { 14 | PIt("still to do", func() { 15 | }) 16 | }) 17 | 18 | Describe("MustWriteFile", func() { 19 | PIt("still to do", func() { 20 | }) 21 | }) 22 | 23 | Describe("MustSerialize", func() { 24 | PIt("still to do", func() { 25 | }) 26 | }) 27 | 28 | Describe("Deserialize", func() { 29 | PIt("still to do", func() { 30 | }) 31 | }) 32 | 33 | Describe("MustDeserialize", func() { 34 | PIt("still to do", func() { 35 | }) 36 | }) 37 | 38 | Describe("MustWriteSerializedFile", func() { 39 | PIt("still to do", func() { 40 | }) 41 | }) 42 | 43 | Describe("DeserializeFile", func() { 44 | PIt("still to do", func() { 45 | }) 46 | }) 47 | 48 | Describe("MustDeserializeFile", func() { 49 | PIt("still to do", func() { 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/19-request-validator-plugin-op-params-override-path-params.yaml: -------------------------------------------------------------------------------- 1 | # When the request-validator is added with path and operation parameters, 2 | # with both sharing same name and location, operation parameters override the path parameters. 3 | 4 | openapi: 3.0.2 5 | 6 | info: 7 | title: Example 8 | version: 1.0.0 9 | 10 | servers: 11 | - url: http://backend.com/path 12 | 13 | x-kong-plugin-request-validator: {} 14 | 15 | paths: 16 | /test/common-param/{common-param}: 17 | parameters: 18 | - in: path 19 | name: common-param 20 | schema: 21 | type: string 22 | required: true 23 | get: 24 | parameters: 25 | - in: path 26 | name: common-param 27 | schema: 28 | type: integer 29 | required: true 30 | responses: 31 | '200': 32 | description: OK 33 | x-kong-plugin-request-validator: 34 | enabled: true 35 | config: 36 | verbose_response: true 37 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/07a-service-defaults.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "myhost.com", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/something", 9 | "plugins": [], 10 | "port": 123, 11 | "protocol": "http", 12 | "routes": [ 13 | { 14 | "id": "663104d8-7e60-525d-b506-e42971b4466b", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_uses-doc-service", 19 | "paths": [ 20 | "~/path1$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_07a-service-defaults.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_07a-service-defaults.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [] 38 | } -------------------------------------------------------------------------------- /merge/merge_testfiles/file1.yml: -------------------------------------------------------------------------------- 1 | # Metadata fields start with an underscore (_) 2 | # Fields that do not start with an underscore represent Kong entities and attributes 3 | 4 | _comment: this is file1 5 | 6 | # _format_version is mandatory, 7 | # it specifies the minimum version of Kong that supports the format 8 | _format_version: "3.0" 9 | 10 | # _transform is optional, defaulting to true. 11 | # It specifies whether schema transformations should be applied when importing this file 12 | # as a rule of thumb, leave this setting to true if you are importing credentials 13 | # with plain passwords, which need to be encrypted/hashed before storing on the database. 14 | # On the other hand, if you are reimporting a database with passwords already encrypted/hashed, 15 | # set it to false. 16 | _transform: true 17 | 18 | services: 19 | - name: file1-service-1 20 | url: http://example.com 21 | routes: 22 | - name: my_route 23 | paths: 24 | - /path 25 | - name: file1-service-2 26 | url: https://example.org 27 | 28 | routes: 29 | - name: file1-route-1 30 | hosts: ["hello.com"] 31 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/03a-server-no-trailing-slash.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "example.com", 6 | "id": "5434f850-a000-5ad5-8772-1a3cd10811e6", 7 | "name": "server-with-no-trailing-slash", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "d44c95fe-dc1c-596c-8b4a-e18ff50607c4", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "server-with-no-trailing-slash_myid", 19 | "paths": [ 20 | "~/path$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_03a-server-no-trailing-slash.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_03a-server-no-trailing-slash.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [] 38 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | concurrency: 4 | # for PR's cancel the running task, if another commit is pushed 5 | group: ${{ github.workflow }} ${{ github.ref }} 6 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | types: 14 | - opened 15 | - ready_for_review 16 | - synchronize 17 | 18 | jobs: 19 | go-test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '^1.20' 28 | - name: Set up ginkgo 29 | run: go install github.com/onsi/ginkgo/v2/ginkgo 30 | - name: Run Tests 31 | run: make test 32 | - name: Run tests with Coverage 33 | run: make coverage 34 | - name: Upload Code Coverage 35 | uses: codecov/codecov-action@v4 36 | with: 37 | name: codecov 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | fail_ci_if_error: false 40 | -------------------------------------------------------------------------------- /openapi2kong/utils_test.go: -------------------------------------------------------------------------------- 1 | package openapi2kong 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCrossProduct(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | slices [][]any 13 | expectedResult [][]any 14 | }{ 15 | { 16 | name: "Empty slices", 17 | slices: [][]any{}, 18 | expectedResult: [][]any{{}}, 19 | }, 20 | { 21 | name: "Single slice", 22 | slices: [][]any{{"a", "b", "c"}}, 23 | expectedResult: [][]any{ 24 | {"a"}, 25 | {"b"}, 26 | {"c"}, 27 | }, 28 | }, 29 | { 30 | name: "Mixed types and different length input slices", 31 | slices: [][]any{ 32 | {"a", "b", "c"}, 33 | {1, 2}, 34 | }, 35 | expectedResult: [][]any{ 36 | {"a", 1}, 37 | {"a", 2}, 38 | {"b", 1}, 39 | {"b", 2}, 40 | {"c", 1}, 41 | {"c", 2}, 42 | }, 43 | }, 44 | } 45 | 46 | for _, tc := range testCases { 47 | t.Run(tc.name, func(t *testing.T) { 48 | result := crossProduct(tc.slices...) 49 | assert.Equal(t, tc.expectedResult, result) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/20-empty-paths.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "simple-api-overview.upstream", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [], 13 | "tags": [ 14 | "OAS3_import", 15 | "OAS3file_20-empty-paths.yaml" 16 | ] 17 | } 18 | ], 19 | "upstreams": [ 20 | { 21 | "id": "811c42d6-ef18-5296-a550-7dca2262b4d8", 22 | "name": "simple-api-overview.upstream", 23 | "tags": [ 24 | "OAS3_import", 25 | "OAS3file_20-empty-paths.yaml" 26 | ], 27 | "targets": [ 28 | { 29 | "tags": [ 30 | "OAS3_import", 31 | "OAS3file_20-empty-paths.yaml" 32 | ], 33 | "target": "server1.com:443" 34 | }, 35 | { 36 | "tags": [ 37 | "OAS3_import", 38 | "OAS3file_20-empty-paths.yaml" 39 | ], 40 | "target": "server2.com:443" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all check-main-dependencies check-test-dependencies check-lint-dependencies build lint test coverage clean 2 | 3 | BINARY_NAME=go-apiops 4 | 5 | echo_fail = printf "\e[31m✘ \033\e[0m$(1)\n" 6 | echo_pass = printf "\e[32m✔ \033\e[0m$(1)\n" 7 | 8 | check-dependency = $(if $(shell command -v $(1)),$(call echo_pass,found $(1)),$(call echo_fail,$(1) not installed);exit 1) 9 | 10 | all: check-test-dependencies check-lint-dependencies build test lint 11 | 12 | check-main-dependencies: 13 | @$(call check-dependency,go) 14 | 15 | check-test-dependencies: check-main-dependencies 16 | @$(call check-dependency,ginkgo) 17 | 18 | check-lint-dependencies: 19 | @$(call check-dependency,golangci-lint) 20 | 21 | build: check-main-dependencies 22 | go build -o ${BINARY_NAME} main.go 23 | 24 | lint: check-lint-dependencies 25 | golangci-lint run 26 | 27 | test: check-test-dependencies 28 | ginkgo -r 29 | 30 | coverage: check-test-dependencies 31 | ginkgo -r --race --coverprofile coverage.out 32 | 33 | clean: check-main-dependencies 34 | $(RM) ./openapi2kong/oas3_testfiles/*.generated.json 35 | $(RM) ./openapi2kong/oas3_testfiles/*.generated_inso.json 36 | $(RM) ./${BINARY_NAME} 37 | $(RM) ./go-apiops 38 | go mod tidy 39 | go clean 40 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/11-references.yaml: -------------------------------------------------------------------------------- 1 | # references should be enabled for; service, upstream, and route defaults, as well 2 | # as for plugin configurations. 3 | # only full entities, no sub entities. 4 | # location must be "#/components/x-kong/..." 5 | 6 | openapi: '3.0.0' 7 | info: 8 | title: Simple API overview 9 | version: v2 10 | servers: 11 | - url: https://server1.com/ 12 | 13 | x-kong-route-defaults: 14 | "$ref": "#/components/x-kong/routes/defaults" 15 | x-kong-service-defaults: 16 | "$ref": "#/components/x-kong/services/defaults" 17 | x-kong-upstream-defaults: 18 | "$ref": "#/components/x-kong/upstreams/defaults" 19 | x-kong-plugin-file-log: 20 | "$ref": "#/components/x-kong/plugins/log_to_file" 21 | 22 | paths: 23 | /path1: 24 | get: 25 | summary: List API versions 26 | responses: 27 | '200': 28 | description: |- 29 | 200 response 30 | 31 | components: 32 | x-kong: 33 | plugins: 34 | log_to_file: 35 | config: 36 | path: "/dev/stderr" 37 | routes: 38 | defaults: 39 | regex_priority: 999 40 | upstreams: 41 | defaults: 42 | slots: 999 43 | services: 44 | defaults: 45 | retries: 999 46 | 47 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/07a-service-defaults.yaml: -------------------------------------------------------------------------------- 1 | # Some of the generated fields for services should be overridable 2 | # using the defaults. 3 | # These are the order in which we should take the values: 4 | # - service.protocol: x-kong-service-defaults -> servers[1].url (first entry servers block) -> default value "https" 5 | # - service.port: x-kong-service-defaults -> servers[1].url (first entry servers block) -> default to 80/443 based on protocol 6 | # - service.path: x-kong-service-defaults -> servers[1].url (first entry servers block) -> default to "/" 7 | # - service.host: x-kong-service-defaults -> if 1 url then servers[1].url (first entry servers block), if multiple create an upstream -> default to "localhost" 8 | # Note: if a host default is specified, then no upstream should be generated 9 | 10 | openapi: '3.0.0' 11 | info: 12 | title: Simple API overview 13 | version: v2 14 | servers: 15 | - url: https://server1.com/anything 16 | - url: https://server2.com/anything 17 | 18 | x-kong-service-defaults: 19 | protocol: http 20 | port: 123 21 | path: /something 22 | host: myhost.com 23 | 24 | paths: 25 | /path1: 26 | get: 27 | operationId: uses-doc-service 28 | summary: List API versions 29 | responses: 30 | '200': 31 | description: |- 32 | 200 response 33 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/15-circular-requestBody-schema.yaml: -------------------------------------------------------------------------------- 1 | # JSONschema's can contain references to the "/components/schemas/" section 2 | # those references can also be recursive. 3 | # This file tests a recursive schema being properly extracted. 4 | 5 | openapi: 3.0.1 6 | 7 | info: 8 | title: Testing Circular 9 | version: 1.1.1 10 | 11 | contact: 12 | name: Kong Insomnia 13 | 14 | servers: 15 | - url: https://some.random.url 16 | 17 | x-kong-plugin-request-validator: 18 | enabled: true 19 | version: draft4 20 | config: 21 | verbose_response: true 22 | 23 | tags: 24 | - name: Testing 25 | 26 | paths: 27 | /testing: 28 | post: 29 | requestBody: 30 | $ref: '#/components/requestBodies/TestRequest' 31 | responses: 32 | '200': 33 | description: success 34 | 35 | components: 36 | requestBodies: 37 | TestRequest: 38 | content: 39 | application/json: 40 | schema: 41 | $ref: '#/components/schemas/TestRequest' 42 | 43 | schemas: 44 | TestRequest: 45 | allOf: 46 | - $ref: '#/components/schemas/CircularStart' 47 | 48 | CircularStart: 49 | allOf: 50 | - $ref: '#/components/schemas/CircularEnd' 51 | 52 | CircularEnd: 53 | allOf: 54 | - $ref: '#/components/schemas/CircularStart' 55 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/25-routes-with-headers.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | title: Simple API overview 4 | version: v2 5 | servers: 6 | - url: https://server1.com/ 7 | paths: 8 | /path1/{pathParam}: 9 | parameters: 10 | - in: path 11 | name: pathParam 12 | required: true 13 | schema: 14 | type: string 15 | get: 16 | operationId: get-doc-service 17 | summary: List API versions 18 | responses: 19 | '200': 20 | description: |- 21 | 200 response 22 | /path2: 23 | parameters: 24 | - in: header 25 | name: X-Path-Level-API-Version 26 | required: true 27 | schema: 28 | type: string 29 | enum: 30 | - v1 31 | - v2 32 | get: 33 | parameters: 34 | - in: header 35 | name: X-Method-Level-Int-Header 36 | required: true 37 | schema: 38 | type: integer 39 | enum: 40 | - 1 41 | - 2 42 | - 3 43 | - in: header 44 | name: X-Method-Level-No-Enum-Header 45 | required: true 46 | schema: 47 | type: string 48 | operationId: get-path2-service 49 | summary: List API versions Path2 50 | responses: 51 | '200': 52 | description: |- 53 | 200 response 54 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/05-service-defaults-document.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-service-defaults can be specified and will be honored on each level 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server1.com/ 9 | x-kong-service-defaults: 10 | retries: 100 11 | paths: 12 | /path1: 13 | # this should create routes attached to the document-level service 14 | get: 15 | operationId: uses-doc-service 16 | summary: List API versions 17 | responses: 18 | '200': 19 | description: |- 20 | 200 response 21 | /path2: 22 | # specify server block to force another service being created 23 | # should inherit the service defaults from the document level 24 | servers: 25 | - url: https://server2.com/ 26 | get: 27 | operationId: uses-path-service 28 | summary: List API versions 29 | responses: 30 | '200': 31 | description: |- 32 | 200 response 33 | post: 34 | # specify server block to force another service being created 35 | # should inherit the service defaults from the document level 36 | servers: 37 | - url: https://server3.com/ 38 | operationId: uses-ops-service 39 | summary: List API versions 40 | responses: 41 | '200': 42 | description: |- 43 | 200 response 44 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/01-names-inferred.yaml: -------------------------------------------------------------------------------- 1 | # inferring names from the document title, the path+method and/or operation id 2 | 3 | openapi: '3.0.0' 4 | info: 5 | # document name detected from the title below 6 | title: Simple API overview 7 | version: v2 8 | servers: 9 | - url: http://backend1.com/some/path 10 | - url: http://backend2.com/some/path 11 | paths: 12 | /: 13 | # path with only a single '/' 14 | get: 15 | # operation with an ID 16 | operationId: opsid1 17 | responses: 18 | '200': 19 | description: 200 ok 20 | post: 21 | # operation without an ID 22 | responses: 23 | '200': 24 | description: 200 ok 25 | /application: 26 | # non-empty path, without trailing / 27 | get: 28 | # operation with an ID 29 | operationId: opsid2 30 | responses: 31 | '200': 32 | description: 200 ok 33 | post: 34 | # operation without an ID 35 | responses: 36 | '200': 37 | description: 200 ok 38 | /application/: 39 | # non-empty path, same as above, but with trailing / 40 | get: 41 | # operation with an ID 42 | operationId: opsid3 43 | responses: 44 | '200': 45 | description: 200 ok 46 | post: 47 | # operation without an ID 48 | responses: 49 | '200': 50 | description: 200 ok 51 | 52 | 53 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/02-names-set.yaml: -------------------------------------------------------------------------------- 1 | # generating names from the specified names in x-kong-name directives 2 | openapi: '3.0.0' 3 | info: 4 | title: Simple API overview 5 | version: v2 6 | servers: 7 | - url: http://backend1.com/some/path 8 | - url: http://backend2.com/some/path 9 | # we're setting the spec name explicitly here 10 | x-kong-name: oas-spec-name 11 | paths: 12 | /app1: 13 | # path with an explicit name 14 | x-kong-name: path-name 15 | get: 16 | # operation with an ID 17 | operationId: opsid1 18 | responses: 19 | '200': 20 | description: 200 ok 21 | post: 22 | # operation without an ID, and without name 23 | responses: 24 | '200': 25 | description: 200 ok 26 | put: 27 | # operation without an ID, with name 28 | x-kong-name: my-put-operation 29 | responses: 30 | '200': 31 | description: 200 ok 32 | /app2: 33 | # path NOT getting an explicit name 34 | get: 35 | # operation with an ID 36 | operationId: opsid2 37 | responses: 38 | '200': 39 | description: 200 ok 40 | post: 41 | # operation without an ID 42 | responses: 43 | '200': 44 | description: 200 ok 45 | put: 46 | # operation without an ID, with name 47 | x-kong-name: my-put-operation 48 | responses: 49 | '200': 50 | description: 200 ok 51 | 52 | 53 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/06-upstream-defaults-document.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-upstream-defaults can be specified and will be honored on each level 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server1.com/ 9 | x-kong-upstream-defaults: 10 | slots: 1000 11 | 12 | paths: 13 | /path1: 14 | # this should create routes attached to the document-level service 15 | get: 16 | operationId: uses-doc-service 17 | summary: List API versions 18 | responses: 19 | '200': 20 | description: |- 21 | 200 response 22 | /path2: 23 | # specify upstream defaults to force another upstream+service being 24 | # created at path level, should inherit server block from the document level 25 | x-kong-upstream-defaults: 26 | slots: 2000 27 | get: 28 | operationId: uses-path-service 29 | summary: List API versions 30 | responses: 31 | '200': 32 | description: |- 33 | 200 response 34 | post: 35 | # specify upstream defaults to force another upstream+service being 36 | # created at operation level, should inherit server block from the document level 37 | x-kong-upstream-defaults: 38 | slots: 3000 39 | operationId: uses-ops-service 40 | summary: List API versions 41 | responses: 42 | '200': 43 | description: |- 44 | 200 response 45 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/_fixup_expected.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is used to fix up the expected output files in the 4 | # oas3_testfiles directory. 5 | # Often when making small changes, a lot of test files are being impacted. 6 | # This script can be used to fix up the expected output files all at once. 7 | # 8 | # 1. make sure the changes have their own tests which succeed, indicating that 9 | # the failures in the other files are false-positives now. 10 | # 2. commit your changes to ensure you can revert the changes by this script!! 11 | # 3. run the tests with `make test`, to generate all the '*.generated.json' files. 12 | # 4. run this script to copy the generated files over the expected files. 13 | # 5. run the tests again with `make test` to verify that the expected files are 14 | # now correct. 15 | 16 | set -e 17 | # set -x 18 | 19 | if [ -d "openapi2kong" ]; then 20 | pushd openapi2kong 21 | fi 22 | if [ -d "oas3_testfiles" ]; then 23 | pushd oas3_testfiles 24 | fi 25 | 26 | function singlefile() { 27 | filename="$1" 28 | postfix="$2" 29 | destname="${filename%.generated"$postfix".json}.expected$postfix.json" 30 | echo "$filename -> $destname" 31 | sed 's/\\u003c//g' > "$destname" 32 | } 33 | 34 | # ls -la 35 | 36 | for f in ./*.generated.json; do 37 | singlefile "$f" "" 38 | done 39 | for f in ./*.generated_inso.json; do 40 | singlefile "$f" "_inso" 41 | done 42 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/016a-security-oidc-generation-no-component-extension.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "localhost", 6 | "id": "bcda787b-18bc-5f83-85f2-1cf83b5f8eb3", 7 | "name": "oidc-tests", 8 | "path": "/", 9 | "plugins": [ 10 | { 11 | "config": { 12 | "issuer": "https://konghq.com/top-level", 13 | "run_on_preflight": false, 14 | "scopes_required": [ 15 | "scope1", 16 | "scope2", 17 | "top-scope" 18 | ] 19 | }, 20 | "name": "openid-connect" 21 | } 22 | ], 23 | "port": 443, 24 | "protocol": "https", 25 | "routes": [ 26 | { 27 | "id": "558eb5de-46b9-5500-ae2a-9a8e321e9d65", 28 | "methods": [ 29 | "GET" 30 | ], 31 | "name": "oidc-tests_opsid1", 32 | "paths": [ 33 | "~/path1$" 34 | ], 35 | "plugins": [], 36 | "regex_priority": 200, 37 | "strip_path": false, 38 | "tags": [ 39 | "OAS3_import", 40 | "OAS3file_016a-security-oidc-generation-no-component-extension.yaml" 41 | ] 42 | } 43 | ], 44 | "tags": [ 45 | "OAS3_import", 46 | "OAS3file_016a-security-oidc-generation-no-component-extension.yaml" 47 | ] 48 | } 49 | ], 50 | "upstreams": [] 51 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/07-service-defaults-overrides.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-service-defaults can be specified and will be honored on each 2 | # level, whilst the used upstream remains the same 3 | 4 | openapi: '3.0.0' 5 | info: 6 | title: Simple API overview 7 | version: v2 8 | servers: 9 | - url: https://server1.com/ 10 | - url: https://server2.com/ 11 | x-kong-service-defaults: 12 | retries: 100 13 | paths: 14 | /path1: 15 | # this should create routes attached to the document-level service 16 | get: 17 | operationId: uses-doc-service 18 | summary: List API versions 19 | responses: 20 | '200': 21 | description: |- 22 | 200 response 23 | /path2: 24 | # specify new defaults to force another service being created 25 | # should inherit upstream from document-level, so no new upstream 26 | # only a new service 27 | x-kong-service-defaults: 28 | retries: 200 29 | get: 30 | operationId: uses-path-service 31 | summary: List API versions 32 | responses: 33 | '200': 34 | description: |- 35 | 200 response 36 | post: 37 | # specify new defaults to force another service being created 38 | # should inherit upstream from document-level, so no new upstream 39 | # only a new service 40 | x-kong-service-defaults: 41 | retries: 300 42 | operationId: uses-ops-service 43 | summary: List API versions 44 | responses: 45 | '200': 46 | description: |- 47 | 200 response 48 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/14-no-request-validator-plugin.yaml: -------------------------------------------------------------------------------- 1 | # When the request-validator is added, but there are no schema's for the parameters 2 | # or body, then the plugin shouldn't be added at all. 3 | # see https://konghq.atlassian.net/browse/FTI-4305 4 | 5 | openapi: '3.0.0' 6 | info: 7 | description: 'OpenAPI Specification' 8 | version: 1.0.1 9 | title: Mock Target API 10 | 11 | servers: 12 | - url: http://httpbin.org/anything 13 | 14 | x-kong-plugin-request-validator: {} 15 | 16 | paths: 17 | /help: 18 | get: 19 | # there is nothing to validate here, so no plugin should be created 20 | summary: Get help 21 | operationId: getHelp 22 | parameters: 23 | # This would not be added to the req-validator plugin config 24 | # as cookie type is not supported yet. 25 | # A warning would be logged and this parameter would be ignored. 26 | - in: cookie 27 | name: cookieid 28 | schema: 29 | type: integer 30 | required: true 31 | responses: 32 | '200': 33 | description: This is a success. 34 | 35 | /user: 36 | # we define content-types-allowed statically (not generated) 37 | # which means the plugin should still be generated, now 38 | # with an empty schema (passes everything). 39 | x-kong-plugin-request-validator: 40 | config: 41 | allowed_content_types: ["application/xml"] 42 | get: 43 | summary: Get help 44 | operationId: getHelp 45 | responses: 46 | '200': 47 | description: This is a success. 48 | 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kong/go-apiops 2 | 3 | go 1.23.0 4 | 5 | replace gopkg.in/yaml.v3 v3.0.1 => github.com/Kong/yaml v1.0.0 6 | 7 | require ( 8 | github.com/go-logr/logr v1.4.2 9 | github.com/go-logr/stdr v1.2.2 10 | github.com/google/go-cmp v0.6.0 11 | github.com/google/uuid v1.6.0 12 | github.com/kong/go-slugify v1.0.0 13 | github.com/onsi/ginkgo/v2 v2.22.2 14 | github.com/onsi/gomega v1.36.2 15 | github.com/pb33f/libopenapi v0.16.13 16 | github.com/speakeasy-api/jsonpath v0.6.2 17 | github.com/spf13/cobra v1.8.1 18 | github.com/stretchr/testify v1.10.0 19 | github.com/yuin/gopher-lua v1.1.1 20 | golang.org/x/term v0.29.0 21 | gopkg.in/yaml.v3 v3.0.1 22 | sigs.k8s.io/yaml v1.4.0 23 | ) 24 | 25 | require ( 26 | github.com/bahlo/generic-list-go v0.2.0 // indirect 27 | github.com/buger/jsonparser v1.1.1 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect 30 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 31 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/mozillazg/go-unidecode v0.2.0 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/spf13/pflag v1.0.5 // indirect 37 | github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect 38 | github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect 39 | golang.org/x/net v0.33.0 // indirect 40 | golang.org/x/sys v0.30.0 // indirect 41 | golang.org/x/text v0.21.0 // indirect 42 | golang.org/x/tools v0.28.0 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/04-servers-upstream.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "simple-api-overview.upstream", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "eee036de-517e-59cf-a2e0-17b3adfa31b5", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_opsid", 19 | "paths": [ 20 | "~/$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_04-servers-upstream.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_04-servers-upstream.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [ 38 | { 39 | "id": "811c42d6-ef18-5296-a550-7dca2262b4d8", 40 | "name": "simple-api-overview.upstream", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_04-servers-upstream.yaml" 44 | ], 45 | "targets": [ 46 | { 47 | "tags": [ 48 | "OAS3_import", 49 | "OAS3file_04-servers-upstream.yaml" 50 | ], 51 | "target": "server1.com:443" 52 | }, 53 | { 54 | "tags": [ 55 | "OAS3_import", 56 | "OAS3file_04-servers-upstream.yaml" 57 | ], 58 | "target": "server2.com:443" 59 | } 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/20-ignore-circular-references.circular-yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Testing Circular 4 | version: 1.1.1 5 | 6 | contact: 7 | name: Kong Insomnia 8 | 9 | servers: 10 | - url: https://some.random.url 11 | 12 | x-kong-plugin-request-validator: 13 | enabled: true 14 | version: draft4 15 | config: 16 | verbose_response: true 17 | 18 | tags: 19 | - name: Testing 20 | 21 | paths: 22 | "/categories": 23 | get: 24 | tags: 25 | - Category 26 | summary: Get all categories for a given classificationId and brand 27 | parameters: 28 | - name: classificationId 29 | in: query 30 | description: 'Classification ID. Example value: 545' 31 | required: true 32 | schema: 33 | type: integer 34 | format: int32 35 | responses: 36 | '200': 37 | description: OK 38 | content: 39 | application/json: 40 | schema: 41 | "$ref": "#/components/schemas/CategoriesResponse" 42 | components: 43 | schemas: 44 | CategoriesResponse: 45 | required: 46 | - categories 47 | type: object 48 | properties: 49 | categories: 50 | type: array 51 | items: 52 | "$ref": "#/components/schemas/Category" 53 | additionalProperties: false 54 | Category: 55 | required: 56 | - id 57 | - subCategories 58 | type: object 59 | properties: 60 | id: 61 | type: integer 62 | format: int32 63 | subCategories: 64 | type: array 65 | items: 66 | "$ref": "#/components/schemas/Category" 67 | additionalProperties: false 68 | -------------------------------------------------------------------------------- /deckformat/deckformat.go: -------------------------------------------------------------------------------- 1 | package deckformat 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | VersionKey = "_format_version" 9 | TransformKey = "_transform" 10 | HistoryKey = "_ignore" // the top-level key in deck files for storing history info 11 | ) 12 | 13 | func init() { 14 | initPointerCollections() 15 | } 16 | 17 | // 18 | // 19 | // Keeping track of the tool/binary version info (set once at startup) 20 | // 21 | // 22 | 23 | var toolInfo = struct { 24 | name string 25 | version string 26 | commit string 27 | }{} 28 | 29 | // ToolVersionSet can be called once to set the tool info that is reported in the history. 30 | // The 'version' and 'commit' strings are optional. Omitting them (lower cardinality) makes 31 | // for a better GitOps experience, but provides less detail. 32 | func ToolVersionSet(name string, version string, commit string) { 33 | if toolInfo.name != "" || name == "" { 34 | panic("the tool information was already set, or cannot be set to an empty string") 35 | } 36 | toolInfo.name = name 37 | toolInfo.version = version 38 | toolInfo.commit = commit 39 | } 40 | 41 | // ToolVersionGet returns the individual components of the info 42 | func ToolVersionGet() (name string, version string, commit string) { 43 | if toolInfo.name == "" { 44 | panic("the tool information wasn't set, call ToolVersionSet first") 45 | } 46 | return toolInfo.name, toolInfo.version, toolInfo.commit 47 | } 48 | 49 | // ToolVersionString returns the info in a single formatted string. eg. "decK 1.2 (123abc)" 50 | func ToolVersionString() string { 51 | n, v, c := ToolVersionGet() 52 | if c != "" { 53 | return fmt.Sprintf("%s %s (%s)", n, v, c) 54 | } 55 | if v != "" { 56 | return fmt.Sprintf("%s %s", n, v) 57 | } 58 | return n 59 | } 60 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/06a-upstream-defaults.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "simple-api-overview.upstream", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/anything", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "663104d8-7e60-525d-b506-e42971b4466b", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_uses-doc-service", 19 | "paths": [ 20 | "~/path1$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_06a-upstream-defaults.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_06a-upstream-defaults.yaml" 34 | ] 35 | } 36 | ], 37 | "upstreams": [ 38 | { 39 | "id": "811c42d6-ef18-5296-a550-7dca2262b4d8", 40 | "name": "simple-api-overview.upstream", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_06a-upstream-defaults.yaml" 44 | ], 45 | "targets": [ 46 | { 47 | "tags": [ 48 | "OAS3_import", 49 | "OAS3file_06a-upstream-defaults.yaml" 50 | ], 51 | "target": "my-server1.com:443" 52 | }, 53 | { 54 | "tags": [ 55 | "OAS3_import", 56 | "OAS3file_06a-upstream-defaults.yaml" 57 | ], 58 | "target": "my-server2.com:443" 59 | } 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/kong/go-apiops/logbasics" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // rootCmd represents the base command when called without any subcommands 12 | var rootCmd = &cobra.Command{ 13 | Use: "go-apiops", 14 | Short: "A CLI for testing the Kong go-apiops library", 15 | Long: `A CLI for testing the Kong go-apiops library. 16 | 17 | go-apiops houses an improved APIOps toolset for operating Kong Gateway deployments.`, 18 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 19 | // set the verbosity level of the log output 20 | verbosity, err := cmd.Flags().GetInt("verbose") 21 | if err != nil { 22 | return err 23 | } 24 | logbasics.Initialize(log.LstdFlags, verbosity) 25 | return nil 26 | }, 27 | 28 | // Uncomment the following line if your bare application 29 | // has an action associated with it: 30 | // Run: func(cmd *cobra.Command, args []string) { }, 31 | } 32 | 33 | // Execute adds all child commands to the root command and sets flags appropriately. 34 | // This is called by main.main(). It only needs to happen once to the rootCmd. 35 | func Execute() { 36 | err := rootCmd.Execute() 37 | if err != nil { 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | func init() { 43 | // Here you will define your flags and configuration settings. 44 | // Cobra supports persistent flags, which, if defined here, 45 | // will be global for your application. 46 | 47 | rootCmd.PersistentFlags().Int("verbose", 0, 48 | "this value sets the verbosity level of the log output (higher == more verbose)") 49 | 50 | // Cobra also supports local flags, which will only run 51 | // when this action is called directly. 52 | // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 53 | } 54 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/11-references.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "simple-api-overview.upstream", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [ 10 | { 11 | "config": { 12 | "path": "/dev/stderr" 13 | }, 14 | "id": "b4eb8681-bd93-5d4b-9c19-702a9452e972", 15 | "name": "file-log", 16 | "tags": [ 17 | "OAS3_import", 18 | "OAS3file_11-references.yaml" 19 | ] 20 | } 21 | ], 22 | "port": 443, 23 | "protocol": "https", 24 | "retries": 999, 25 | "routes": [ 26 | { 27 | "id": "ef141006-f57c-5a01-ae4a-2a83a671ff76", 28 | "methods": [ 29 | "GET" 30 | ], 31 | "name": "simple-api-overview_path1_get", 32 | "paths": [ 33 | "~/path1$" 34 | ], 35 | "plugins": [], 36 | "regex_priority": 999, 37 | "strip_path": false, 38 | "tags": [ 39 | "OAS3_import", 40 | "OAS3file_11-references.yaml" 41 | ] 42 | } 43 | ], 44 | "tags": [ 45 | "OAS3_import", 46 | "OAS3file_11-references.yaml" 47 | ] 48 | } 49 | ], 50 | "upstreams": [ 51 | { 52 | "id": "811c42d6-ef18-5296-a550-7dca2262b4d8", 53 | "name": "simple-api-overview.upstream", 54 | "slots": 999, 55 | "tags": [ 56 | "OAS3_import", 57 | "OAS3file_11-references.yaml" 58 | ], 59 | "targets": [ 60 | { 61 | "tags": [ 62 | "OAS3_import", 63 | "OAS3file_11-references.yaml" 64 | ], 65 | "target": "server1.com:443" 66 | } 67 | ] 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/21-nullable-path-param.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "backend.com", 6 | "id": "aac33fce-7ade-5b69-9249-97bea965698e", 7 | "name": "odata-style-api-with-nullable-path-params", 8 | "path": "/path", 9 | "plugins": [], 10 | "port": 80, 11 | "protocol": "http", 12 | "routes": [ 13 | { 14 | "id": "649b2718-47a3-57ec-aec0-b8a4f1bf010d", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "odata-style-api-with-nullable-path-params_customproductdata", 19 | "paths": [ 20 | "~/CustomWarehouseData\\(Warehouse='(?\u003cwarehouse\u003e[^#?/]*)',Product='(?\u003cproduct\u003e[^#?/]+)',Location='(?\u003clocation\u003e[^#?/]*)'\\)$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 100, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_21-nullable-path-param.yaml" 28 | ] 29 | }, 30 | { 31 | "id": "dd39d9e0-196d-509d-b141-13e77ce94f81", 32 | "methods": [ 33 | "GET" 34 | ], 35 | "name": "odata-style-api-with-nullable-path-params_customdata", 36 | "paths": [ 37 | "~/custom/(?\u003ccustomid\u003e[^#?/]+)$" 38 | ], 39 | "plugins": [], 40 | "regex_priority": 100, 41 | "strip_path": false, 42 | "tags": [ 43 | "OAS3_import", 44 | "OAS3file_21-nullable-path-param.yaml" 45 | ] 46 | } 47 | ], 48 | "tags": [ 49 | "OAS3_import", 50 | "OAS3file_21-nullable-path-param.yaml" 51 | ] 52 | } 53 | ], 54 | "upstreams": [] 55 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/10-generic-plugins-multi-service.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-plugin- added to a spec where new service entities are 2 | # created on path and operation level. 3 | 4 | openapi: '3.0.0' 5 | info: 6 | title: Simple API overview 7 | version: v2 8 | servers: 9 | - url: https://server1.com/ 10 | 11 | x-kong-plugin-request-termination: 12 | # name will be determined from the tag 13 | # name: request-termination; 14 | config: 15 | status_code: 403 16 | message: So long and thanks for all the fish! 17 | 18 | paths: 19 | /path1: 20 | get: 21 | # This operation should not get a plugin, but rely on the document level 22 | # service-entity based plugin 23 | operationId: uses-doc-service-and-plugins 24 | summary: List API versions 25 | responses: 26 | '200': 27 | description: |- 28 | 200 response 29 | /path2: 30 | # add service defaults to force a new service entity on path-level. This service should 31 | # get the same plugins as the doc-level service entity + the ones defined here 32 | x-kong-service-defaults: {} 33 | x-kong-plugin-some-plugin1: {} 34 | get: 35 | x-kong-plugin-some-plugin2: {} 36 | operationId: uses-doc-plugin-on-path-service 37 | summary: List API versions 38 | responses: 39 | '200': 40 | description: |- 41 | 200 response 42 | post: 43 | # add another service on operation-level. This one should get the document level 44 | # plugins, the path-level one, and the one defined here. 45 | # The plugins are only applicable to a single route/operation, hence they will 46 | # all be on the route, and none on the service 47 | x-kong-service-defaults: {} 48 | x-kong-plugin-some-plugin3: {} 49 | operationId: uses-plugins-on-ops-level 50 | summary: List API versions 51 | responses: 52 | '200': 53 | description: |- 54 | 200 response 55 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/21-no-doc-servers.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "order-service.example.com", 6 | "id": "8b6ad746-12b9-50fd-8604-5e7768c4f5e9", 7 | "name": "multi-service-api_orders_post", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "e8d42325-3ee0-57b3-9883-5ea51618f4ac", 15 | "methods": [ 16 | "POST" 17 | ], 18 | "name": "multi-service-api_orders_post", 19 | "paths": [ 20 | "~/orders$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_21-no-doc-servers.yaml" 28 | ] 29 | } 30 | ], 31 | "tags": [ 32 | "OAS3_import", 33 | "OAS3file_21-no-doc-servers.yaml" 34 | ] 35 | }, 36 | { 37 | "host": "user-service.example.com", 38 | "id": "6e5e1d3b-894b-5ce8-b807-532df7b6d01d", 39 | "name": "multi-service-api_users_get", 40 | "path": "/", 41 | "plugins": [], 42 | "port": 443, 43 | "protocol": "https", 44 | "routes": [ 45 | { 46 | "id": "744254cb-7b0f-527d-a9e3-41998313e8fd", 47 | "methods": [ 48 | "GET" 49 | ], 50 | "name": "multi-service-api_users_get", 51 | "paths": [ 52 | "~/users$" 53 | ], 54 | "plugins": [], 55 | "regex_priority": 200, 56 | "strip_path": false, 57 | "tags": [ 58 | "OAS3_import", 59 | "OAS3file_21-no-doc-servers.yaml" 60 | ] 61 | } 62 | ], 63 | "tags": [ 64 | "OAS3_import", 65 | "OAS3file_21-no-doc-servers.yaml" 66 | ] 67 | } 68 | ], 69 | "upstreams": [] 70 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/15-circular-requestBody-schema.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "some.random.url", 6 | "id": "68804eaf-310b-508e-ae22-fe6a7b9ab716", 7 | "name": "testing-circular", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "761badfc-6c93-585c-a5ed-cee7559aad27", 15 | "methods": [ 16 | "POST" 17 | ], 18 | "name": "testing-circular_testing_post", 19 | "paths": [ 20 | "~/testing$" 21 | ], 22 | "plugins": [ 23 | { 24 | "config": { 25 | "allowed_content_types": [ 26 | "application/json" 27 | ], 28 | "body_schema": "{\"$ref\":\"#/definitions/TestRequest\",\"definitions\":{\"CircularEnd\":{\"allOf\":[{\"$ref\":\"#/definitions/CircularStart\"}]},\"CircularStart\":{\"allOf\":[{\"$ref\":\"#/definitions/CircularEnd\"}]},\"TestRequest\":{\"allOf\":[{\"$ref\":\"#/definitions/CircularStart\"}]}}}", 29 | "verbose_response": true, 30 | "version": "draft4" 31 | }, 32 | "enabled": true, 33 | "id": "72eaa98a-0562-5669-9ee8-45d0fa479e7d", 34 | "name": "request-validator", 35 | "tags": [ 36 | "OAS3_import", 37 | "OAS3file_15-circular-requestBody-schema.yaml" 38 | ], 39 | "version": "draft4" 40 | } 41 | ], 42 | "regex_priority": 200, 43 | "strip_path": false, 44 | "tags": [ 45 | "OAS3_import", 46 | "OAS3file_15-circular-requestBody-schema.yaml" 47 | ] 48 | } 49 | ], 50 | "tags": [ 51 | "OAS3_import", 52 | "OAS3file_15-circular-requestBody-schema.yaml" 53 | ] 54 | } 55 | ], 56 | "upstreams": [] 57 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/08-route-defaults-overrides.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-route-defaults can be specified and will be honored on each level 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: Simple API overview 6 | version: v2 7 | servers: 8 | - url: https://server1.com/ 9 | x-kong-route-defaults: 10 | regex_priority: 100 11 | # routes cannot have a foreign key to a service (is set by the conversion) 12 | service: delete-me 13 | paths: 14 | /path1: 15 | get: 16 | # should get the document level defaults 17 | operationId: uses-doc-defaults 18 | summary: List API versions 19 | responses: 20 | '200': 21 | description: |- 22 | 200 response 23 | /path1/{param}: 24 | get: 25 | # should get the document level defaults, but with a path-parameter the 26 | # regex_priority is set to 1 less than the defaults given 27 | operationId: uses-doc-defaults-with-path-param 28 | summary: List API versions 29 | responses: 30 | '200': 31 | description: |- 32 | 200 response 33 | /path2: 34 | # specify new defaults to override document level 35 | x-kong-route-defaults: 36 | regex_priority: 200 37 | # routes cannot have a foreign key to a service (is set by the conversion) 38 | service: delete-me 39 | get: 40 | operationId: uses-path-defaults 41 | summary: List API versions 42 | responses: 43 | '200': 44 | description: |- 45 | 200 response 46 | post: 47 | # specify new defaults to override path level 48 | x-kong-route-defaults: 49 | regex_priority: 300 50 | # routes cannot have a foreign key to a service (is set by the conversion) 51 | service: delete-me 52 | # strip_path defaults to false, but can be set to true 53 | strip_path: true 54 | operationId: uses-ops-defaults 55 | summary: List API versions 56 | responses: 57 | '200': 58 | description: |- 59 | 200 response 60 | -------------------------------------------------------------------------------- /deckformat/history.go: -------------------------------------------------------------------------------- 1 | package deckformat 2 | 3 | import "github.com/kong/go-apiops/jsonbasics" 4 | 5 | // 6 | // 7 | // Section for tracking history of the file 8 | // 9 | // 10 | 11 | // HistoryGet returns a the history info array. If there is none, or if filedata is nil, 12 | // it will return an empty one. 13 | func HistoryGet(filedata map[string]interface{}) (historyArray []interface{}) { 14 | if filedata == nil || filedata[HistoryKey] == nil { 15 | historyInfo := make([]interface{}, 0) 16 | return historyInfo 17 | } 18 | 19 | trackInfo, err := jsonbasics.ToArray(filedata[HistoryKey]) 20 | if err != nil { 21 | // the entry wasn't an array, so wrap it in one 22 | trackInfo = []interface{}{filedata[HistoryKey]} 23 | } 24 | 25 | // Return a copy 26 | return jsonbasics.DeepCopyArray(trackInfo) 27 | } 28 | 29 | // HistorySet sets the history info array. Setting to nil will delete the history. 30 | func HistorySet(filedata map[string]interface{}, historyArray []interface{}) { 31 | if historyArray == nil { 32 | HistoryClear(filedata) 33 | return 34 | } 35 | filedata[HistoryKey] = historyArray 36 | 37 | // TODO: remove this after the we get support for metafields in deck 38 | HistoryClear(filedata) 39 | } 40 | 41 | // HistoryAppend appends an entry (if non-nil) to the history info array. If there is 42 | // no array, it will create one. 43 | func HistoryAppend(filedata map[string]interface{}, newEntry interface{}) { 44 | hist := HistoryGet(filedata) 45 | hist = append(hist, newEntry) 46 | HistorySet(filedata, hist) 47 | } 48 | 49 | func HistoryClear(filedata map[string]interface{}) { 50 | delete(filedata, HistoryKey) 51 | } 52 | 53 | // HistoryNewEntry returns a new JSONobject with tool version and command keys set. 54 | func HistoryNewEntry(cmd string) map[string]interface{} { 55 | return map[string]interface{}{ 56 | "tool": ToolVersionString(), 57 | "command": cmd, 58 | // For now: no timestamps in git-ops! 59 | // "time": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), // ISO8601 format 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/20-ignore-circular-references.expected_no_circular.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "some.random.url", 6 | "id": "68804eaf-310b-508e-ae22-fe6a7b9ab716", 7 | "name": "testing-circular", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "ce628b39-dec4-5171-a149-df610ebca091", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "testing-circular_categories_get", 19 | "paths": [ 20 | "~/categories$" 21 | ], 22 | "plugins": [ 23 | { 24 | "config": { 25 | "parameter_schema": [ 26 | { 27 | "explode": true, 28 | "in": "query", 29 | "name": "classificationId", 30 | "required": true, 31 | "schema": "{\"format\":\"int32\",\"type\":\"integer\"}", 32 | "style": "form" 33 | } 34 | ], 35 | "verbose_response": true, 36 | "version": "draft4" 37 | }, 38 | "enabled": true, 39 | "id": "7ed4fd33-1079-5822-84cf-fcfd81b22dd5", 40 | "name": "request-validator", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_20-ignore-circular-references.circular-yaml" 44 | ], 45 | "version": "draft4" 46 | } 47 | ], 48 | "regex_priority": 200, 49 | "strip_path": false, 50 | "tags": [ 51 | "OAS3_import", 52 | "OAS3file_20-ignore-circular-references.circular-yaml" 53 | ] 54 | } 55 | ], 56 | "tags": [ 57 | "OAS3_import", 58 | "OAS3file_20-ignore-circular-references.circular-yaml" 59 | ] 60 | } 61 | ], 62 | "upstreams": [] 63 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/20-ignore-circular-references.generated_no_circular.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "some.random.url", 6 | "id": "68804eaf-310b-508e-ae22-fe6a7b9ab716", 7 | "name": "testing-circular", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "ce628b39-dec4-5171-a149-df610ebca091", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "testing-circular_categories_get", 19 | "paths": [ 20 | "~/categories$" 21 | ], 22 | "plugins": [ 23 | { 24 | "config": { 25 | "parameter_schema": [ 26 | { 27 | "explode": true, 28 | "in": "query", 29 | "name": "classificationId", 30 | "required": true, 31 | "schema": "{\"format\":\"int32\",\"type\":\"integer\"}", 32 | "style": "form" 33 | } 34 | ], 35 | "verbose_response": true, 36 | "version": "draft4" 37 | }, 38 | "enabled": true, 39 | "id": "7ed4fd33-1079-5822-84cf-fcfd81b22dd5", 40 | "name": "request-validator", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_20-ignore-circular-references.circular-yaml" 44 | ], 45 | "version": "draft4" 46 | } 47 | ], 48 | "regex_priority": 200, 49 | "strip_path": false, 50 | "tags": [ 51 | "OAS3_import", 52 | "OAS3file_20-ignore-circular-references.circular-yaml" 53 | ] 54 | } 55 | ], 56 | "tags": [ 57 | "OAS3_import", 58 | "OAS3file_20-ignore-circular-references.circular-yaml" 59 | ] 60 | } 61 | ], 62 | "upstreams": [] 63 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/19-request-validator-plugin-op-params-override-path-params.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "backend.com", 6 | "id": "730d612d-914b-5fe8-8ead-e6aa654318ef", 7 | "name": "example", 8 | "path": "/path", 9 | "plugins": [], 10 | "port": 80, 11 | "protocol": "http", 12 | "routes": [ 13 | { 14 | "id": "bee0be08-646a-562a-91b9-71737169585b", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "example_test-common-param-common-param_get", 19 | "paths": [ 20 | "~/test/common-param/(?[^#?/]+)$" 21 | ], 22 | "plugins": [ 23 | { 24 | "config": { 25 | "parameter_schema": [ 26 | { 27 | "explode": false, 28 | "in": "path", 29 | "name": "common_param", 30 | "required": true, 31 | "schema": "{\"type\":\"integer\"}", 32 | "style": "simple" 33 | } 34 | ], 35 | "verbose_response": true, 36 | "version": "draft4" 37 | }, 38 | "enabled": true, 39 | "id": "3bcb9a87-847d-5ccf-93dc-b2aa1b32b77d", 40 | "name": "request-validator", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_19-request-validator-plugin-op-params-override-path-params.yaml" 44 | ] 45 | } 46 | ], 47 | "regex_priority": 100, 48 | "strip_path": false, 49 | "tags": [ 50 | "OAS3_import", 51 | "OAS3file_19-request-validator-plugin-op-params-override-path-params.yaml" 52 | ] 53 | } 54 | ], 55 | "tags": [ 56 | "OAS3_import", 57 | "OAS3file_19-request-validator-plugin-op-params-override-path-params.yaml" 58 | ] 59 | } 60 | ], 61 | "upstreams": [] 62 | } -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | concurrency: 4 | # for PR's cancel the running task, if another commit is pushed 5 | group: ${{ github.workflow }} ${{ github.ref }} 6 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 7 | 8 | on: 9 | # build on PR and push-to-main. This works for short-lived branches, and saves 10 | # CPU cycles on duplicated tests. 11 | # For long-lived branches that diverge, you'll want to run on all pushes, not 12 | # just on push-to-main. 13 | pull_request: {} 14 | push: 15 | tags: 16 | - v* 17 | branches: 18 | - main 19 | 20 | permissions: 21 | contents: read 22 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 23 | # pull-requests: read 24 | 25 | jobs: 26 | golangci: 27 | name: lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/setup-go@v5 31 | # with: 32 | # go-version: 1.17 33 | - uses: actions/checkout@v4 34 | - name: golangci-lint 35 | uses: golangci/golangci-lint-action@v6 36 | # with: 37 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 38 | # version: v3.0 39 | 40 | # Optional: working directory, useful for monorepos 41 | # working-directory: somedir 42 | 43 | # Optional: golangci-lint command line arguments. 44 | # args: --issues-exit-code=0 45 | 46 | # Optional: show only new issues if it's a pull request. The default value is `false`. 47 | # only-new-issues: true 48 | 49 | # Optional: if set to true then the all caching functionality will be complete disabled, 50 | # takes precedence over all other caching options. 51 | # skip-cache: true 52 | 53 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 54 | # skip-pkg-cache: true 55 | 56 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 57 | # skip-build-cache: true 58 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/14-no-request-validator-plugin.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "httpbin.org", 6 | "id": "069ec36b-308c-5cf4-8536-b7bad57907ea", 7 | "name": "mock-target-api", 8 | "path": "/anything", 9 | "plugins": [], 10 | "port": 80, 11 | "protocol": "http", 12 | "routes": [ 13 | { 14 | "id": "34dc34c8-2b0e-539b-9a6c-8b0ff4cd9fbb", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "mock-target-api_gethelp", 19 | "paths": [ 20 | "~/help$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_14-no-request-validator-plugin.yaml" 28 | ] 29 | }, 30 | { 31 | "id": "34dc34c8-2b0e-539b-9a6c-8b0ff4cd9fbb", 32 | "methods": [ 33 | "GET" 34 | ], 35 | "name": "mock-target-api_gethelp", 36 | "paths": [ 37 | "~/user$" 38 | ], 39 | "plugins": [ 40 | { 41 | "config": { 42 | "allowed_content_types": [ 43 | "application/xml" 44 | ], 45 | "body_schema": "{}", 46 | "version": "draft4" 47 | }, 48 | "id": "40037e1a-ea6d-5d78-85b2-7f502e4a60e9", 49 | "name": "request-validator", 50 | "tags": [ 51 | "OAS3_import", 52 | "OAS3file_14-no-request-validator-plugin.yaml" 53 | ] 54 | } 55 | ], 56 | "regex_priority": 200, 57 | "strip_path": false, 58 | "tags": [ 59 | "OAS3_import", 60 | "OAS3file_14-no-request-validator-plugin.yaml" 61 | ] 62 | } 63 | ], 64 | "tags": [ 65 | "OAS3_import", 66 | "OAS3file_14-no-request-validator-plugin.yaml" 67 | ] 68 | } 69 | ], 70 | "upstreams": [] 71 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/21-nullable-path-param.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: OData style API with nullable path params 4 | version: 0.0.1 5 | servers: 6 | - url: http://backend.com/path 7 | paths: 8 | /custom/{customId}: 9 | parameters: 10 | - name: customId 11 | in: path 12 | required: true 13 | description: Custom ID 14 | schema: 15 | type: string 16 | maxLength: 4 17 | get: 18 | summary: Read the custom information 19 | description: Read the custom information of the API 20 | tags: 21 | - Metadata 22 | responses: 23 | "200": 24 | description: Retrieved metadata 25 | 4XX: 26 | description: all 4XX errors 27 | operationId: customdata 28 | ? "/CustomWarehouseData(Warehouse='{Warehouse}',Product='{Product}',Location='{Location}')" 29 | : parameters: 30 | - name: Warehouse 31 | in: path 32 | required: true 33 | description: ID of warehouse 34 | schema: 35 | type: string 36 | maxLength: 4 37 | minLength: 0 38 | - name: Product 39 | in: path 40 | required: true 41 | description: Product Number 42 | schema: 43 | type: string 44 | maxLength: 40 45 | - name: Location 46 | in: path 47 | required: true 48 | description: Product Number 49 | schema: 50 | type: string 51 | maxLength: 40 52 | get: 53 | summary: List entities from Warehouse based on product ID, location and warehouse ID 54 | parameters: 55 | - name: Location #overrides the param defined in path level 56 | in: path 57 | required: true 58 | description: Product Number 59 | schema: 60 | type: string 61 | maxLength: 40 62 | minLength: 0 63 | tags: 64 | - WarehouseProductData 65 | responses: 66 | "200": 67 | description: Retrieved product entities 68 | 4XX: 69 | description: all 4XX errors 70 | operationId: customproductdata -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - asciicheck 4 | #- depguard 5 | - dogsled 6 | - durationcheck 7 | - errcheck 8 | - exhaustive 9 | # - exportloopref 10 | - gci 11 | - goconst 12 | - gofmt 13 | - gofumpt 14 | - goimports 15 | - gomodguard 16 | - gosec 17 | - govet 18 | - importas 19 | - lll 20 | - megacheck 21 | - misspell 22 | - nakedret 23 | - nilerr 24 | - nolintlint 25 | - predeclared 26 | #- revive 27 | - stylecheck 28 | - unconvert 29 | - unparam 30 | - wastedassign 31 | issues: 32 | exclude-rules: 33 | - linters: 34 | - gosec 35 | text: "weak cryptographic primitive" 36 | path: "state/indexers/md5Indexer.*" 37 | - linters: 38 | - gosec 39 | text: "weak random number generator" 40 | path: _test\.go 41 | - linters: 42 | - errcheck 43 | text: "Error return value" # ignore err not checked in test files 44 | path: _test\.go 45 | - linters: 46 | - gosec 47 | text: "Expect WriteFile permissions to be 0600 or less" 48 | path: file/codegen/main.go 49 | # ignore unused warnings in test utils files 50 | - linters: 51 | - unused 52 | - unparam 53 | path: test_.* 54 | # ignore SA1019 deprecation warning 55 | - linters: 56 | - staticcheck 57 | text: "SA1019.*" 58 | # ignore formatting warnings in cmd/root.go due to nolint statements 59 | - linters: 60 | - gofumpt 61 | text: "File is not `gofumpt`-ed" 62 | path: cmd/root.go 63 | - linters: 64 | - gofmt 65 | text: "File is not `gofmt`-ed with `-s`" 66 | path: cmd/root.go 67 | - linters: 68 | - goimports 69 | text: "File is not `goimports`-ed" 70 | path: cmd/root.go 71 | 72 | linters-settings: 73 | goconst: 74 | min-occurrences: 10 75 | gomodguard: 76 | blocked: 77 | modules: 78 | - github.com/ghodss/yaml: 79 | recommendations: 80 | - sigs.k8s.io/yaml 81 | - gopkg.in/yaml.v2: 82 | recommendations: 83 | - sigs.k8s.io/yaml 84 | # The below package is required for the JSONpath library, see 85 | # https://github.com/vmware-labs/yaml-jsonpath 86 | # - gopkg.in/yaml.v3: 87 | # recommendations: 88 | # - sigs.k8s.io/yaml 89 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/17-request-validator-plugin-oneOf-usage.yaml: -------------------------------------------------------------------------------- 1 | # When the request-validator is added without a body or parameter schema 2 | # the generator should automatically generate it. 3 | 4 | openapi: 3.0.2 5 | 6 | info: 7 | title: Example 8 | version: 1.0.0 9 | 10 | servers: 11 | - url: http://backend.com/path 12 | 13 | x-kong-plugin-request-validator: {} 14 | 15 | paths: 16 | /params/test: 17 | get: 18 | x-kong-plugin-request-validator: 19 | enabled: true 20 | config: 21 | body_schema: '{}' 22 | parameters: 23 | - in: query 24 | name: queryid 25 | schema: 26 | type: number 27 | oneOf: 28 | - type: integer 29 | example: 10 30 | - type: number 31 | example: 2.5 32 | required: true 33 | - in: header 34 | name: testHeader 35 | schema: 36 | $ref: '#/components/schemas/headerType' 37 | required: true 38 | - in: header 39 | name: secondTestHeader 40 | schema: 41 | $ref: '#/components/schemas/secondHeaderType' 42 | required: true 43 | - in: query 44 | name: testArrayOne 45 | schema: 46 | type: array 47 | items: 48 | oneOf: 49 | - type: string 50 | - type: integer 51 | required: true 52 | - in: query 53 | name: testArrayTwo 54 | schema: 55 | type: array 56 | items: 57 | oneOf: 58 | - $ref: '#/components/schemas/headerType' 59 | - $ref: '#/components/schemas/newType' 60 | required: true 61 | components: 62 | schemas: 63 | headerType: 64 | type: string 65 | oneOf: 66 | - $ref: '#/components/schemas/stringType' 67 | - $ref: '#/components/schemas/numberType' 68 | secondHeaderType: 69 | $ref: '#/components/schemas/headerType' 70 | stringType: 71 | type: string 72 | example: "10" 73 | numberType: 74 | type: number 75 | example: 2.5 76 | newType: 77 | type: string 78 | oneOf: 79 | - type: string 80 | - type: number 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/09a-plugins-with-consumers.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-plugin- can carry a consumer id. Those plugins must be added 2 | # on the top-level of the output because they have 2 foreign keys. 3 | # 4 | # the top-level plugins array should be lexically sorted by plugin-name + ID 5 | 6 | openapi: '3.0.0' 7 | info: 8 | title: Simple API overview 9 | version: v2 10 | servers: 11 | - url: https://server1.com/ 12 | 13 | x-kong-plugin-request-termination: 14 | # name will be determined from the tag 15 | # name: request-termination; 16 | config: 17 | status_code: 403 18 | message: So long and thanks for all the fish! 19 | # foreign keys to consumer 20 | consumer: johndoe1 21 | 22 | paths: 23 | /path1: 24 | get: 25 | # This operation should not get a plugin, but rely on the document level 26 | # service-entity based plugin 27 | operationId: uses-doc-plugin 28 | summary: List API versions 29 | responses: 30 | '200': 31 | description: |- 32 | 200 response 33 | /path2: 34 | # This plugin gets attached to a route, overriding the document service-entity 35 | # attached plugin 36 | x-kong-plugin-request-termination: 37 | config: 38 | status_code: 403 39 | message: The answer to life, the universe, and everything! 40 | # foreign keys to consumer 41 | consumer: johndoe2 42 | get: 43 | # gets the plugin from the path-level, attached on route-entity 44 | operationId: uses-path-plugin 45 | summary: List API versions 46 | responses: 47 | '200': 48 | description: |- 49 | 200 response 50 | post: 51 | # This plugin gets attached to a route, taking precedence over the path-level 52 | # plugin. On the route it will override the document level service-entity 53 | # attached plugin 54 | x-kong-plugin-request-termination: 55 | config: 56 | status_code: 403 57 | message: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. 58 | # foreign keys to consumer 59 | consumer: johndoe3 60 | operationId: uses-ops-plugin 61 | summary: List API versions 62 | responses: 63 | '200': 64 | description: |- 65 | 200 response 66 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/18-request-validator-plugin-path-params-outside-ops.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "backend.com", 6 | "id": "730d612d-914b-5fe8-8ead-e6aa654318ef", 7 | "name": "example", 8 | "path": "/path", 9 | "plugins": [], 10 | "port": 80, 11 | "protocol": "http", 12 | "routes": [ 13 | { 14 | "id": "bee0be08-646a-562a-91b9-71737169585b", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "example_test-common-param-common-param_get", 19 | "paths": [ 20 | "~/test/common-param/(?[^#?/]+)$" 21 | ], 22 | "plugins": [ 23 | { 24 | "config": { 25 | "parameter_schema": [ 26 | { 27 | "explode": false, 28 | "in": "path", 29 | "name": "common_param", 30 | "required": true, 31 | "schema": "{\"type\":\"integer\"}", 32 | "style": "simple" 33 | }, 34 | { 35 | "explode": true, 36 | "in": "query", 37 | "name": "metadata", 38 | "required": false, 39 | "schema": "{\"type\":\"boolean\"}", 40 | "style": "form" 41 | } 42 | ], 43 | "verbose_response": true, 44 | "version": "draft4" 45 | }, 46 | "enabled": true, 47 | "id": "3bcb9a87-847d-5ccf-93dc-b2aa1b32b77d", 48 | "name": "request-validator", 49 | "tags": [ 50 | "OAS3_import", 51 | "OAS3file_18-request-validator-plugin-path-params-outside-ops.yaml" 52 | ] 53 | } 54 | ], 55 | "regex_priority": 100, 56 | "strip_path": false, 57 | "tags": [ 58 | "OAS3_import", 59 | "OAS3file_18-request-validator-plugin-path-params-outside-ops.yaml" 60 | ] 61 | } 62 | ], 63 | "tags": [ 64 | "OAS3_import", 65 | "OAS3file_18-request-validator-plugin-path-params-outside-ops.yaml" 66 | ] 67 | } 68 | ], 69 | "upstreams": [] 70 | } -------------------------------------------------------------------------------- /patch/patch-file.yml: -------------------------------------------------------------------------------- 1 | # This file describes the possible format of the supported patch operations by 2 | # the CLI command `deck patch`. 3 | 4 | # The current version only implements 'deck' format 5 | _format_version: 1.0 6 | 7 | # patches is an array of patches, to be applied in order 8 | # the "format" key is optional. It is auto-detected based on the presence of the 9 | # "patch", "operations", or "values" fields 10 | patches: 11 | # Patch format: deck (these patches CANNOT error) 12 | # Media-Type: n.a. 13 | # Notes: 14 | # - this is equivalent to the `--value` flag on the CLI 15 | - format: deck 16 | selectors: 17 | - "a jsonpath query" # defaults to "$" 18 | values: 19 | # if values isn't an object but an array, then the array entries will be added 20 | # to the target array selected by the selectors. 21 | field1: any-json-value 22 | field2: any-json-value 23 | remove: ["field3", "field4"] # removes the fields, same as an empty value in the CLI 24 | 25 | 26 | # NOT IMPLEMENTED YET! 27 | # Patch format: RFC-7396 (these patches CANNOT error) 28 | # Media-Type: application/merge-patch+json 29 | # Notes: 30 | # - if the target is not an object, the value is dropped and replaced by an empty object 31 | # before the patch is applied 32 | # - if the patch is NOT an object, then the target is replaced with the patch. 33 | # NOT IMPLEMENTED YET! 34 | - format: application/merge-patch+json 35 | selectors: 36 | - "a jsonpath query" # defaults to "$" 37 | patch: # this field contains the "merge-patch" object as per the RFC 38 | field1: This object is the 39 | field2: merge-patch to apply 40 | 41 | 42 | # NOT IMPLEMENTED YET! 43 | # Patch format: RFC-6902 (these patches CAN error) 44 | # Media-Type: application/json-patch+json 45 | # Notes: 46 | # - if "path" == "" then it targets the entire document, see https://www.rfc-editor.org/rfc/rfc6901#section-5 47 | # NOT IMPLEMENTED YET! 48 | - format: application/json-patch+json 49 | selectors: 50 | - "a jsonpath query" # defaults to "$" 51 | operations: # this field contains the "json-patch" array as per the RFC 52 | - op: add # one of; "add", "remove", "replace", "move", "copy", "test" 53 | path: /a/b/c # 'path' always is a JSON pointer; RFC-6901 54 | value: This is the new value 55 | - op: delete 56 | path: /delete/me 57 | -------------------------------------------------------------------------------- /logbasics/logbasics.go: -------------------------------------------------------------------------------- 1 | // This package provides a global logger instance that is used by other packages 2 | // within go-apiops. The default is to discard any logging. 3 | // It uses go-logr/logr. The logger instance can be set by calling SetLogger. 4 | // 5 | // General behaviour; 6 | // * Errors will not be logged, but returned instead. Logging those is up to the caller. 7 | // * The library does not use verbosity level 0 8 | // * level 1 is used for informational messages (when calling `Info`) 9 | // * level 2 is used for debug messages (when calling `Debug`) 10 | package logbasics 11 | 12 | import ( 13 | "log" 14 | "os" 15 | 16 | "github.com/go-logr/logr" 17 | "github.com/go-logr/stdr" 18 | ) 19 | 20 | var ( 21 | globalLogger logr.Logger 22 | defaultLogger *logr.Logger 23 | ) 24 | 25 | // Info logs an informational message ("info" at verbosity level 1). 26 | func Info(msg string, keysAndValues ...interface{}) { 27 | globalLogger.V(1).Info(msg, keysAndValues...) 28 | } 29 | 30 | // Debug logs a debug message ("info" at verbosity level 2). 31 | func Debug(msg string, keysAndValues ...interface{}) { 32 | globalLogger.V(2).Info(msg, keysAndValues...) 33 | } 34 | 35 | // Error logs an error message. Preferably errors should bubble up to the caller, 36 | // so only if that is not possible, this method should be used. 37 | func Error(err error, msg string, keysAndValues ...interface{}) { 38 | globalLogger.Error(err, msg, keysAndValues...) 39 | } 40 | 41 | // SetLogger set the logger instance to use for logging. 42 | // Setting it to nil will disable logging. 43 | func SetLogger(l *logr.Logger) { 44 | if l == nil { 45 | globalLogger = logr.Discard() 46 | } else { 47 | globalLogger = *l 48 | } 49 | } 50 | 51 | // GetLogger returns the logger instance to use for logging. 52 | func GetLogger() logr.Logger { 53 | return globalLogger 54 | } 55 | 56 | func newStderrLogger(flags int) stdr.StdLogger { 57 | return log.New(os.Stderr, "", flags) 58 | } 59 | 60 | // Initialize creates and sets a logger instance to log to stderr. 61 | // Any follow up calls to Initialize will ignore the parameters and set the logger to 62 | // the initially created instance. 63 | // see https://pkg.go.dev/log#pkg-constants for the flag values 64 | func Initialize(flags int, verbosity int) { 65 | if defaultLogger == nil { 66 | stdr.SetVerbosity(verbosity) 67 | l := stdr.New(newStderrLogger(flags)) 68 | defaultLogger = &l 69 | } 70 | SetLogger(defaultLogger) 71 | } 72 | 73 | func init() { 74 | SetLogger(nil) // discard logs by default 75 | } 76 | -------------------------------------------------------------------------------- /cmd/merge.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/kong/go-apiops/deckformat" 9 | "github.com/kong/go-apiops/filebasics" 10 | "github.com/kong/go-apiops/logbasics" 11 | "github.com/kong/go-apiops/merge" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // Executes the CLI command "merge" 16 | func executeMerge(cmd *cobra.Command, args []string) error { 17 | verbosity, _ := cmd.Flags().GetInt("verbose") 18 | logbasics.Initialize(log.LstdFlags, verbosity) 19 | 20 | outputFilename, err := cmd.Flags().GetString("output-file") 21 | if err != nil { 22 | return fmt.Errorf("failed getting cli argument 'output-file'; %w", err) 23 | } 24 | 25 | var outputFormat string 26 | { 27 | outputFormat, err = cmd.Flags().GetString("format") 28 | if err != nil { 29 | return fmt.Errorf("failed getting cli argument 'format'; %w", err) 30 | } 31 | outputFormat = strings.ToUpper(outputFormat) 32 | } 33 | 34 | // do the work: read/merge 35 | merged, info, err := merge.Files(args) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | historyEntry := deckformat.HistoryNewEntry("merge") 41 | historyEntry["output"] = outputFilename 42 | historyEntry["files"] = info 43 | deckformat.HistoryClear(merged) 44 | deckformat.HistoryAppend(merged, historyEntry) 45 | 46 | return filebasics.WriteSerializedFile(outputFilename, merged, filebasics.OutputFormat(outputFormat)) 47 | } 48 | 49 | // 50 | // 51 | // Define the CLI data for the merge command 52 | // 53 | // 54 | 55 | var mergeCmd = &cobra.Command{ 56 | Use: "merge [flags] filename [...filename]", 57 | Short: "Merges multiple decK files into one", 58 | Long: `Merges multiple decK files into one. 59 | 60 | The files can be either json or yaml format. Will merge all top-level arrays by simply 61 | concatenating them. Any other keys will be copied. The files will be processed in the order 62 | provided. No checks on content will be done, eg. duplicates, nor any validations. 63 | 64 | If the input files are not compatible an error will be returned. Compatibility is 65 | determined by the '_transform' and '_format_version' fields.`, 66 | RunE: executeMerge, 67 | Args: cobra.MinimumNArgs(1), 68 | } 69 | 70 | func init() { 71 | rootCmd.AddCommand(mergeCmd) 72 | mergeCmd.Flags().StringP("output-file", "o", "-", "output file to write. Use - to write to stdout") 73 | mergeCmd.Flags().StringP("format", "", string(filebasics.OutputFormatYaml), "output format: "+ 74 | string(filebasics.OutputFormatJSON)+" or "+string(filebasics.OutputFormatYaml)) 75 | } 76 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/09-generic-plugins.yaml: -------------------------------------------------------------------------------- 1 | # x-kong-plugin- are accepted on each level. The document level plugins 2 | # end up on the service. The path and operation level plugins on the route. 3 | # 4 | # if the same plugin is on a path and operation, the operation one wins (to be 5 | # attached to a route). 6 | # 7 | # Plugin-defaults cannot have foreign-keys to service/route entities (ignored) 8 | 9 | openapi: '3.0.0' 10 | info: 11 | title: Simple API overview 12 | version: v2 13 | servers: 14 | - url: https://server1.com/ 15 | 16 | x-kong-plugin-request-termination: 17 | # name will be determined from the tag 18 | # name: request-termination; 19 | config: 20 | status_code: 403 21 | message: So long and thanks for all the fish! 22 | # foreign keys to be ignored 23 | service: delete-me 24 | route: delete-me-to 25 | 26 | paths: 27 | /path1: 28 | get: 29 | # This operation should not get a plugin, but rely on the document level 30 | # service-entity based plugin 31 | operationId: uses-doc-plugin 32 | summary: List API versions 33 | responses: 34 | '200': 35 | description: |- 36 | 200 response 37 | /path2: 38 | # This plugin gets attached to a route, overriding the document service-entity 39 | # attached plugin 40 | x-kong-plugin-request-termination: 41 | config: 42 | status_code: 403 43 | message: The answer to life, the universe, and everything! 44 | # foreign keys to be ignored 45 | service: delete-me 46 | route: delete-me-to 47 | get: 48 | # gets the plugin from the path-level, attached on route-entity 49 | operationId: uses-path-plugin 50 | summary: List API versions 51 | responses: 52 | '200': 53 | description: |- 54 | 200 response 55 | post: 56 | # This plugin gets attached to a route, taking precedence over the path-level 57 | # plugin. On the route it will override the document level service-entity 58 | # attached plugin 59 | x-kong-plugin-request-termination: 60 | config: 61 | status_code: 403 62 | message: For a moment, nothing happened. Then, after a second or so, nothing continued to happen. 63 | # foreign keys to be ignored 64 | service: delete-me 65 | route: delete-me-to 66 | operationId: uses-ops-plugin 67 | summary: List API versions 68 | responses: 69 | '200': 70 | description: |- 71 | 200 response 72 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/08-route-defaults-overrides.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "server1.com", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "44777959-fdbe-5873-a5c1-beabbc822656", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "simple-api-overview_uses-doc-defaults", 19 | "paths": [ 20 | "~/path1$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 100, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_08-route-defaults-overrides.yaml" 28 | ] 29 | }, 30 | { 31 | "id": "b6ab5ad9-ed23-5957-9d57-d071678162d1", 32 | "methods": [ 33 | "GET" 34 | ], 35 | "name": "simple-api-overview_uses-doc-defaults-with-path-param", 36 | "paths": [ 37 | "~/path1/(?[^#?/]+)$" 38 | ], 39 | "plugins": [], 40 | "regex_priority": 99, 41 | "strip_path": false, 42 | "tags": [ 43 | "OAS3_import", 44 | "OAS3file_08-route-defaults-overrides.yaml" 45 | ] 46 | }, 47 | { 48 | "id": "a8cf87ef-dae0-5948-93e4-48f579fe12a0", 49 | "methods": [ 50 | "GET" 51 | ], 52 | "name": "simple-api-overview_uses-path-defaults", 53 | "paths": [ 54 | "~/path2$" 55 | ], 56 | "plugins": [], 57 | "regex_priority": 200, 58 | "strip_path": false, 59 | "tags": [ 60 | "OAS3_import", 61 | "OAS3file_08-route-defaults-overrides.yaml" 62 | ] 63 | }, 64 | { 65 | "id": "5a98eef7-b0d2-572e-8656-4654a89c4179", 66 | "methods": [ 67 | "POST" 68 | ], 69 | "name": "simple-api-overview_uses-ops-defaults", 70 | "paths": [ 71 | "~/path2$" 72 | ], 73 | "plugins": [], 74 | "regex_priority": 300, 75 | "strip_path": true, 76 | "tags": [ 77 | "OAS3_import", 78 | "OAS3file_08-route-defaults-overrides.yaml" 79 | ] 80 | } 81 | ], 82 | "tags": [ 83 | "OAS3_import", 84 | "OAS3file_08-route-defaults-overrides.yaml" 85 | ] 86 | } 87 | ], 88 | "upstreams": [] 89 | } -------------------------------------------------------------------------------- /patch/deckpatchfile.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kong/go-apiops/deckformat" 7 | "github.com/kong/go-apiops/filebasics" 8 | "github.com/kong/go-apiops/jsonbasics" 9 | "github.com/kong/go-apiops/logbasics" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // DeckPatchFile represents a list of patches. 14 | type DeckPatchFile struct { 15 | VersionMajor int // 0 if not present 16 | VersionMinor int // 0 if not present 17 | Patches []DeckPatch 18 | } 19 | 20 | // ParseFile parses a patchfile. Any non-object in the 'patches' array will be 21 | // ignored. If the array doesn't exist, it returns an empty array. 22 | func (patchFile *DeckPatchFile) ParseFile(filename string) error { 23 | data, err := filebasics.DeserializeFile(filename) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if data[deckformat.VersionKey] != nil { 29 | logbasics.Debug("parsed patch file", "file", filename, "version", data[deckformat.VersionKey]) 30 | patchFile.VersionMajor, patchFile.VersionMinor, err = deckformat.ParseFormatVersion(data) 31 | if err != nil { 32 | return fmt.Errorf("%s: has an invalid "+deckformat.VersionKey+" specified; %w", filename, err) 33 | } 34 | } else { 35 | logbasics.Debug("parsed unversioned patch-file", "file", filename) 36 | } 37 | 38 | patchesRead, err := jsonbasics.GetObjectArrayField(data, "patches") 39 | if err != nil { 40 | return fmt.Errorf("%s: field 'patches' is not an array; %w", filename, err) 41 | } 42 | 43 | patchFile.Patches = make([]DeckPatch, 0) 44 | for i, patch := range patchesRead { 45 | if patch["values"] != nil || patch["remove"] != nil { 46 | // deck patch 47 | var patchParsed DeckPatch 48 | err := patchParsed.Parse(patch, fmt.Sprintf("%s: patches[%d]", filename, i)) 49 | if err != nil { 50 | return err 51 | } 52 | patchFile.Patches = append(patchFile.Patches, patchParsed) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Apply applies the set of patches on the yaml.Node given. 60 | func (patchFile *DeckPatchFile) Apply(yamlData *yaml.Node) error { 61 | for i, patch := range patchFile.Patches { 62 | err := patch.ApplyToNodes(yamlData) 63 | if err != nil { 64 | return fmt.Errorf("failed to apply patch %d; %w", i, err) 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | // MustApply applies the set of patches on the yaml.Node given. Same as Apply, but 71 | // in case of an error it will panic. 72 | // 'source' will be used to format the error in case of a panic. 73 | func (patchFile *DeckPatchFile) MustApply(yamlData *yaml.Node, source string) { 74 | err := patchFile.Apply(yamlData) 75 | if err != nil { 76 | panic(fmt.Errorf("failed to apply patchfile '%s'; %w", source, err)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /yamlbasics/nodeset.go: -------------------------------------------------------------------------------- 1 | package yamlbasics 2 | 3 | import ( 4 | "github.com/kong/go-apiops/logbasics" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // 9 | // 10 | // NodeSet implementation, just a list of yaml nodes 11 | // 12 | // 13 | 14 | // represents a set of yaml nodes 15 | type NodeSet []*yaml.Node 16 | 17 | // Intersection returns the intersection of the two sets of nodes. 18 | // nil entries will be ignored. The result will a copy and have no duplicates. 19 | // The second return value is the remainder of set2 after the intersection was removed (also a copy). 20 | func (mainSet *NodeSet) Intersection(set2 NodeSet) (intersection NodeSet, remainder NodeSet) { 21 | if len(*mainSet) == 0 || len(set2) == 0 { 22 | intersection := make(NodeSet, 0) 23 | remainder := make(NodeSet, len(set2)) 24 | copy(remainder, set2) 25 | return intersection, remainder 26 | } 27 | 28 | // deduplicate 29 | seen1 := make(map[*yaml.Node]bool) 30 | for _, node := range *mainSet { 31 | if node != nil { 32 | seen1[node] = true 33 | } 34 | } 35 | 36 | intersection = make(NodeSet, 0) 37 | remainder = make(NodeSet, 0) 38 | seen2 := make(map[*yaml.Node]bool) 39 | for _, node := range set2 { 40 | if node != nil && !seen2[node] { 41 | seen2[node] = true 42 | if seen1[node] { 43 | intersection = append(intersection, node) 44 | } else { 45 | remainder = append(remainder, node) 46 | } 47 | } 48 | } 49 | logbasics.Debug("intersection", "#found", len(intersection), "#remainder", len(remainder)) 50 | return intersection, remainder 51 | } 52 | 53 | // IsIntersection returns true if all nodes in the subset also appear in the main set. 54 | // nil entries will be ignored. Returns true if subset is empty. 55 | func (mainSet *NodeSet) IsIntersection(subset NodeSet) bool { 56 | _, remainder := mainSet.Intersection(subset) 57 | return len(remainder) == 0 58 | } 59 | 60 | // Subtract returns the set of nodes that are in mainSet but not in setToSubtract. 61 | // nil entries will be ignored. The result will have no duplicates. 62 | func (mainSet *NodeSet) Subtract(setToSubtract NodeSet) NodeSet { 63 | _, remainder := setToSubtract.Intersection(*mainSet) 64 | return remainder 65 | } 66 | 67 | // Union returns the union of the two (or more) sets of nodes. 68 | // nil entries will be ignored. The result will have no duplicates. 69 | func (mainSet *NodeSet) Union(sets ...NodeSet) NodeSet { 70 | union := make(NodeSet, 0) 71 | sets = append([]NodeSet{*mainSet}, sets...) 72 | 73 | seen := make(map[*yaml.Node]bool) 74 | for _, nodeset := range sets { 75 | for _, node := range nodeset { 76 | if node != nil && !seen[node] { 77 | seen[node] = true 78 | union = append(union, node) 79 | } 80 | } 81 | } 82 | 83 | return union 84 | } 85 | -------------------------------------------------------------------------------- /plugins/pluginfile_test.go: -------------------------------------------------------------------------------- 1 | package plugins_test 2 | 3 | import ( 4 | "github.com/kong/go-apiops/filebasics" 5 | "github.com/kong/go-apiops/jsonbasics" 6 | "github.com/kong/go-apiops/plugins" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("plugin files", func() { 12 | Describe("AddPluginPatch", func() { 13 | Describe("Parse", func() { 14 | It("parses a valid patch", func() { 15 | data := filebasics.MustDeserialize([]byte(` 16 | { 17 | "selectors": [ 18 | "$.services[*]" 19 | ], 20 | "overwrite": true, 21 | "plugins": [{ 22 | "name": "my-plugin" 23 | }] 24 | }`)) 25 | 26 | var pp plugins.AddPluginPatch 27 | err := pp.Parse(data, "test") 28 | Expect(err).ShouldNot(HaveOccurred()) 29 | Expect(pp.Selectors).To(Equal([]string{"$.services[*]"})) 30 | Expect(pp.Overwrite).To(BeTrue()) 31 | Expect(pp.Plugins).To(Equal([]map[string]interface{}{ 32 | {"name": "my-plugin"}, 33 | })) 34 | }) 35 | 36 | It("sets proper defaults", func() { 37 | data := filebasics.MustDeserialize([]byte(`{}`)) 38 | 39 | var pp plugins.AddPluginPatch 40 | err := pp.Parse(data, "test") 41 | Expect(err).ShouldNot(HaveOccurred()) 42 | Expect(pp.Selectors).To(Equal([]string{})) 43 | Expect(pp.Overwrite).To(BeFalse()) 44 | Expect(pp.Plugins).To(Equal([]map[string]interface{}{})) 45 | }) 46 | 47 | It("validates selectors", func() { 48 | data := filebasics.MustDeserialize([]byte(` 49 | { 50 | "selectors": ["this is not a jsonpath"] 51 | }`)) 52 | 53 | var pp plugins.AddPluginPatch 54 | err := pp.Parse(data, "test") 55 | Expect(err).Should(HaveOccurred()) 56 | }) 57 | }) 58 | 59 | Describe("Apply", func() { 60 | It("applies a patch", func() { 61 | data := jsonbasics.ConvertToYamlNode(filebasics.MustDeserialize([]byte(` 62 | { 63 | "services": [ 64 | { 65 | "name": "my-service", 66 | "plugins": [ 67 | { "name": "my-plugin" } 68 | ] 69 | } 70 | ] 71 | }`))) 72 | 73 | var pp plugins.AddPluginPatch 74 | err := pp.Parse(filebasics.MustDeserialize([]byte(` 75 | { 76 | "selectors": ["$.services[*]"], 77 | "overwrite": true, 78 | "plugins": [{ 79 | "name": "my-other-plugin" 80 | }] 81 | }`)), "test") 82 | Expect(err).ShouldNot(HaveOccurred()) 83 | 84 | err = pp.Apply(data) 85 | Expect(err).ShouldNot(HaveOccurred()) 86 | Expect(filebasics.MustSerialize(jsonbasics.ConvertToJSONobject(data), "JSON")).To(MatchJSON(` 87 | { 88 | "services": [ 89 | { 90 | "name": "my-service", 91 | "plugins": [ 92 | { "name": "my-plugin" }, 93 | { "name": "my-other-plugin" } 94 | ] 95 | } 96 | ] 97 | }`)) 98 | }) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /patch/patchcli.go: -------------------------------------------------------------------------------- 1 | package patch 2 | 3 | // This file implements the '--selector' and '--value' CLI flags 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/kong/go-apiops/jsonbasics" 11 | "github.com/kong/go-apiops/logbasics" 12 | ) 13 | 14 | // ValidateValuesFlags parses the CLI '--values' keys formatted 'key:json-string', into 15 | // a map. The map will hold the parsed JSON value by the key. The second return value is an 16 | // array of field names that is supposed to be deleted form the target. 17 | // Returns an error is value is not a valid JSON string. Important: strings 18 | // must be quoted; 19 | // 20 | // '--value foo:bar' is invalid 21 | // '--value foo:"bar"' results in string "bar" 22 | // '--value foo:true' results in boolean true 23 | // '--value foo:' results in deleting key 'foo' if it exists 24 | // '--value ["foo"]' results in appending string "foo" to arrays 25 | func ValidateValuesFlags(values []string) (map[string]interface{}, []string, []interface{}, error) { 26 | valuesMap := make(map[string]interface{}) 27 | removeArr := make([]string, 0) 28 | appendArr := make([]interface{}, 0) 29 | 30 | for _, content := range values { 31 | if strings.HasPrefix(strings.TrimSpace(content), "[") && 32 | strings.HasSuffix(strings.TrimSpace(content), "]") { 33 | // this is an array snippet 34 | var value interface{} 35 | err := json.Unmarshal([]byte(content), &value) 36 | if err != nil { 37 | return nil, nil, nil, fmt.Errorf("expected '--value' entry to be a valid json array '[ entry1, entry2, ... ]', "+ 38 | "failed parsing json-string in '%s'", content) 39 | } 40 | values, _ := jsonbasics.ToArray(value) 41 | 42 | logbasics.Debug("parsed patch-instruction", "array", value) 43 | appendArr = append(appendArr, values...) 44 | } else { 45 | // this is a key-value pair or delete-instruction 46 | subs := strings.SplitN(content, ":", 2) 47 | if len(subs) == 1 { 48 | return nil, nil, nil, fmt.Errorf("expected '--value' entry to have format 'key:json-string', "+ 49 | "or '[ json-array ], got: '%s'", content) 50 | } 51 | 52 | key := subs[0] 53 | val := strings.TrimSpace(subs[1]) 54 | 55 | var value interface{} 56 | if val == "" { 57 | // this is a delete-instruction, so inject the delete marker 58 | logbasics.Debug("parsed delete-instruction", "key", key) 59 | removeArr = append(removeArr, key) 60 | } else { 61 | // this is a key-value pair, parse the value 62 | err := json.Unmarshal([]byte(val), &value) 63 | if err != nil { 64 | return nil, nil, nil, fmt.Errorf("expected '--value' entry to have format 'key:json-string', "+ 65 | "failed parsing json-string in '%s' (did you forget to wrap a json-string-value in quotes?)", 66 | content) 67 | } 68 | logbasics.Debug("parsed patch-instruction", "key", key, "value", value) 69 | valuesMap[key] = value 70 | } 71 | } 72 | } 73 | 74 | return valuesMap, removeArr, appendArr, nil 75 | } 76 | -------------------------------------------------------------------------------- /openapi2kong/jsonschema.go: -------------------------------------------------------------------------------- 1 | package openapi2kong 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/pb33f/libopenapi/datamodel/high/base" 8 | ) 9 | 10 | // dereferenceSchema walks the schema and adds every subschema to the seenBefore map. 11 | // This is safe to recursive schemas. 12 | func dereferenceSchema(sr *base.SchemaProxy, seenBefore map[string]*base.SchemaProxy) { 13 | if sr == nil { 14 | return 15 | } 16 | 17 | srRef := sr.GetReference() 18 | 19 | if srRef != "" { 20 | if seenBefore[srRef] != nil { 21 | return 22 | } 23 | seenBefore[srRef] = sr 24 | } 25 | 26 | s := sr.Schema() 27 | allSchemas := [][]*base.SchemaProxy{s.AllOf, s.AnyOf, s.OneOf} 28 | for _, schemas := range allSchemas { 29 | for _, schema := range schemas { 30 | dereferenceSchema(schema, seenBefore) 31 | } 32 | } 33 | 34 | schemaMap := s.Properties 35 | schema := schemaMap.First() 36 | for schema != nil { 37 | dereferenceSchema(schema.Value(), seenBefore) 38 | schema = schema.Next() 39 | } 40 | 41 | dereferenceSchema(s.Not, seenBefore) 42 | 43 | if s.AdditionalProperties != nil && s.AdditionalProperties.IsA() { 44 | dereferenceSchema(s.AdditionalProperties.A, seenBefore) 45 | } 46 | 47 | if s.Items != nil && s.Items.IsA() { 48 | dereferenceSchema(s.Items.A, seenBefore) 49 | } 50 | } 51 | 52 | // extractSchema will extract a schema, including all sub-schemas/references and 53 | // return it as a single JSONschema string. All components will be moved under the 54 | // "#/definitions/" key. Along with that, it will also return the schema as a 55 | // map, so that if any further operations or validations need to be done on the schema 56 | // they could be performed. 57 | func extractSchema(s *base.SchemaProxy) (string, map[string]interface{}) { 58 | if s == nil || s.Schema() == nil { 59 | return "", nil 60 | } 61 | 62 | seenBefore := make(map[string]*base.SchemaProxy) 63 | dereferenceSchema(s, seenBefore) 64 | 65 | finalSchema := make(map[string]interface{}) 66 | 67 | if s.IsReference() { 68 | finalSchema["$ref"] = s.GetReference() 69 | } else { 70 | // copy the primary schema, if no ref string is present 71 | jConf, _ := s.Schema().MarshalJSON() 72 | _ = json.Unmarshal(jConf, &finalSchema) 73 | } 74 | 75 | // inject subschema's referenced 76 | if len(seenBefore) > 0 { 77 | definitions := make(map[string]interface{}) 78 | for key, schema := range seenBefore { 79 | // copy the subschema 80 | var copySchema map[string]interface{} 81 | 82 | if schema.Schema() == nil { 83 | continue 84 | } 85 | 86 | jConf, _ := schema.Schema().MarshalJSON() 87 | _ = json.Unmarshal(jConf, ©Schema) 88 | 89 | // store under new key 90 | definitions[strings.Replace(key, "#/components/schemas/", "", 1)] = copySchema 91 | } 92 | finalSchema["definitions"] = definitions 93 | } 94 | 95 | result, _ := json.Marshal(finalSchema) 96 | // update the $ref values; this is safe because plain " (double-quotes) would be escaped if in actual values 97 | return strings.ReplaceAll(string(result), "\"$ref\":\"#/components/schemas/", "\"$ref\":\"#/definitions/"), finalSchema 98 | } 99 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/05-service-defaults-document.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "server1.com", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "retries": 100, 13 | "routes": [ 14 | { 15 | "id": "663104d8-7e60-525d-b506-e42971b4466b", 16 | "methods": [ 17 | "GET" 18 | ], 19 | "name": "simple-api-overview_uses-doc-service", 20 | "paths": [ 21 | "~/path1$" 22 | ], 23 | "plugins": [], 24 | "regex_priority": 200, 25 | "strip_path": false, 26 | "tags": [ 27 | "OAS3_import", 28 | "OAS3file_05-service-defaults-document.yaml" 29 | ] 30 | } 31 | ], 32 | "tags": [ 33 | "OAS3_import", 34 | "OAS3file_05-service-defaults-document.yaml" 35 | ] 36 | }, 37 | { 38 | "host": "server2.com", 39 | "id": "a79c5a8c-0924-599e-9412-39f5a4ff0c3e", 40 | "name": "simple-api-overview_path2", 41 | "path": "/", 42 | "plugins": [], 43 | "port": 443, 44 | "protocol": "https", 45 | "retries": 100, 46 | "routes": [ 47 | { 48 | "id": "c1b84366-8ff8-57b9-b118-bedd1b9ab1c8", 49 | "methods": [ 50 | "GET" 51 | ], 52 | "name": "simple-api-overview_uses-path-service", 53 | "paths": [ 54 | "~/path2$" 55 | ], 56 | "plugins": [], 57 | "regex_priority": 200, 58 | "strip_path": false, 59 | "tags": [ 60 | "OAS3_import", 61 | "OAS3file_05-service-defaults-document.yaml" 62 | ] 63 | } 64 | ], 65 | "tags": [ 66 | "OAS3_import", 67 | "OAS3file_05-service-defaults-document.yaml" 68 | ] 69 | }, 70 | { 71 | "host": "server3.com", 72 | "id": "5d05e040-c551-50a1-90e7-6af5d3a7c0dc", 73 | "name": "simple-api-overview_uses-ops-service", 74 | "path": "/", 75 | "plugins": [], 76 | "port": 443, 77 | "protocol": "https", 78 | "retries": 100, 79 | "routes": [ 80 | { 81 | "id": "ca1c6faa-7076-5a09-9346-9c9dc3e019cb", 82 | "methods": [ 83 | "POST" 84 | ], 85 | "name": "simple-api-overview_uses-ops-service", 86 | "paths": [ 87 | "~/path2$" 88 | ], 89 | "plugins": [], 90 | "regex_priority": 200, 91 | "strip_path": false, 92 | "tags": [ 93 | "OAS3_import", 94 | "OAS3file_05-service-defaults-document.yaml" 95 | ] 96 | } 97 | ], 98 | "tags": [ 99 | "OAS3_import", 100 | "OAS3file_05-service-defaults-document.yaml" 101 | ] 102 | } 103 | ], 104 | "upstreams": [] 105 | } -------------------------------------------------------------------------------- /docs/security-o2k.md: -------------------------------------------------------------------------------- 1 | # OpenAPI2Kong support for security directives 2 | 3 | This document documents the use of `security` directives within the `deck` subcommand `openapi2kong`. There are 2 sections; 4 | 5 | 1. [OpenAPI 3 security directives explained](#openapi-3-security-directives-explained) 6 | 2. [Conversion logic to Kong plugins](#conversion-logic-to-kong-plugins) 7 | 8 | --- 9 | 10 | # Openapi 3 security directives explained 11 | 12 | Within OpenAPI the security directives can be specified on the document root (the `OpenAPI` object). It can also be specified on the `Operation` object, in which case it will override the document level one. 13 | Specifically; it cannot be specified on the `path` object level, the level in between document and operation. 14 | 15 | A nice explanation is here: https://swagger.io/docs/specification/authentication/ 16 | 17 | --- 18 | 19 | # Conversion logic to Kong plugins 20 | 21 | To enable the generation of Kong plugins the `deck` flag `--generate-security` must be specified. 22 | 23 | The `securityScheme` object has a `type` property. These are the possible values 24 | and their support for Kong plugins conversions: 25 | 26 | Type | supported | Kong plugin 27 | -|-|- 28 | `http`| no | 29 | `apiKey`| no | 30 | `openIdConnect`| yes | `openid-connect` | 31 | `oauth2`| no | 32 | 33 | The non-supported types will result in errors when doing a conversion. To ignore those the flag `--ignore-security-errors` can be specified. 34 | 35 | ## Boolean logic 36 | 37 | No boolean AND/OR logic is supported. So a `security` directive can only have 1 `security requirement`, and within that only a single `securityScheme`. 38 | Again; the errors generated can be ignored by specifying the `--ignore-security-errors` flag. 39 | 40 | ## Extensions 41 | 42 | Within a `securityScheme` of type `openIdConnect`, the extension `x-kong-security-openid-connect` can be used to configure the plugin options. 43 | (The name is the plugin-name, prefixed with "`x-kong-security-`") 44 | 45 | ## Conversion 46 | The following table describes property behaviour: 47 | 48 | OpenID Connect plugin | securityScheme | Notes 49 | -|-|- 50 | `config` | `x-kong-security-openid-connect` | The basis configuration is taken from the extension. Defaults to an empty object if omitted. 51 | `config.issuer` | `openIdConnectUrl` | 52 | `config.scopes_required` | | Union of the scopes in the extension, and the scopes listed in the `securityRequirement` scopes array. 53 | 54 | Example: 55 | ```yaml 56 | security: 57 | - myOpenId: [ "scope3" ] 58 | components: 59 | securitySchemes: 60 | myOpenId: 61 | type: openIdConnect 62 | openIdConnectUrl: https://konghq.com/oauth2/.well-known/openid-configuration 63 | x-kong-security-openid-connect: 64 | config: 65 | run_on_preflight: false 66 | scopes_required: ["scope1", "scope2"] 67 | ``` 68 | 69 | Will result in a plugin entry as follows: 70 | ```yaml 71 | plugins: 72 | - name: openid-connect 73 | config: 74 | issuer: https://konghq.com/oauth2/.well-known/openid-configuration 75 | run_on_preflight: false 76 | scopes_required: ["scope1", "scope2", "scope3"] 77 | ``` 78 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/12-path-parameter-regex.yaml: -------------------------------------------------------------------------------- 1 | # The path parameters should be converted to a regex for matching the route. 2 | # Including creating captures of the parameter values. 3 | 4 | openapi: 3.0.3 5 | info: 6 | title: Path parameter test 7 | version: v1 8 | servers: 9 | - url: "https://example.com" 10 | 11 | paths: 12 | # simple regex capture names. Also the path has a trailing slash. 13 | /demo/{something}/else/{todo}/: 14 | get: 15 | operationId: opsid 16 | parameters: 17 | - in: path 18 | name: something 19 | required: true 20 | schema: 21 | type: string 22 | - in: path 23 | name: todo 24 | required: true 25 | schema: 26 | type: string 27 | responses: 28 | "200": 29 | description: OK 30 | # note that the names are not valid regex capture names, hence they must be sanitized 31 | # in the output. Also the path has NO trailing slash. 32 | /demo/{ white space }/{x*()_-+=x}/{-To_Do}: 33 | get: 34 | operationId: opsid 35 | parameters: 36 | - in: path 37 | name: " white space " 38 | required: true 39 | schema: 40 | type: string 41 | - in: path 42 | name: "x*()_-+=x" 43 | required: true 44 | schema: 45 | type: string 46 | - in: path 47 | name: -To_Do 48 | required: true 49 | schema: 50 | type: string 51 | responses: 52 | "200": 53 | description: OK 54 | # some more complex paths, with and without parameters 55 | # examples from: https://github.com/Kong/insomnia/pull/5246 56 | /batchs(Material='iron',Batch='10'): 57 | get: 58 | responses: 59 | "200": 60 | description: no parameters 61 | operationId: getBatchNoParams 62 | /batchs(Material='{Material}',Batch='{Batch}'): 63 | get: 64 | parameters: 65 | - in: path 66 | name: Material 67 | required: true 68 | schema: 69 | type: string 70 | - in: path 71 | name: Batch 72 | required: true 73 | schema: 74 | type: string 75 | responses: 76 | "200": 77 | description: An echo message. 78 | operationId: getBatchWithParams 79 | post: 80 | # test validating not escaping the path twice; we have 2 methods on 1 path 81 | parameters: 82 | - in: path 83 | name: Material 84 | required: true 85 | schema: 86 | type: string 87 | - in: path 88 | name: Batch 89 | required: true 90 | schema: 91 | type: string 92 | responses: 93 | "200": 94 | description: An echo message. 95 | operationId: postBatchWithParams 96 | # dollar sign in path 97 | /some/$test: 98 | get: 99 | responses: 100 | "200": 101 | description: no parameters 102 | operationId: dollar-sign 103 | -------------------------------------------------------------------------------- /yamlbasics/selectors.go: -------------------------------------------------------------------------------- 1 | package yamlbasics 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kong/go-apiops/logbasics" 7 | "github.com/speakeasy-api/jsonpath/pkg/jsonpath" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // 12 | // 13 | // SelectorSet implementation, handles multiple instead of 1 JSONpath selector 14 | // 15 | // 16 | 17 | // Represents a set of JSONpath selectors. Call NewSelectorSet to create one. 18 | // The SelectorSet can be empty, in which case it will return only empty results. 19 | type SelectorSet struct { 20 | selectors []*jsonpath.JSONPath // the compiled selectors 21 | source []string // matching source strings of the selectors 22 | initialized bool // indicator whether is was initialized or not 23 | } 24 | 25 | // NewSelectorSet compiles the given selectors into a list of yaml nodes. 26 | // If any of the selectors is invalid, an error will be returned. 27 | // If the selectors are omitted/empty then an empty set is returned. 28 | func NewSelectorSet(selectors []string) (SelectorSet, error) { 29 | var ( 30 | set SelectorSet 31 | err error 32 | ) 33 | 34 | set.selectors = make([]*jsonpath.JSONPath, len(selectors)) 35 | set.source = make([]string, len(selectors)) 36 | for i, selector := range selectors { 37 | set.source[i] = selector 38 | set.selectors[i], err = jsonpath.NewPath(selector) 39 | if err != nil { 40 | return SelectorSet{}, fmt.Errorf("selector '%s' is not a valid JSONpath expression; %w", selector, err) 41 | } 42 | } 43 | set.initialized = true 44 | return set, nil 45 | } 46 | 47 | // IsEmpty returns true if the selector set is empty. 48 | func (set *SelectorSet) IsEmpty() bool { 49 | //nolint:gosimple 50 | return set.selectors == nil || len(set.selectors) == 0 51 | } 52 | 53 | // GetSources returns a copy of the selector sources 54 | func (set *SelectorSet) GetSources() []string { 55 | sources := make([]string, len(set.source)) 56 | copy(sources, set.source) 57 | return sources 58 | } 59 | 60 | // Find executes the given selectors on the given yaml node. 61 | // The result will never be nil, will not have duplicates, but can be an empty array. 62 | // An error is only returned if any of the selectors errors when searching. 63 | // nodeToSearch cannot be nil, in which case it will panic. 64 | func (set *SelectorSet) Find(nodeToSearch *yaml.Node) (NodeSet, error) { 65 | if !set.initialized { 66 | panic("selector set uninitialized, call NewSelectorSet to create and initialize one") 67 | } 68 | if nodeToSearch == nil { 69 | panic("expected nodeToSearch to be non-nil") 70 | } 71 | //nolint:gosimple 72 | if set.selectors == nil || len(set.selectors) == 0 { 73 | return make(NodeSet, 0), nil 74 | } 75 | 76 | results := make(NodeSet, 0) 77 | seen := make(map[*yaml.Node]bool) 78 | for i, selector := range set.selectors { 79 | matches := selector.Query(nodeToSearch) 80 | logbasics.Debug("selector results", "selector", set.source[i], "#found", len(matches)) 81 | 82 | for _, match := range matches { 83 | if match != nil && !seen[match] { 84 | results = append(results, match) 85 | seen[match] = true 86 | } 87 | } 88 | } 89 | 90 | return results, nil 91 | } 92 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/22-doc-path-and-ops-servers.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "multi-service-api.upstream", 6 | "id": "eff78c7b-04db-5898-9e25-bd40f5cde525", 7 | "name": "multi-service-api", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [], 13 | "tags": [ 14 | "OAS3_import", 15 | "OAS3file_22-doc-path-and-ops-servers.yaml" 16 | ] 17 | }, 18 | { 19 | "host": "order-service.example.com", 20 | "id": "90bcf95e-2064-5bcf-b94a-d4294e29c00c", 21 | "name": "multi-service-api_orders", 22 | "path": "/", 23 | "plugins": [], 24 | "port": 443, 25 | "protocol": "https", 26 | "routes": [ 27 | { 28 | "id": "e8d42325-3ee0-57b3-9883-5ea51618f4ac", 29 | "methods": [ 30 | "POST" 31 | ], 32 | "name": "multi-service-api_orders_post", 33 | "paths": [ 34 | "~/orders$" 35 | ], 36 | "plugins": [], 37 | "regex_priority": 200, 38 | "strip_path": false, 39 | "tags": [ 40 | "OAS3_import", 41 | "OAS3file_22-doc-path-and-ops-servers.yaml" 42 | ] 43 | } 44 | ], 45 | "tags": [ 46 | "OAS3_import", 47 | "OAS3file_22-doc-path-and-ops-servers.yaml" 48 | ] 49 | }, 50 | { 51 | "host": "user-service.example.com", 52 | "id": "6e5e1d3b-894b-5ce8-b807-532df7b6d01d", 53 | "name": "multi-service-api_users_get", 54 | "path": "/", 55 | "plugins": [], 56 | "port": 443, 57 | "protocol": "https", 58 | "routes": [ 59 | { 60 | "id": "744254cb-7b0f-527d-a9e3-41998313e8fd", 61 | "methods": [ 62 | "GET" 63 | ], 64 | "name": "multi-service-api_users_get", 65 | "paths": [ 66 | "~/users$" 67 | ], 68 | "plugins": [], 69 | "regex_priority": 200, 70 | "strip_path": false, 71 | "tags": [ 72 | "OAS3_import", 73 | "OAS3file_22-doc-path-and-ops-servers.yaml" 74 | ] 75 | } 76 | ], 77 | "tags": [ 78 | "OAS3_import", 79 | "OAS3file_22-doc-path-and-ops-servers.yaml" 80 | ] 81 | } 82 | ], 83 | "upstreams": [ 84 | { 85 | "id": "c9507064-ce65-5a42-9501-58faa57ad09c", 86 | "name": "multi-service-api.upstream", 87 | "tags": [ 88 | "OAS3_import", 89 | "OAS3file_22-doc-path-and-ops-servers.yaml" 90 | ], 91 | "targets": [ 92 | { 93 | "tags": [ 94 | "OAS3_import", 95 | "OAS3file_22-doc-path-and-ops-servers.yaml" 96 | ], 97 | "target": "server1.com:443" 98 | }, 99 | { 100 | "tags": [ 101 | "OAS3_import", 102 | "OAS3file_22-doc-path-and-ops-servers.yaml" 103 | ], 104 | "target": "server2.com:443" 105 | } 106 | ] 107 | } 108 | ] 109 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/13-request-validator-plugin.yaml: -------------------------------------------------------------------------------- 1 | # When the request-validator is added without a body or parameter schema 2 | # the generator should automatically generate it. 3 | 4 | openapi: 3.0.2 5 | 6 | info: 7 | title: Example 8 | version: 1.0.0 9 | 10 | servers: 11 | - url: http://backend.com/path 12 | 13 | x-kong-plugin-request-validator: {} 14 | 15 | paths: 16 | /global: 17 | get: 18 | requestBody: 19 | content: 20 | application/json: 21 | schema: 22 | $ref: '#/components/schemas/jsonSchema' 23 | responses: 24 | "200": 25 | description: OK 26 | /params/{path-id}: 27 | get: 28 | x-kong-plugin-request-validator: 29 | enabled: true 30 | config: 31 | body_schema: '{}' 32 | parameters: 33 | - in: query 34 | name: queryid 35 | schema: 36 | type: integer 37 | required: true 38 | - in: header 39 | name: User-Id 40 | schema: 41 | type: integer 42 | required: true 43 | # This would not be added to the req-validator plugin config 44 | # as cookie type is not supported yet. 45 | # A warning would be logged and this parameter would be ignored. 46 | - in: cookie 47 | name: cookieid 48 | schema: 49 | type: integer 50 | required: true 51 | - in: path 52 | name: path_id 53 | schema: 54 | type: integer 55 | required: true 56 | responses: 57 | "200": 58 | description: OK 59 | /body: 60 | post: 61 | requestBody: 62 | content: 63 | application/json: 64 | schema: 65 | $ref: '#/components/schemas/jsonSchema' 66 | application/xml: 67 | schema: 68 | $ref: '#/components/schemas/xmlSchema' 69 | responses: 70 | "200": 71 | description: OK 72 | /body-single-content-xml: 73 | post: 74 | requestBody: 75 | content: 76 | application/xml: 77 | schema: 78 | $ref: '#/components/schemas/xmlSchema' 79 | responses: 80 | "200": 81 | description: OK 82 | 83 | /body-single-content-type-with-charset: 84 | post: 85 | requestBody: 86 | content: 87 | application/json; charset=UTF-8: 88 | schema: 89 | $ref: '#/components/schemas/jsonSchema' 90 | responses: 91 | "200": 92 | description: OK 93 | 94 | /body-single-content-type-with-structured-syntax-suffix: 95 | post: 96 | requestBody: 97 | content: 98 | application/merge-patch+json: 99 | schema: 100 | $ref: '#/components/schemas/jsonSchema' 101 | responses: 102 | "200": 103 | description: OK 104 | 105 | components: 106 | schemas: 107 | jsonSchema: 108 | type: object 109 | properties: 110 | id: 111 | type: integer 112 | name: 113 | type: string 114 | xmlSchema: 115 | type: object 116 | properties: 117 | prop: 118 | type: integer 119 | -------------------------------------------------------------------------------- /yamlbasics/selectors_test.go: -------------------------------------------------------------------------------- 1 | package yamlbasics_test 2 | 3 | import ( 4 | . "github.com/kong/go-apiops/yamlbasics" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | var _ = Describe("selectors", func() { 11 | Describe("NewSelectorSet", func() { 12 | Context("when given a valid selector", func() { 13 | It("should return a non-empty set", func() { 14 | set, err := NewSelectorSet([]string{"$.a.b"}) 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(set.IsEmpty()).To(BeFalse()) 17 | }) 18 | }) 19 | Context("when given an invalid selector", func() { 20 | It("should return an error", func() { 21 | _, err := NewSelectorSet([]string{"$.a.b["}) 22 | Expect(err).To(HaveOccurred()) 23 | }) 24 | }) 25 | Context("when given an empty selector", func() { 26 | It("should return an empty set", func() { 27 | set, err := NewSelectorSet([]string{}) 28 | Expect(err).ToNot(HaveOccurred()) 29 | Expect(set.IsEmpty()).To(BeTrue()) 30 | }) 31 | }) 32 | Context("when given a nil selector", func() { 33 | It("should return an empty set", func() { 34 | set, err := NewSelectorSet(nil) 35 | Expect(err).ToNot(HaveOccurred()) 36 | Expect(set.IsEmpty()).To(BeTrue()) 37 | }) 38 | }) 39 | }) 40 | Describe("IsEmpty", func() { 41 | Context("when the set is empty", func() { 42 | It("should return true", func() { 43 | set := SelectorSet{} 44 | Expect(set.IsEmpty()).To(BeTrue()) 45 | }) 46 | }) 47 | Context("when the set is not empty", func() { 48 | It("should return false", func() { 49 | set, _ := NewSelectorSet([]string{"$.a.b"}) 50 | Expect(set.IsEmpty()).To(BeFalse()) 51 | }) 52 | }) 53 | }) 54 | Describe("GetSources", func() { 55 | Context("when the set is empty", func() { 56 | It("should return an empty list", func() { 57 | set := SelectorSet{} 58 | Expect(set.GetSources()).To(BeEmpty()) 59 | }) 60 | }) 61 | Context("when the set is not empty", func() { 62 | It("should return a copy of the sources", func() { 63 | sources := []string{"$.a.b", "$.c.d"} 64 | set, _ := NewSelectorSet(sources) 65 | Expect(set.GetSources()).To(Equal(sources)) 66 | Expect(set.GetSources()).ToNot(BeIdenticalTo(sources)) 67 | }) 68 | }) 69 | }) 70 | Describe("Find", func() { 71 | Context("when the set is not initialized", func() { 72 | It("should panic", func() { 73 | set := SelectorSet{} 74 | node := &yaml.Node{} 75 | Expect(func() { set.Find(node) }).To(Panic()) 76 | }) 77 | }) 78 | Context("when the set is empty", func() { 79 | It("should return an empty list", func() { 80 | set, _ := NewSelectorSet([]string{}) 81 | node := &yaml.Node{} 82 | Expect(set.Find(node)).To(BeEmpty()) 83 | }) 84 | }) 85 | Context("when the set is not empty", func() { 86 | Context("when the node is nil", func() { 87 | It("should panic", func() { 88 | set, _ := NewSelectorSet([]string{"$.a.b"}) 89 | Expect(func() { set.Find(nil) }).To(Panic()) 90 | }) 91 | }) 92 | Context("when the node is not nil", func() { 93 | It("should return a list of nodes", func() { 94 | set, _ := NewSelectorSet([]string{"$.a.b"}) 95 | node := &yaml.Node{} 96 | Expect(set.Find(node)).ToNot(BeNil()) 97 | }) 98 | }) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-apiops 2 | 3 | Home of Kong's Go based APIOps library. 4 | 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/kong/go-apiops/test.yml?branch=main&label=Tests)](https://github.com/kong/go-apiops/actions?query=branch%3Amain+event%3Apush) 6 | [![Lint Status](https://img.shields.io/github/actions/workflow/status/kong/go-apiops/golangci-lint.yml?branch=main&label=Linter)](https://github.com/kong/go-apiops/actions?query=branch%3Amain+event%3Apush) 7 | [![codecov](https://codecov.io/gh/Kong/go-apiops/branch/main/graph/badge.svg?token=8XTDGNP8VW)](https://codecov.io/gh/Kong/go-apiops) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/kong/go-apiops)](https://goreportcard.com/report/github.com/kong/go-apiops) 9 | [![SemVer](https://img.shields.io/github/v/tag/kong/go-apiops?color=brightgreen&label=SemVer&logo=semver&sort=semver)](https://github.com/Kong/go-apiops/releases) 10 | [![License](https://img.shields.io/github/license/Kong/go-apiops)](LICENSE) 11 | 12 | ## What is APIOps 13 | 14 | API Lifecycle Automation, or APIOps, is the process of applying API best practices via automation frameworks. 15 | This library contains functions to aid the development of tools to apply APIOps to Kong Gateway deployments. 16 | 17 | See the [Kong Blog](https://konghq.com/blog/tag/apiops) for more information on APIOps concepts. 18 | 19 | ## What is this library? 20 | 21 | The [go-apiops](https://github.com/Kong/go-apiops) library provides a set of tools (generation and transformation) 22 | for working with API specifications and [Kong Gateway](https://docs.konghq.com/gateway/latest/) declarative configurations. 23 | Conceptually, these tools are intended to be organized into a pipeline of individual steps configured for a particular 24 | users needs. The overall purpose of the library is to enable users to build a CI/CD workflow which deliver APIs from 25 | specification to deployment. This pipeline design allows users to customize the delivery of APIs based on their specific needs. 26 | 27 | ## What is the current status of this library? 28 | 29 | The library is an [Apache 2.0 license](LICENSE). 30 | The library functionality will be be made available through 31 | the [deck](https://docs.konghq.com/deck/latest/) cli tool. 32 | 33 | ## Installation & Usage 34 | 35 | Currently, the functionality is released as a library and as part of the [decK](https://github.com/Kong/deck) CLI tool. The repository also contains a CLI named `go-apiops` for local testing. 36 | For general CLI usage, the [decK tool](https://docs.konghq.com/deck/latest/) should be used. 37 | 38 | ### Local Build 39 | 40 | * make sure [Go-lang tools](https://go.dev/doc/install) are installed 41 | * Checkout the Git repository (switch to a specific tag to select a version if required) 42 | * use the makefile to build the project via `make build` 43 | 44 | ## Reporting issues 45 | 46 | Issues using the `go-apiops` CLI or the library can be reported in the [Github repo](https://github.com/Kong/go-apiops/issues). 47 | 48 | ## Releasing new versions 49 | 50 | The releases are automated. To create a new release: 51 | 52 | * tag at the desired place to release 53 | 54 | ``` bash 55 | git tag vX.Y.Z 56 | ``` 57 | 58 | * push the tag and CI will create a new release 59 | 60 | ```bash 61 | git push vX.Y.Z 62 | ``` 63 | 64 | * verify the release on [the releases page](https://github.com/Kong/go-apiops/releases), possibly edit the release-notes (which will be generated from the commit history) 65 | -------------------------------------------------------------------------------- /cmd/addtags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/kong/go-apiops/deckformat" 9 | "github.com/kong/go-apiops/filebasics" 10 | "github.com/kong/go-apiops/logbasics" 11 | "github.com/kong/go-apiops/tags" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // Executes the CLI command "add-tags" 16 | func executeAddTags(cmd *cobra.Command, tagsToAdd []string) error { 17 | verbosity, _ := cmd.Flags().GetInt("verbose") 18 | logbasics.Initialize(log.LstdFlags, verbosity) 19 | 20 | inputFilename, err := cmd.Flags().GetString("state") 21 | if err != nil { 22 | return fmt.Errorf("failed getting cli argument 'state'; %w", err) 23 | } 24 | 25 | outputFilename, err := cmd.Flags().GetString("output-file") 26 | if err != nil { 27 | return fmt.Errorf("failed getting cli argument 'output-file'; %w", err) 28 | } 29 | 30 | var outputFormat string 31 | { 32 | outputFormat, err = cmd.Flags().GetString("format") 33 | if err != nil { 34 | return fmt.Errorf("failed getting cli argument 'format'; %w", err) 35 | } 36 | outputFormat = strings.ToUpper(outputFormat) 37 | } 38 | 39 | var selectors []string 40 | { 41 | selectors, err = cmd.Flags().GetStringArray("selector") 42 | if err != nil { 43 | return fmt.Errorf("failed getting cli argument 'selector'; %w", err) 44 | } 45 | } 46 | 47 | // do the work: read/add-tags/write 48 | data, err := filebasics.DeserializeFile(inputFilename) 49 | if err != nil { 50 | return fmt.Errorf("failed to read input file '%s'; %w", inputFilename, err) 51 | } 52 | 53 | tagger := tags.Tagger{} 54 | tagger.SetData(data) 55 | err = tagger.SetSelectors(selectors) 56 | if err != nil { 57 | return fmt.Errorf("failed to set selectors; %w", err) 58 | } 59 | err = tagger.AddTags(tagsToAdd) 60 | if err != nil { 61 | return fmt.Errorf("failed to add tags; %w", err) 62 | } 63 | data = tagger.GetData() 64 | 65 | trackInfo := deckformat.HistoryNewEntry("add-tags") 66 | trackInfo["input"] = inputFilename 67 | trackInfo["output"] = outputFilename 68 | trackInfo["tags"] = tagsToAdd 69 | trackInfo["selectors"] = selectors 70 | deckformat.HistoryAppend(data, trackInfo) 71 | 72 | return filebasics.WriteSerializedFile(outputFilename, data, filebasics.OutputFormat(outputFormat)) 73 | } 74 | 75 | // 76 | // 77 | // Define the CLI data for the add-tags command 78 | // 79 | // 80 | 81 | var addTagsCmd = &cobra.Command{ 82 | Use: "add-tags [flags] tag [...tag]", 83 | Short: "Adds tags to objects in a decK file", 84 | Long: `Adds tags to objects in a decK file. 85 | 86 | The tags are added to all objects that match the selector expressions. If no 87 | selectors are given, all Kong entities are tagged.`, 88 | RunE: executeAddTags, 89 | Args: cobra.MinimumNArgs(1), 90 | } 91 | 92 | func init() { 93 | rootCmd.AddCommand(addTagsCmd) 94 | addTagsCmd.Flags().StringP("state", "s", "-", "decK file to process. Use - to read from stdin") 95 | addTagsCmd.Flags().StringArray("selector", []string{}, "JSON path expression to select "+ 96 | "objects to add tags to,\ndefaults to all Kong entities (repeat for multiple selectors)") 97 | addTagsCmd.Flags().StringP("output-file", "o", "-", "output file to write. Use - to write to stdout") 98 | addTagsCmd.Flags().StringP("format", "", string(filebasics.OutputFormatYaml), "output format: "+ 99 | string(filebasics.OutputFormatJSON)+" or "+string(filebasics.OutputFormatYaml)) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/listtags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/kong/go-apiops/filebasics" 9 | "github.com/kong/go-apiops/logbasics" 10 | "github.com/kong/go-apiops/tags" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // Executes the CLI command "list-tags" 15 | func executeListTags(cmd *cobra.Command, _ []string) error { 16 | verbosity, _ := cmd.Flags().GetInt("verbose") 17 | logbasics.Initialize(log.LstdFlags, verbosity) 18 | 19 | inputFilename, err := cmd.Flags().GetString("state") 20 | if err != nil { 21 | return fmt.Errorf("failed getting cli argument 'state'; %w", err) 22 | } 23 | 24 | outputFilename, err := cmd.Flags().GetString("output-file") 25 | if err != nil { 26 | return fmt.Errorf("failed getting cli argument 'output-file'; %w", err) 27 | } 28 | 29 | var outputFormat string 30 | { 31 | outputFormat, err = cmd.Flags().GetString("format") 32 | if err != nil { 33 | return fmt.Errorf("failed getting cli argument 'format'; %w", err) 34 | } 35 | outputFormat = strings.ToUpper(outputFormat) 36 | } 37 | 38 | var selectors []string 39 | { 40 | selectors, err = cmd.Flags().GetStringArray("selector") 41 | if err != nil { 42 | return fmt.Errorf("failed getting cli argument 'selector'; %w", err) 43 | } 44 | } 45 | 46 | // do the work: read/list-tags/write 47 | data, err := filebasics.DeserializeFile(inputFilename) 48 | if err != nil { 49 | return fmt.Errorf("failed to read input file '%s'; %w", inputFilename, err) 50 | } 51 | 52 | tagger := tags.Tagger{} 53 | tagger.SetData(data) 54 | err = tagger.SetSelectors(selectors) 55 | if err != nil { 56 | return fmt.Errorf("failed to set selectors; %w", err) 57 | } 58 | list, err := tagger.ListTags() 59 | if err != nil { 60 | return fmt.Errorf("failed to list tags; %w", err) 61 | } 62 | 63 | if outputFormat == "PLAIN" { 64 | // return as a plain text format, unix style; line separated 65 | result := []byte(strings.Join(list, "\n")) 66 | return filebasics.WriteFile(outputFilename, result) 67 | } 68 | // return as yaml/json, create an object containing only a tags-array 69 | result := make(map[string]interface{}) 70 | result["tags"] = list 71 | return filebasics.WriteSerializedFile(outputFilename, result, filebasics.OutputFormat(outputFormat)) 72 | } 73 | 74 | // 75 | // 76 | // Define the CLI data for the list-tags command 77 | // 78 | // 79 | 80 | var ListTagsCmd = &cobra.Command{ 81 | Use: "list-tags [flags]", 82 | Short: "Lists current tags to objects in a decK file", 83 | Long: `Lists current tags to objects in a decK file. 84 | 85 | The tags will be collected from all objects that match the selector expressions. If no 86 | selectors are given, all Kong entities will be scanned.`, 87 | RunE: executeListTags, 88 | Args: cobra.NoArgs, 89 | } 90 | 91 | func init() { 92 | rootCmd.AddCommand(ListTagsCmd) 93 | ListTagsCmd.Flags().StringP("state", "s", "-", "decK file to process. Use - to read from stdin") 94 | ListTagsCmd.Flags().StringArray("selector", []string{}, "JSON path expression to select "+ 95 | "objects to scan for tags,\ndefaults to all Kong entities (repeat for multiple selectors)") 96 | ListTagsCmd.Flags().StringP("output-file", "o", "-", "output file to write. Use - to write to stdout") 97 | ListTagsCmd.Flags().StringP("format", "", "PLAIN", "output format: "+ 98 | string(filebasics.OutputFormatJSON)+", "+string(filebasics.OutputFormatYaml)+", or PLAIN") 99 | } 100 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/09-generic-plugins.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "server1.com", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [ 10 | { 11 | "config": { 12 | "message": "So long and thanks for all the fish!", 13 | "status_code": 403 14 | }, 15 | "id": "ef93d30f-dc7d-581b-8b1b-3f8942a0d171", 16 | "name": "request-termination", 17 | "tags": [ 18 | "OAS3_import", 19 | "OAS3file_09-generic-plugins.yaml" 20 | ] 21 | } 22 | ], 23 | "port": 443, 24 | "protocol": "https", 25 | "routes": [ 26 | { 27 | "id": "d020c736-e762-5dad-8415-1684ee4f2061", 28 | "methods": [ 29 | "GET" 30 | ], 31 | "name": "simple-api-overview_uses-doc-plugin", 32 | "paths": [ 33 | "~/path1$" 34 | ], 35 | "plugins": [], 36 | "regex_priority": 200, 37 | "strip_path": false, 38 | "tags": [ 39 | "OAS3_import", 40 | "OAS3file_09-generic-plugins.yaml" 41 | ] 42 | }, 43 | { 44 | "id": "ef2ca083-29b3-5d7b-87c5-e4315d830c33", 45 | "methods": [ 46 | "GET" 47 | ], 48 | "name": "simple-api-overview_uses-path-plugin", 49 | "paths": [ 50 | "~/path2$" 51 | ], 52 | "plugins": [ 53 | { 54 | "config": { 55 | "message": "The answer to life, the universe, and everything!", 56 | "status_code": 403 57 | }, 58 | "id": "aa56031e-7155-599f-a9e9-93e6b271ba58", 59 | "name": "request-termination", 60 | "tags": [ 61 | "OAS3_import", 62 | "OAS3file_09-generic-plugins.yaml" 63 | ] 64 | } 65 | ], 66 | "regex_priority": 200, 67 | "strip_path": false, 68 | "tags": [ 69 | "OAS3_import", 70 | "OAS3file_09-generic-plugins.yaml" 71 | ] 72 | }, 73 | { 74 | "id": "f9c8a7d7-3518-5c25-b66b-40943e59f91b", 75 | "methods": [ 76 | "POST" 77 | ], 78 | "name": "simple-api-overview_uses-ops-plugin", 79 | "paths": [ 80 | "~/path2$" 81 | ], 82 | "plugins": [ 83 | { 84 | "config": { 85 | "message": "For a moment, nothing happened. Then, after a second or so, nothing continued to happen.", 86 | "status_code": 403 87 | }, 88 | "id": "ead16074-ccb0-52dd-9f56-4193529e8ffa", 89 | "name": "request-termination", 90 | "tags": [ 91 | "OAS3_import", 92 | "OAS3file_09-generic-plugins.yaml" 93 | ] 94 | } 95 | ], 96 | "regex_priority": 200, 97 | "strip_path": false, 98 | "tags": [ 99 | "OAS3_import", 100 | "OAS3file_09-generic-plugins.yaml" 101 | ] 102 | } 103 | ], 104 | "tags": [ 105 | "OAS3_import", 106 | "OAS3file_09-generic-plugins.yaml" 107 | ] 108 | } 109 | ], 110 | "upstreams": [] 111 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/12-path-parameter-regex.expected_inso.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "example.com", 6 | "name": "Path_parameter_test", 7 | "path": "/", 8 | "plugins": [], 9 | "port": 443, 10 | "protocol": "https", 11 | "routes": [ 12 | { 13 | "methods": [ 14 | "GET" 15 | ], 16 | "name": "Path_parameter_test-getBatchNoParams", 17 | "paths": [ 18 | "~/batchs\\(Material='iron',Batch='10'\\)$" 19 | ], 20 | "plugins": [], 21 | "regex_priority": 200, 22 | "strip_path": false, 23 | "tags": [ 24 | "OAS3_import", 25 | "OAS3file_12-path-parameter-regex.yaml" 26 | ] 27 | }, 28 | { 29 | "methods": [ 30 | "GET" 31 | ], 32 | "name": "Path_parameter_test-getBatchWithParams", 33 | "paths": [ 34 | "~/batchs\\(Material='(?[^#?/]+)',Batch='(?[^#?/]+)'\\)$" 35 | ], 36 | "plugins": [], 37 | "regex_priority": 100, 38 | "strip_path": false, 39 | "tags": [ 40 | "OAS3_import", 41 | "OAS3file_12-path-parameter-regex.yaml" 42 | ] 43 | }, 44 | { 45 | "methods": [ 46 | "POST" 47 | ], 48 | "name": "Path_parameter_test-postBatchWithParams", 49 | "paths": [ 50 | "~/batchs\\(Material='(?[^#?/]+)',Batch='(?[^#?/]+)'\\)$" 51 | ], 52 | "plugins": [], 53 | "regex_priority": 100, 54 | "strip_path": false, 55 | "tags": [ 56 | "OAS3_import", 57 | "OAS3file_12-path-parameter-regex.yaml" 58 | ] 59 | }, 60 | { 61 | "methods": [ 62 | "GET" 63 | ], 64 | "name": "Path_parameter_test-opsid", 65 | "paths": [ 66 | "~/demo/(?[^#?/]+)/(?[^#?/]+)/(?[^#?/]+)$" 67 | ], 68 | "plugins": [], 69 | "regex_priority": 100, 70 | "strip_path": false, 71 | "tags": [ 72 | "OAS3_import", 73 | "OAS3file_12-path-parameter-regex.yaml" 74 | ] 75 | }, 76 | { 77 | "methods": [ 78 | "GET" 79 | ], 80 | "name": "Path_parameter_test-opsid", 81 | "paths": [ 82 | "~/demo/(?[^#?/]+)/else/(?[^#?/]+)/$" 83 | ], 84 | "plugins": [], 85 | "regex_priority": 100, 86 | "strip_path": false, 87 | "tags": [ 88 | "OAS3_import", 89 | "OAS3file_12-path-parameter-regex.yaml" 90 | ] 91 | }, 92 | { 93 | "methods": [ 94 | "GET" 95 | ], 96 | "name": "Path_parameter_test-dollar-sign", 97 | "paths": [ 98 | "~/some/\\$test$" 99 | ], 100 | "plugins": [], 101 | "regex_priority": 200, 102 | "strip_path": false, 103 | "tags": [ 104 | "OAS3_import", 105 | "OAS3file_12-path-parameter-regex.yaml" 106 | ] 107 | } 108 | ], 109 | "tags": [ 110 | "OAS3_import", 111 | "OAS3file_12-path-parameter-regex.yaml" 112 | ] 113 | } 114 | ], 115 | "upstreams": [] 116 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/16-security-oidc-generation.yaml: -------------------------------------------------------------------------------- 1 | # security directives can generate oidc plugin configs 2 | 3 | openapi: '3.0.0' 4 | info: 5 | title: OIDC tests 6 | version: v2 7 | servers: 8 | - url: / 9 | 10 | # defining a toplevel security spec 11 | security: 12 | - doclevel-oidc: ["top-scope"] 13 | 14 | # overriding the top level with per operation ones below 15 | paths: 16 | /path1: 17 | get: 18 | operationId: OpsId1 19 | # this should get the plugin per doclevel-oidc specs, since we do not override 20 | summary: List API versions 21 | responses: 22 | '200': 23 | description: |- 24 | 200 response 25 | /path2: 26 | get: 27 | operationId: OpsId2 28 | # this should get the plugin per "oidc2" specs, NO additional scopes 29 | summary: List API versions 30 | security: 31 | - oidc2: [] # no additional scopes 32 | responses: 33 | '200': 34 | description: |- 35 | 200 response 36 | /path3: 37 | get: 38 | operationId: OpsId3 39 | # this should get the plugin per "oidc3" specs, with additional scopes 40 | summary: List API versions 41 | security: 42 | - oidc3: [ "extra-scope" ] # with additional scopes 43 | responses: 44 | '200': 45 | description: |- 46 | 200 response 47 | /path4: 48 | get: 49 | operationId: OpsId4 50 | # this should get the plugin per "oidc4" specs, without references 51 | summary: List API versions 52 | security: 53 | - oidc4: [ "oidc4-scope" ] 54 | responses: 55 | '200': 56 | description: |- 57 | 200 response 58 | /path5: 59 | get: 60 | operationId: OpsId5 61 | # same as no 4, but a different scope 62 | summary: List API versions 63 | security: 64 | - oidc4: [ "oidc5-scope" ] # different scope; 5 65 | responses: 66 | '200': 67 | description: |- 68 | 200 response 69 | 70 | 71 | components: 72 | x-kong: 73 | plugins: 74 | oidc2: 75 | config: 76 | run_on_preflight: true 77 | scopes_required: ["oidc2-plugin-ref"] 78 | security: 79 | oidc3: 80 | config: 81 | run_on_preflight: true 82 | scopes_required: ["oidc3-security-ref"] 83 | 84 | 85 | securitySchemes: 86 | doclevel-oidc: 87 | type: openIdConnect 88 | openIdConnectUrl: https://konghq.com/top-level 89 | x-kong-security-openid-connect: 90 | config: 91 | run_on_preflight: false 92 | scopes_required: ["scope2", "scope1"] # reversed order to test sorting! 93 | 94 | oidc2: 95 | type: openIdConnect 96 | openIdConnectUrl: https://konghq.com/oidc2 97 | # using a reference to a security scheme, in the "plugins" section 98 | x-kong-security-openid-connect: 99 | "$ref": "#/components/x-kong/plugins/oidc2" 100 | 101 | oidc3: 102 | type: openIdConnect 103 | openIdConnectUrl: https://konghq.com/oidc3 104 | # using a reference to a security scheme, in the "security" section 105 | x-kong-security-openid-connect: 106 | "$ref": "#/components/x-kong/security/oidc3" 107 | 108 | oidc4: 109 | type: openIdConnect 110 | openIdConnectUrl: https://konghq.com/oidc4 111 | x-kong-security-openid-connect: 112 | config: 113 | run_on_preflight: false 114 | scopes_required: [] 115 | issuer: "https://konghq.com/oidc4/overridden" 116 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/09a-plugins-with-consumers.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "plugins": [ 4 | { 5 | "config": { 6 | "message": "For a moment, nothing happened. Then, after a second or so, nothing continued to happen.", 7 | "status_code": 403 8 | }, 9 | "consumer": "johndoe3", 10 | "id": "ead16074-ccb0-52dd-9f56-4193529e8ffa", 11 | "name": "request-termination", 12 | "route": "simple-api-overview_uses-ops-plugin", 13 | "tags": [ 14 | "OAS3_import", 15 | "OAS3file_09a-plugins-with-consumers.yaml" 16 | ] 17 | }, 18 | { 19 | "config": { 20 | "message": "The answer to life, the universe, and everything!", 21 | "status_code": 403 22 | }, 23 | "consumer": "johndoe2", 24 | "id": "aa56031e-7155-599f-a9e9-93e6b271ba58", 25 | "name": "request-termination", 26 | "route": "simple-api-overview_uses-path-plugin", 27 | "tags": [ 28 | "OAS3_import", 29 | "OAS3file_09a-plugins-with-consumers.yaml" 30 | ] 31 | }, 32 | { 33 | "config": { 34 | "message": "So long and thanks for all the fish!", 35 | "status_code": 403 36 | }, 37 | "consumer": "johndoe1", 38 | "id": "ef93d30f-dc7d-581b-8b1b-3f8942a0d171", 39 | "name": "request-termination", 40 | "service": "simple-api-overview", 41 | "tags": [ 42 | "OAS3_import", 43 | "OAS3file_09a-plugins-with-consumers.yaml" 44 | ] 45 | } 46 | ], 47 | "services": [ 48 | { 49 | "host": "server1.com", 50 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 51 | "name": "simple-api-overview", 52 | "path": "/", 53 | "plugins": [], 54 | "port": 443, 55 | "protocol": "https", 56 | "routes": [ 57 | { 58 | "id": "d020c736-e762-5dad-8415-1684ee4f2061", 59 | "methods": [ 60 | "GET" 61 | ], 62 | "name": "simple-api-overview_uses-doc-plugin", 63 | "paths": [ 64 | "~/path1$" 65 | ], 66 | "plugins": [], 67 | "regex_priority": 200, 68 | "strip_path": false, 69 | "tags": [ 70 | "OAS3_import", 71 | "OAS3file_09a-plugins-with-consumers.yaml" 72 | ] 73 | }, 74 | { 75 | "id": "ef2ca083-29b3-5d7b-87c5-e4315d830c33", 76 | "methods": [ 77 | "GET" 78 | ], 79 | "name": "simple-api-overview_uses-path-plugin", 80 | "paths": [ 81 | "~/path2$" 82 | ], 83 | "plugins": [], 84 | "regex_priority": 200, 85 | "strip_path": false, 86 | "tags": [ 87 | "OAS3_import", 88 | "OAS3file_09a-plugins-with-consumers.yaml" 89 | ] 90 | }, 91 | { 92 | "id": "f9c8a7d7-3518-5c25-b66b-40943e59f91b", 93 | "methods": [ 94 | "POST" 95 | ], 96 | "name": "simple-api-overview_uses-ops-plugin", 97 | "paths": [ 98 | "~/path2$" 99 | ], 100 | "plugins": [], 101 | "regex_priority": 200, 102 | "strip_path": false, 103 | "tags": [ 104 | "OAS3_import", 105 | "OAS3file_09a-plugins-with-consumers.yaml" 106 | ] 107 | } 108 | ], 109 | "tags": [ 110 | "OAS3_import", 111 | "OAS3file_09a-plugins-with-consumers.yaml" 112 | ] 113 | } 114 | ], 115 | "upstreams": [] 116 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/02-names-set.expected_inso.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "oas-spec-name.upstream", 6 | "name": "oas-spec-name", 7 | "path": "/some/path", 8 | "plugins": [], 9 | "port": 80, 10 | "protocol": "http", 11 | "routes": [ 12 | { 13 | "methods": [ 14 | "GET" 15 | ], 16 | "name": "oas-spec-name-opsid1", 17 | "paths": [ 18 | "~/app1$" 19 | ], 20 | "plugins": [], 21 | "regex_priority": 200, 22 | "strip_path": false, 23 | "tags": [ 24 | "OAS3_import", 25 | "OAS3file_02-names-set.yaml" 26 | ] 27 | }, 28 | { 29 | "methods": [ 30 | "POST" 31 | ], 32 | "name": "oas-spec-name-path-name-post", 33 | "paths": [ 34 | "~/app1$" 35 | ], 36 | "plugins": [], 37 | "regex_priority": 200, 38 | "strip_path": false, 39 | "tags": [ 40 | "OAS3_import", 41 | "OAS3file_02-names-set.yaml" 42 | ] 43 | }, 44 | { 45 | "methods": [ 46 | "PUT" 47 | ], 48 | "name": "oas-spec-name-my-put-operation", 49 | "paths": [ 50 | "~/app1$" 51 | ], 52 | "plugins": [], 53 | "regex_priority": 200, 54 | "strip_path": false, 55 | "tags": [ 56 | "OAS3_import", 57 | "OAS3file_02-names-set.yaml" 58 | ] 59 | }, 60 | { 61 | "methods": [ 62 | "GET" 63 | ], 64 | "name": "oas-spec-name-opsid2", 65 | "paths": [ 66 | "~/app2$" 67 | ], 68 | "plugins": [], 69 | "regex_priority": 200, 70 | "strip_path": false, 71 | "tags": [ 72 | "OAS3_import", 73 | "OAS3file_02-names-set.yaml" 74 | ] 75 | }, 76 | { 77 | "methods": [ 78 | "POST" 79 | ], 80 | "name": "oas-spec-name-app2-post", 81 | "paths": [ 82 | "~/app2$" 83 | ], 84 | "plugins": [], 85 | "regex_priority": 200, 86 | "strip_path": false, 87 | "tags": [ 88 | "OAS3_import", 89 | "OAS3file_02-names-set.yaml" 90 | ] 91 | }, 92 | { 93 | "methods": [ 94 | "PUT" 95 | ], 96 | "name": "oas-spec-name-my-put-operation", 97 | "paths": [ 98 | "~/app2$" 99 | ], 100 | "plugins": [], 101 | "regex_priority": 200, 102 | "strip_path": false, 103 | "tags": [ 104 | "OAS3_import", 105 | "OAS3file_02-names-set.yaml" 106 | ] 107 | } 108 | ], 109 | "tags": [ 110 | "OAS3_import", 111 | "OAS3file_02-names-set.yaml" 112 | ] 113 | } 114 | ], 115 | "upstreams": [ 116 | { 117 | "name": "oas-spec-name.upstream", 118 | "tags": [ 119 | "OAS3_import", 120 | "OAS3file_02-names-set.yaml" 121 | ], 122 | "targets": [ 123 | { 124 | "tags": [ 125 | "OAS3_import", 126 | "OAS3file_02-names-set.yaml" 127 | ], 128 | "target": "backend1.com:80" 129 | }, 130 | { 131 | "tags": [ 132 | "OAS3_import", 133 | "OAS3file_02-names-set.yaml" 134 | ], 135 | "target": "backend2.com:80" 136 | } 137 | ] 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /namespace/namespace_host_test.go: -------------------------------------------------------------------------------- 1 | package namespace_test 2 | 3 | import ( 4 | "github.com/kong/go-apiops/namespace" 5 | "github.com/kong/go-apiops/yamlbasics" 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Host-Namespace", func() { 11 | Describe("ApplyNamespaceHost", func() { 12 | // clear == fasle/true 13 | // hosts exists/not exists 14 | // hosts has a name, has no name 15 | // hosts has the namespace (eg adding duplicate) 16 | Describe("clear hosts", func() { 17 | It("clears the hosts", func() { 18 | data := `{ 19 | "routes": [ 20 | { 21 | "hosts": ["one", "two"] 22 | } 23 | ] 24 | }` 25 | deckfile := toYaml(data) 26 | hosts := []string{"three"} 27 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, true, false) 28 | Expect(err).To(BeNil()) 29 | Expect(toString(deckfile)).To(MatchJSON(`{ 30 | "routes": [ 31 | { 32 | "hosts": ["three"] 33 | } 34 | ] 35 | }`)) 36 | }) 37 | It("clears the hosts, no hosts", func() { 38 | data := `{ 39 | "routes": [ 40 | { 41 | "paths": [] 42 | } 43 | ] 44 | }` 45 | deckfile := toYaml(data) 46 | hosts := []string{"three"} 47 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, true, false) 48 | Expect(err).To(BeNil()) 49 | Expect(toString(deckfile)).To(MatchJSON(`{ 50 | "routes": [ 51 | { 52 | "paths": [], 53 | "hosts": ["three"] 54 | } 55 | ] 56 | }`)) 57 | }) 58 | }) 59 | }) 60 | Describe("appends hosts", func() { 61 | It("Route without hosts array", func() { 62 | data := `{ 63 | "routes": [ 64 | { 65 | "paths": [] 66 | } 67 | ] 68 | }` 69 | deckfile := toYaml(data) 70 | hosts := []string{"three"} 71 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false) 72 | Expect(err).To(BeNil()) 73 | Expect(toString(deckfile)).To(MatchJSON(`{ 74 | "routes": [ 75 | { 76 | "paths": [], 77 | "hosts": ["three"] 78 | } 79 | ] 80 | }`)) 81 | }) 82 | It("Route with empty hosts array", func() { 83 | data := `{ 84 | "routes": [ 85 | { 86 | "hosts": [] 87 | } 88 | ] 89 | }` 90 | deckfile := toYaml(data) 91 | hosts := []string{"three"} 92 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false) 93 | Expect(err).To(BeNil()) 94 | Expect(toString(deckfile)).To(MatchJSON(`{ 95 | "routes": [ 96 | { 97 | "hosts": ["three"] 98 | } 99 | ] 100 | }`)) 101 | }) 102 | It("adds hosts", func() { 103 | data := `{ 104 | "routes": [ 105 | { 106 | "hosts": ["one", "two"] 107 | } 108 | ] 109 | }` 110 | deckfile := toYaml(data) 111 | hosts := []string{"three"} 112 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false) 113 | Expect(err).To(BeNil()) 114 | Expect(toString(deckfile)).To(MatchJSON(`{ 115 | "routes": [ 116 | { 117 | "hosts": ["one", "two", "three"] 118 | } 119 | ] 120 | }`)) 121 | }) 122 | It("doesn't add duplicate hosts", func() { 123 | data := `{ 124 | "routes": [ 125 | { 126 | "hosts": ["one", "two"] 127 | } 128 | ] 129 | }` 130 | deckfile := toYaml(data) 131 | hosts := []string{"one", "two", "three"} 132 | err := namespace.ApplyNamespaceHost(deckfile, yamlbasics.SelectorSet{}, hosts, false, false) 133 | Expect(err).To(BeNil()) 134 | Expect(toString(deckfile)).To(MatchJSON(`{ 135 | "routes": [ 136 | { 137 | "hosts": ["one", "two", "three"] 138 | } 139 | ] 140 | }`)) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /openapi2kong/service_test.go: -------------------------------------------------------------------------------- 1 | package openapi2kong 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | v3 "github.com/pb33f/libopenapi/datamodel/high/v3" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | ) 11 | 12 | func Test_parseServerUris(t *testing.T) { 13 | // basics 14 | 15 | servers := []*v3.Server{ 16 | { 17 | URL: "http://cookiemonster.com/chocolate/cookie", 18 | }, 19 | { 20 | URL: "https://konghq.com/bitter/sweet", 21 | }, 22 | } 23 | expected := []*url.URL{ 24 | { 25 | Scheme: "http", 26 | Host: "cookiemonster.com", 27 | Path: "/chocolate/cookie", 28 | }, { 29 | Scheme: httpsScheme, 30 | Host: "konghq.com", 31 | Path: "/bitter/sweet", 32 | }, 33 | } 34 | targets, err := parseServerUris(servers) 35 | if err != nil { 36 | t.Errorf("did not expect error: %v", err) 37 | } 38 | if diff := cmp.Diff(targets, expected); diff != "" { 39 | t.Errorf(diff) //nolint:staticcheck 40 | } 41 | 42 | variables := orderedmap.New[string, *v3.ServerVariable]() 43 | variables.Set("var1", &v3.ServerVariable{ 44 | Default: "hello", 45 | Enum: []string{"hello", "world"}, 46 | }) 47 | variables.Set("var2", &v3.ServerVariable{ 48 | Default: "Welt", 49 | Enum: []string{"hallo", "Welt"}, 50 | }) 51 | 52 | // replaces variables with defaults 53 | servers = []*v3.Server{ 54 | { 55 | URL: "http://{var1}-{var2}.com/chocolate/cookie", 56 | Variables: variables, 57 | }, 58 | } 59 | 60 | expected = []*url.URL{ 61 | { 62 | Scheme: "http", 63 | Host: "hello-Welt.com", 64 | Path: "/chocolate/cookie", 65 | }, 66 | } 67 | targets, err = parseServerUris(servers) 68 | if err != nil { 69 | t.Errorf("did not expect error: %v", err) 70 | } 71 | if diff := cmp.Diff(targets, expected); diff != "" { 72 | t.Errorf(diff) //nolint:staticcheck 73 | } 74 | 75 | // returns error on a bad URL 76 | 77 | servers = []*v3.Server{ 78 | { 79 | URL: "http://cookiemonster.com/chocolate/cookie", 80 | }, { 81 | URL: "not really a url...", 82 | }, 83 | } 84 | _, err = parseServerUris(servers) 85 | if err == nil { 86 | t.Error("expected an error") 87 | } 88 | 89 | // returns no error if servers is empty 90 | 91 | expected = []*url.URL{ 92 | { 93 | Path: "/", 94 | }, 95 | } 96 | targets, err = parseServerUris([]*v3.Server{}) 97 | if err != nil { 98 | t.Errorf("did not expect error: %v", err) 99 | } 100 | if diff := cmp.Diff(targets, expected); diff != "" { 101 | t.Errorf(diff) //nolint:staticcheck 102 | } 103 | 104 | // returns no error if servers is nil 105 | 106 | expected = []*url.URL{ 107 | { 108 | Path: "/", 109 | }, 110 | } 111 | targets, err = parseServerUris(nil) 112 | if err != nil { 113 | t.Errorf("did not expect error: %v", err) 114 | } 115 | if diff := cmp.Diff(targets, expected); diff != "" { 116 | t.Errorf(diff) //nolint:staticcheck 117 | } 118 | } 119 | 120 | func Test_setServerDefaults(t *testing.T) { 121 | defaultTests := []struct { 122 | name string 123 | inURL string 124 | outPort string 125 | outScheme string 126 | }{ 127 | {"adds default scheme", "//host/path", "443", "https"}, 128 | {"adds port 80 for http", "http://host/path", "80", "http"}, 129 | {"adds port 443 for https", "https://host/path", "443", "https"}, 130 | } 131 | 132 | for _, tst := range defaultTests { 133 | inURL, _ := url.Parse(tst.inURL) 134 | urls := []*url.URL{inURL} 135 | setServerDefaults(urls, "https") 136 | if urls[0].Port() != tst.outPort { 137 | t.Errorf("%s: expected port to be '%s', but got '%s'", tst.name, tst.outPort, urls[0].Port()) 138 | } 139 | if urls[0].Scheme != tst.outScheme { 140 | t.Errorf("%s: expected scheme to be '%s', but got '%s'", tst.name, tst.outScheme, urls[0].Scheme) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/07-service-defaults-overrides.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "simple-api-overview.upstream", 6 | "id": "0907c4ab-d9e4-5d21-813b-c57a97eeaad9", 7 | "name": "simple-api-overview", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "retries": 100, 13 | "routes": [ 14 | { 15 | "id": "663104d8-7e60-525d-b506-e42971b4466b", 16 | "methods": [ 17 | "GET" 18 | ], 19 | "name": "simple-api-overview_uses-doc-service", 20 | "paths": [ 21 | "~/path1$" 22 | ], 23 | "plugins": [], 24 | "regex_priority": 200, 25 | "strip_path": false, 26 | "tags": [ 27 | "OAS3_import", 28 | "OAS3file_07-service-defaults-overrides.yaml" 29 | ] 30 | } 31 | ], 32 | "tags": [ 33 | "OAS3_import", 34 | "OAS3file_07-service-defaults-overrides.yaml" 35 | ] 36 | }, 37 | { 38 | "host": "simple-api-overview.upstream", 39 | "id": "a79c5a8c-0924-599e-9412-39f5a4ff0c3e", 40 | "name": "simple-api-overview_path2", 41 | "path": "/", 42 | "plugins": [], 43 | "port": 443, 44 | "protocol": "https", 45 | "retries": 200, 46 | "routes": [ 47 | { 48 | "id": "c1b84366-8ff8-57b9-b118-bedd1b9ab1c8", 49 | "methods": [ 50 | "GET" 51 | ], 52 | "name": "simple-api-overview_uses-path-service", 53 | "paths": [ 54 | "~/path2$" 55 | ], 56 | "plugins": [], 57 | "regex_priority": 200, 58 | "strip_path": false, 59 | "tags": [ 60 | "OAS3_import", 61 | "OAS3file_07-service-defaults-overrides.yaml" 62 | ] 63 | } 64 | ], 65 | "tags": [ 66 | "OAS3_import", 67 | "OAS3file_07-service-defaults-overrides.yaml" 68 | ] 69 | }, 70 | { 71 | "host": "simple-api-overview.upstream", 72 | "id": "5d05e040-c551-50a1-90e7-6af5d3a7c0dc", 73 | "name": "simple-api-overview_uses-ops-service", 74 | "path": "/", 75 | "plugins": [], 76 | "port": 443, 77 | "protocol": "https", 78 | "retries": 300, 79 | "routes": [ 80 | { 81 | "id": "ca1c6faa-7076-5a09-9346-9c9dc3e019cb", 82 | "methods": [ 83 | "POST" 84 | ], 85 | "name": "simple-api-overview_uses-ops-service", 86 | "paths": [ 87 | "~/path2$" 88 | ], 89 | "plugins": [], 90 | "regex_priority": 200, 91 | "strip_path": false, 92 | "tags": [ 93 | "OAS3_import", 94 | "OAS3file_07-service-defaults-overrides.yaml" 95 | ] 96 | } 97 | ], 98 | "tags": [ 99 | "OAS3_import", 100 | "OAS3file_07-service-defaults-overrides.yaml" 101 | ] 102 | } 103 | ], 104 | "upstreams": [ 105 | { 106 | "id": "811c42d6-ef18-5296-a550-7dca2262b4d8", 107 | "name": "simple-api-overview.upstream", 108 | "tags": [ 109 | "OAS3_import", 110 | "OAS3file_07-service-defaults-overrides.yaml" 111 | ], 112 | "targets": [ 113 | { 114 | "tags": [ 115 | "OAS3_import", 116 | "OAS3file_07-service-defaults-overrides.yaml" 117 | ], 118 | "target": "server1.com:443" 119 | }, 120 | { 121 | "tags": [ 122 | "OAS3_import", 123 | "OAS3file_07-service-defaults-overrides.yaml" 124 | ], 125 | "target": "server2.com:443" 126 | } 127 | ] 128 | } 129 | ] 130 | } -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/01-names-inferred.expected_inso.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "Simple_API_overview.upstream", 6 | "name": "Simple_API_overview", 7 | "path": "/some/path", 8 | "plugins": [], 9 | "port": 80, 10 | "protocol": "http", 11 | "routes": [ 12 | { 13 | "methods": [ 14 | "GET" 15 | ], 16 | "name": "Simple_API_overview-opsid1", 17 | "paths": [ 18 | "~/$" 19 | ], 20 | "plugins": [], 21 | "regex_priority": 200, 22 | "strip_path": false, 23 | "tags": [ 24 | "OAS3_import", 25 | "OAS3file_01-names-inferred.yaml" 26 | ] 27 | }, 28 | { 29 | "methods": [ 30 | "POST" 31 | ], 32 | "name": "Simple_API_overview-post", 33 | "paths": [ 34 | "~/$" 35 | ], 36 | "plugins": [], 37 | "regex_priority": 200, 38 | "strip_path": false, 39 | "tags": [ 40 | "OAS3_import", 41 | "OAS3file_01-names-inferred.yaml" 42 | ] 43 | }, 44 | { 45 | "methods": [ 46 | "GET" 47 | ], 48 | "name": "Simple_API_overview-opsid2", 49 | "paths": [ 50 | "~/application$" 51 | ], 52 | "plugins": [], 53 | "regex_priority": 200, 54 | "strip_path": false, 55 | "tags": [ 56 | "OAS3_import", 57 | "OAS3file_01-names-inferred.yaml" 58 | ] 59 | }, 60 | { 61 | "methods": [ 62 | "POST" 63 | ], 64 | "name": "Simple_API_overview-application-post", 65 | "paths": [ 66 | "~/application$" 67 | ], 68 | "plugins": [], 69 | "regex_priority": 200, 70 | "strip_path": false, 71 | "tags": [ 72 | "OAS3_import", 73 | "OAS3file_01-names-inferred.yaml" 74 | ] 75 | }, 76 | { 77 | "methods": [ 78 | "GET" 79 | ], 80 | "name": "Simple_API_overview-opsid3", 81 | "paths": [ 82 | "~/application/$" 83 | ], 84 | "plugins": [], 85 | "regex_priority": 200, 86 | "strip_path": false, 87 | "tags": [ 88 | "OAS3_import", 89 | "OAS3file_01-names-inferred.yaml" 90 | ] 91 | }, 92 | { 93 | "methods": [ 94 | "POST" 95 | ], 96 | "name": "Simple_API_overview-application-post", 97 | "paths": [ 98 | "~/application/$" 99 | ], 100 | "plugins": [], 101 | "regex_priority": 200, 102 | "strip_path": false, 103 | "tags": [ 104 | "OAS3_import", 105 | "OAS3file_01-names-inferred.yaml" 106 | ] 107 | } 108 | ], 109 | "tags": [ 110 | "OAS3_import", 111 | "OAS3file_01-names-inferred.yaml" 112 | ] 113 | } 114 | ], 115 | "upstreams": [ 116 | { 117 | "name": "Simple_API_overview.upstream", 118 | "tags": [ 119 | "OAS3_import", 120 | "OAS3file_01-names-inferred.yaml" 121 | ], 122 | "targets": [ 123 | { 124 | "tags": [ 125 | "OAS3_import", 126 | "OAS3file_01-names-inferred.yaml" 127 | ], 128 | "target": "backend1.com:80" 129 | }, 130 | { 131 | "tags": [ 132 | "OAS3_import", 133 | "OAS3file_01-names-inferred.yaml" 134 | ], 135 | "target": "backend2.com:80" 136 | } 137 | ] 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /deckformat/deckformat_test.go: -------------------------------------------------------------------------------- 1 | package deckformat_test 2 | 3 | import ( 4 | . "github.com/kong/go-apiops/deckformat" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("deckformat", func() { 10 | Describe("application version", func() { 11 | It("ToolVersionSet/Get/String", func() { 12 | ToolVersionSet("my-name", "1.2.3", "commit-xyz") 13 | 14 | n, v, c := ToolVersionGet() 15 | Expect(n).To(BeIdenticalTo("my-name")) 16 | Expect(v).To(BeIdenticalTo("1.2.3")) 17 | Expect(c).To(BeIdenticalTo("commit-xyz")) 18 | 19 | Expect(ToolVersionString()).Should(Equal("my-name 1.2.3 (commit-xyz)")) 20 | 21 | Expect(func() { 22 | ToolVersionSet("another name", "1.2.3", "commit-xyz") 23 | }).Should(Panic()) 24 | }) 25 | }) 26 | Describe("history", func() { 27 | It("the key is set to '_ignore'", func() { 28 | Expect(HistoryKey).To(Equal("_ignore")) 29 | }) 30 | 31 | Describe("HistoryGet", func() { 32 | It("returns the history array", func() { 33 | hist := []interface{}{"one", "two"} 34 | data := map[string]interface{}{ 35 | HistoryKey: hist, 36 | } 37 | res := HistoryGet(data) 38 | 39 | Expect(res).To(BeEquivalentTo(hist)) 40 | }) 41 | 42 | It("returns an empty array if none found", func() { 43 | res := HistoryGet(map[string]interface{}{}) 44 | Expect(res).To(BeEquivalentTo([]interface{}{})) 45 | }) 46 | 47 | It("returns an empty array if data-in is nil", func() { 48 | res := HistoryGet(nil) 49 | Expect(res).To(BeEquivalentTo([]interface{}{})) 50 | }) 51 | 52 | It("appends the existing history entry if it's not an array", func() { 53 | data := map[string]interface{}{ 54 | HistoryKey: "foobar", 55 | } 56 | res := HistoryGet(data) 57 | Expect(res).To(BeEquivalentTo([]interface{}{"foobar"})) 58 | }) 59 | }) 60 | 61 | It("HistoryNewEntry creates a new entry", func() { 62 | // ToolVersionSet("my-name", "1.2.3", "commit-xyz") 63 | cmd := "myCmd" 64 | entry := HistoryNewEntry(cmd) 65 | 66 | Expect(entry).To(BeEquivalentTo(map[string]interface{}{ 67 | "command": cmd, 68 | "tool": ToolVersionString(), 69 | })) 70 | }) 71 | 72 | Describe("HistorySet", func() { 73 | PIt("sets the history array", func() { 74 | hist := []interface{}{"one", "two"} 75 | data := map[string]interface{}{} 76 | 77 | HistorySet(data, hist) 78 | res := data[HistoryKey].([]interface{}) 79 | Expect(res).To(BeEquivalentTo(hist)) 80 | }) 81 | 82 | It("deletes history-array if nil", func() { 83 | data := map[string]interface{}{ 84 | HistoryKey: "delete me", 85 | } 86 | HistorySet(data, nil) 87 | 88 | res, found := data[HistoryKey] 89 | Expect(res).To(BeNil()) 90 | Expect(found).To(BeFalse()) 91 | }) 92 | }) 93 | 94 | PDescribe("HistoryAppend", func() { 95 | It("adds an entry to an existing array", func() { 96 | hist := []interface{}{"one", "two"} 97 | data := map[string]interface{}{ 98 | HistoryKey: hist, 99 | } 100 | HistoryAppend(data, "three") 101 | res := HistoryGet(data) 102 | 103 | Expect(res).To(BeEquivalentTo([]interface{}{"one", "two", "three"})) 104 | }) 105 | 106 | It("creates an array if it doesn't exist", func() { 107 | data := map[string]interface{}{} 108 | 109 | HistoryAppend(data, "one") 110 | 111 | res := HistoryGet(data) 112 | Expect(res).To(BeEquivalentTo([]interface{}{"one"})) 113 | }) 114 | }) 115 | 116 | Describe("HistoryClear", func() { 117 | It("clears the history key", func() { 118 | data := map[string]interface{}{ 119 | HistoryKey: "delete me", 120 | } 121 | 122 | HistoryClear(data) 123 | 124 | res, found := data[HistoryKey] 125 | Expect(res).To(BeNil()) 126 | Expect(found).To(BeFalse()) 127 | }) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /openapi2kong/utils.go: -------------------------------------------------------------------------------- 1 | package openapi2kong 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kong/go-slugify" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Slugify converts a name to a valid Kong name by removing and replacing unallowed characters 12 | // and sanitizing non-latin characters. Multiple inputs will be concatenated using '_'. 13 | func Slugify(insoCompat bool, name ...string) string { 14 | var ( 15 | slugifier *slugify.Slugifier 16 | concatBy string 17 | ) 18 | if insoCompat { 19 | slugifier = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") 20 | slugifier.AllowedSet("a-zA-Z0-9\\-") 21 | concatBy = "-" 22 | } else { 23 | slugifier = (&slugify.Slugifier{}).ToLower(true).InvalidChar("-").WordSeparator("-") 24 | concatBy = "_" 25 | } 26 | 27 | for i, elem := range name { 28 | name[i] = slugifier.Slugify(elem) 29 | } 30 | 31 | // drop empty strings from the array 32 | for i := 0; i < len(name); i++ { 33 | if name[i] == "" { 34 | name = append(name[:i], name[i+1:]...) 35 | i-- 36 | } 37 | } 38 | 39 | return strings.Join(name, concatBy) 40 | } 41 | 42 | // sanitizeRegexCapture will remove illegal characters from the path-variable name. 43 | // The returned name will be valid for PCRE regex captures; Alphanumeric + '_', starting 44 | // with [a-zA-Z]. 45 | func sanitizeRegexCapture(varName string, insoCompat bool) string { 46 | var regexName *slugify.Slugifier 47 | if insoCompat { 48 | regexName = (&slugify.Slugifier{}).ToLower(false).InvalidChar("_").WordSeparator("_") 49 | } else { 50 | regexName = (&slugify.Slugifier{}).ToLower(true).InvalidChar("_").WordSeparator("_") 51 | } 52 | return regexName.Slugify(varName) 53 | } 54 | 55 | func dereferenceJSONObject( 56 | value map[string]interface{}, 57 | components *map[string]interface{}, 58 | ) (map[string]interface{}, error) { 59 | var pointer string 60 | 61 | switch value["$ref"].(type) { 62 | case nil: // it is not a reference, so return the object 63 | return value, nil 64 | 65 | case string: // it is a json pointer 66 | pointer = value["$ref"].(string) 67 | if !strings.HasPrefix(pointer, "#/components/x-kong/") { 68 | return nil, fmt.Errorf("all 'x-kong-...' references must be at '#/components/x-kong/...'") 69 | } 70 | 71 | default: // bad pointer 72 | return nil, fmt.Errorf("expected '$ref' pointer to be a string") 73 | } 74 | 75 | // walk the tree to find the reference 76 | segments := strings.Split(pointer, "/") 77 | path := "#/components/x-kong" 78 | result := components 79 | 80 | for i := 3; i < len(segments); i++ { 81 | segment := segments[i] 82 | path = path + "/" + segment 83 | 84 | switch (*result)[segment].(type) { 85 | case nil: 86 | return nil, fmt.Errorf("reference '%s' not found", pointer) 87 | case map[string]interface{}: 88 | target := (*result)[segment].(map[string]interface{}) 89 | result = &target 90 | default: 91 | return nil, fmt.Errorf("expected '%s' to be a JSON object", path) 92 | } 93 | } 94 | 95 | return *result, nil 96 | } 97 | 98 | func convertYamlNodeToBytes(node *yaml.Node) ([]byte, error) { 99 | var data interface{} 100 | err := node.Decode(&data) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return yaml.Marshal(data) 105 | } 106 | 107 | // crossProduct computes the Cartesian product of the input slices. 108 | // The slices can be of any type 109 | func crossProduct(slices ...[]any) [][]any { 110 | if len(slices) == 0 { 111 | return [][]any{{}} 112 | } 113 | 114 | result := [][]any{{}} 115 | for _, slice := range slices { 116 | var next [][]any 117 | for _, prefix := range result { 118 | for _, elem := range slice { 119 | newTuple := append([]any{}, prefix...) 120 | newTuple = append(newTuple, elem) 121 | next = append(next, newTuple) 122 | } 123 | } 124 | result = next 125 | } 126 | 127 | return result 128 | } 129 | -------------------------------------------------------------------------------- /openapi2kong/oas3_testfiles/12-path-parameter-regex.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "_format_version": "3.0", 3 | "services": [ 4 | { 5 | "host": "example.com", 6 | "id": "520e4991-2d1a-59b8-bf2e-579cca0969a0", 7 | "name": "path-parameter-test", 8 | "path": "/", 9 | "plugins": [], 10 | "port": 443, 11 | "protocol": "https", 12 | "routes": [ 13 | { 14 | "id": "38dcec89-d67c-5f67-ab5e-b38cc2180f77", 15 | "methods": [ 16 | "GET" 17 | ], 18 | "name": "path-parameter-test_getbatchnoparams", 19 | "paths": [ 20 | "~/batchs\\(Material='iron',Batch='10'\\)$" 21 | ], 22 | "plugins": [], 23 | "regex_priority": 200, 24 | "strip_path": false, 25 | "tags": [ 26 | "OAS3_import", 27 | "OAS3file_12-path-parameter-regex.yaml" 28 | ] 29 | }, 30 | { 31 | "id": "2af1536a-706a-5941-b3fe-2682e031edf9", 32 | "methods": [ 33 | "GET" 34 | ], 35 | "name": "path-parameter-test_getbatchwithparams", 36 | "paths": [ 37 | "~/batchs\\(Material='(?[^#?/]+)',Batch='(?[^#?/]+)'\\)$" 38 | ], 39 | "plugins": [], 40 | "regex_priority": 100, 41 | "strip_path": false, 42 | "tags": [ 43 | "OAS3_import", 44 | "OAS3file_12-path-parameter-regex.yaml" 45 | ] 46 | }, 47 | { 48 | "id": "8438e81a-7724-53a2-9b5b-5bb400ac8531", 49 | "methods": [ 50 | "POST" 51 | ], 52 | "name": "path-parameter-test_postbatchwithparams", 53 | "paths": [ 54 | "~/batchs\\(Material='(?[^#?/]+)',Batch='(?[^#?/]+)'\\)$" 55 | ], 56 | "plugins": [], 57 | "regex_priority": 100, 58 | "strip_path": false, 59 | "tags": [ 60 | "OAS3_import", 61 | "OAS3file_12-path-parameter-regex.yaml" 62 | ] 63 | }, 64 | { 65 | "id": "0da1f8dc-e918-5379-b3b0-ffc061ae1691", 66 | "methods": [ 67 | "GET" 68 | ], 69 | "name": "path-parameter-test_opsid", 70 | "paths": [ 71 | "~/demo/(?[^#?/]+)/(?[^#?/]+)/(?[^#?/]+)$" 72 | ], 73 | "plugins": [], 74 | "regex_priority": 100, 75 | "strip_path": false, 76 | "tags": [ 77 | "OAS3_import", 78 | "OAS3file_12-path-parameter-regex.yaml" 79 | ] 80 | }, 81 | { 82 | "id": "0da1f8dc-e918-5379-b3b0-ffc061ae1691", 83 | "methods": [ 84 | "GET" 85 | ], 86 | "name": "path-parameter-test_opsid", 87 | "paths": [ 88 | "~/demo/(?[^#?/]+)/else/(?[^#?/]+)/$" 89 | ], 90 | "plugins": [], 91 | "regex_priority": 100, 92 | "strip_path": false, 93 | "tags": [ 94 | "OAS3_import", 95 | "OAS3file_12-path-parameter-regex.yaml" 96 | ] 97 | }, 98 | { 99 | "id": "106b6fda-9cf1-5f75-80a4-adf271ef897b", 100 | "methods": [ 101 | "GET" 102 | ], 103 | "name": "path-parameter-test_dollar-sign", 104 | "paths": [ 105 | "~/some/\\$test$" 106 | ], 107 | "plugins": [], 108 | "regex_priority": 200, 109 | "strip_path": false, 110 | "tags": [ 111 | "OAS3_import", 112 | "OAS3file_12-path-parameter-regex.yaml" 113 | ] 114 | } 115 | ], 116 | "tags": [ 117 | "OAS3_import", 118 | "OAS3file_12-path-parameter-regex.yaml" 119 | ] 120 | } 121 | ], 122 | "upstreams": [] 123 | } -------------------------------------------------------------------------------- /namespace/namespace_host.go: -------------------------------------------------------------------------------- 1 | package namespace 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/kong/go-apiops/logbasics" 8 | "github.com/kong/go-apiops/yamlbasics" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // ApplyNamespaceHost applies the namespace to the hosts field of the selected routes 13 | // by adding the listed hosts if they ar not in the list already. 14 | func ApplyNamespaceHost( 15 | deckfile *yaml.Node, // the deckFile to operate on 16 | selectors yamlbasics.SelectorSet, // the selectors to use to select the routes 17 | hosts []string, // the hosts to add to the routes 18 | //nolint:predeclared 19 | clear bool, // if true, clear the hosts field before adding the hosts 20 | allowEmptySelection bool, // if true, do not return an error if no routes are selected 21 | ) error { 22 | if deckfile == nil { 23 | panic("expected 'deckfile' to be non-nil") 24 | } 25 | 26 | allRoutes := getAllRoutes(deckfile) 27 | var targetRoutes yamlbasics.NodeSet 28 | var err error 29 | if selectors.IsEmpty() { 30 | // no selectors, apply to all routes 31 | targetRoutes = make(yamlbasics.NodeSet, len(allRoutes)) 32 | copy(targetRoutes, allRoutes) 33 | } else { 34 | targetRoutes, err = selectors.Find(deckfile) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | 40 | var remainder yamlbasics.NodeSet 41 | targetRoutes, remainder = allRoutes.Intersection(targetRoutes) // check for non-routes 42 | if len(remainder) != 0 { 43 | return fmt.Errorf("the selectors returned non-route entities; %d", len(remainder)) 44 | } 45 | if len(targetRoutes) == 0 { 46 | if allowEmptySelection { 47 | logbasics.Info("no routes matched the selectors, nothing to do") 48 | return nil 49 | } 50 | return errors.New("no routes matched the selectors") 51 | } 52 | 53 | return updateRouteHosts(targetRoutes, hosts, clear) 54 | } 55 | 56 | // updateRouteHosts updates the hosts field of the provided routes. 57 | // If clear is true, the hosts field is cleared before adding the hosts. 58 | // 59 | //nolint:predeclared 60 | func updateRouteHosts(routes yamlbasics.NodeSet, hosts []string, clear bool) error { 61 | for _, route := range routes { 62 | if err := yamlbasics.CheckType(route, yamlbasics.TypeObject); err != nil { 63 | logbasics.Info("ignoring route: " + err.Error()) 64 | continue 65 | } 66 | 67 | hostsValueNode := yamlbasics.GetFieldValue(route, "hosts") 68 | if hostsValueNode == nil { 69 | // the 'hosts' array doesn't exist 70 | if len(hosts) == 0 { 71 | // nothing to do since we're not adding anything 72 | continue 73 | } 74 | // create an empty 'hosts' array, so we can add to it 75 | hostsValueNode = yamlbasics.NewArray() 76 | yamlbasics.SetFieldValue(route, "hosts", hostsValueNode) 77 | } else { 78 | // the 'hosts' array exists, check the type 79 | if err := yamlbasics.CheckType(hostsValueNode, yamlbasics.TypeArray); err != nil { 80 | logbasics.Info("ignoring route.hosts property: " + err.Error()) 81 | continue 82 | } 83 | } 84 | 85 | if clear && len(hostsValueNode.Content) > 0 { 86 | hostsValueNode.Content = make([]*yaml.Node, 0) 87 | } 88 | 89 | if len(hosts) > 0 { 90 | appendHosts(hostsValueNode, hosts) 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // appendHosts appends the provided hosts to the hosts array, without duplicates. 98 | func appendHosts(hostsValueNode *yaml.Node, hosts []string) { 99 | if hostsValueNode == nil || hostsValueNode.Kind != yaml.SequenceNode { 100 | panic("expected 'hostsValueNode' to be a sequence node") 101 | } 102 | if len(hosts) == 0 { 103 | panic("expected 'hosts' to be non-nil and non-empty") 104 | } 105 | 106 | for _, hostname := range hosts { 107 | exists := false 108 | for _, hostNameNode := range hostsValueNode.Content { 109 | if hostNameNode.Value == hostname { 110 | // already exists, skip 111 | exists = true 112 | break 113 | } 114 | } 115 | if !exists { 116 | // add the hostname to the array 117 | hostsValueNode.Content = append(hostsValueNode.Content, yamlbasics.NewString(hostname)) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /plugins/pluginfile.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kong/go-apiops/deckformat" 7 | "github.com/kong/go-apiops/filebasics" 8 | "github.com/kong/go-apiops/jsonbasics" 9 | "github.com/kong/go-apiops/logbasics" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // 14 | // 15 | // A single patch which can be repeated in a file 16 | // 17 | // 18 | 19 | // AddPluginPatch represents a single-patch that adds plugins to a set of objects. 20 | type AddPluginPatch struct { 21 | Selectors []string // JSONpath selectors 22 | Plugins []map[string]interface{} // plugin objects to add 23 | Overwrite bool // overwrite existing plugins 24 | } 25 | 26 | // Parse parses a patch. It will return an error if the patch is invalid. 27 | // 'source' will be used to format the error in case of an error. 28 | func (patch *AddPluginPatch) Parse(patchData map[string]interface{}, source string) error { 29 | var err error 30 | patch.Selectors, err = jsonbasics.GetStringArrayField(patchData, "selectors") 31 | if err != nil { 32 | return fmt.Errorf("%s.selectors: is not an array; %w", source, err) 33 | } 34 | 35 | // try and compile the json paths 36 | var tester Plugger 37 | err = tester.SetSelectors(patch.Selectors) 38 | if err != nil { 39 | return fmt.Errorf("%s.selectors: %w", source, err) 40 | } 41 | 42 | patch.Plugins, err = jsonbasics.GetObjectArrayField(patchData, "plugins") 43 | if err != nil { 44 | return fmt.Errorf("%s.plugins is not an array; %w", source, err) 45 | } 46 | 47 | patch.Overwrite = false 48 | if patchData["overwrite"] != nil { 49 | patch.Overwrite, err = jsonbasics.GetBoolField(patchData, "overwrite") 50 | if err != nil { 51 | return fmt.Errorf("%s.overwrite: %w", source, err) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Apply applies the patch to the yaml.Node given. 59 | func (patch *AddPluginPatch) Apply(yamlData *yaml.Node) error { 60 | var plugger Plugger 61 | err := plugger.SetSelectors(patch.Selectors) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | plugger.SetYamlData(yamlData) 67 | 68 | return plugger.AddPlugins(patch.Plugins, patch.Overwrite) 69 | } 70 | 71 | // 72 | // 73 | // A file which can hold multiple AddPluginPatches 74 | // 75 | // 76 | 77 | // DeckPluginFile represents a list of Add-Plugin patches. 78 | type DeckPluginFile struct { 79 | VersionMajor int // 0 if not present 80 | VersionMinor int // 0 if not present 81 | Plugins []AddPluginPatch 82 | } 83 | 84 | // ParseFile parses a pluginfile. Any non-object in the 'patches' array will be 85 | // ignored. If the array doesn't exist, it returns an empty array. 86 | func (pluginFile *DeckPluginFile) ParseFile(filename string) error { 87 | data, err := filebasics.DeserializeFile(filename) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if data[deckformat.VersionKey] != nil { 93 | logbasics.Debug("parsed plugin file", "file", filename, "version", data[deckformat.VersionKey]) 94 | pluginFile.VersionMajor, pluginFile.VersionMinor, err = deckformat.ParseFormatVersion(data) 95 | if err != nil { 96 | return fmt.Errorf("%s: has an invalid "+deckformat.VersionKey+" specified; %w", filename, err) 97 | } 98 | } else { 99 | logbasics.Debug("parsed unversioned plugin-file", "file", filename) 100 | } 101 | 102 | patchesRead, err := jsonbasics.GetObjectArrayField(data, "add-plugins") 103 | if err != nil { 104 | return fmt.Errorf("%s: field 'add-plugins' is not an array; %w", filename, err) 105 | } 106 | 107 | for i, patch := range patchesRead { 108 | var addPluginPatch AddPluginPatch 109 | err := addPluginPatch.Parse(patch, fmt.Sprintf("%s: add-plugins[%d]", filename, i)) 110 | if err != nil { 111 | return err 112 | } 113 | pluginFile.Plugins = append(pluginFile.Plugins, addPluginPatch) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // Apply applies the set of patches on the yaml.Node given. 120 | func (pluginFile *DeckPluginFile) Apply(yamlData *yaml.Node) error { 121 | for i, patch := range pluginFile.Plugins { 122 | err := patch.Apply(yamlData) 123 | if err != nil { 124 | return fmt.Errorf("failed to apply add-plugin patch %d; %w", i, err) 125 | } 126 | } 127 | return nil 128 | } 129 | --------------------------------------------------------------------------------