├── .changelog ├── 101.txt ├── 143.txt ├── 154.txt ├── 156.txt ├── 162.txt ├── 167.txt ├── 187.txt ├── 195.txt ├── 197.txt ├── 200.txt ├── 201.txt ├── 202.txt ├── 207.txt ├── 224.txt ├── 225.txt ├── 227.txt ├── 247.txt ├── 278.txt ├── 279.txt ├── 282.txt ├── 283.txt ├── 290.txt ├── 291.txt ├── 308.txt ├── 368.txt ├── 371.txt ├── 377.txt ├── 384.txt ├── 385.txt ├── 391.txt ├── 397.txt ├── 405.txt ├── 406.txt ├── 412.txt ├── 424.txt ├── 426.txt ├── 433.txt ├── 449.txt ├── 450.txt ├── 459.txt ├── 470.txt ├── 474.txt ├── 483.txt ├── 505.txt ├── 521.txt ├── 537.txt ├── 547.txt ├── 570.txt ├── 595.txt ├── changelog.tmpl ├── release-note.tmpl └── types.txt ├── .copywrite.hcl ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── SUPPORT.md ├── actions │ ├── goenv │ │ └── action.yml │ ├── setup-eks │ │ ├── action.yml │ │ └── cluster.tf │ ├── setup-kind │ │ └── action.yml │ └── teardown-eks │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── backport.yml │ ├── build.yml │ ├── changelog.yml │ ├── ci.yml │ ├── conformance.yml │ └── conformance_with_build.yml ├── .gitignore ├── .golangci.yml ├── .release ├── ci.hcl ├── release-metadata.hcl └── security-scan.hcl ├── CHANGELOG.md ├── CODEOWNERS ├── Dockerfile ├── Dockerfile.local ├── LICENSE ├── Makefile ├── README.md ├── assets └── logo.svg ├── config ├── base │ ├── gateway-class-config.yaml │ └── gateway-class.yaml ├── crd │ ├── bases │ │ ├── api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml │ │ └── api-gateway.consul.hashicorp.com_meshservices.yaml │ └── kustomization.yaml ├── deployment │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ ├── service.yaml │ └── serviceaccount.yaml ├── example │ ├── certificates.yaml │ ├── external-dns.yaml │ ├── gateway.yaml │ ├── kustomization.yaml │ ├── route.yaml │ └── service.yaml ├── kustomization.yaml └── rbac │ └── role.yaml ├── dev ├── config │ ├── consul │ │ └── acl-policy.hcl │ ├── helm │ │ └── consul.yaml │ ├── k8s │ │ ├── consul-api-gateway.yaml │ │ ├── consul-ui │ │ │ └── kustomization.yaml │ │ ├── rbac.yaml │ │ ├── service-account-secret.yaml │ │ └── service-account.yaml │ └── kind │ │ └── cluster.yaml ├── docs │ ├── README.md │ ├── assets │ │ ├── components.png │ │ ├── components.uxf │ │ ├── deployment.png │ │ └── deployment.uxf │ ├── components.md │ ├── deployment.md │ ├── example-setup.md │ ├── getting-started.md │ └── supported-features.md ├── run └── version ├── go.mod ├── go.sum ├── internal ├── adapters │ └── consul │ │ ├── http.go │ │ ├── http_test.go │ │ ├── sync.go │ │ ├── sync_test.go │ │ └── testdata │ │ ├── multiple-rules.golden.json │ │ ├── multiple-rules.json │ │ ├── multiple-services.golden.json │ │ ├── multiple-services.json │ │ ├── single-service.golden.json │ │ └── single-service.json ├── commands │ ├── exec │ │ ├── command.go │ │ ├── command_test.go │ │ ├── exec.go │ │ └── exec_test.go │ ├── server │ │ ├── command.go │ │ ├── command_test.go │ │ ├── k8s_e2e_test.go │ │ ├── server.go │ │ └── server_test.go │ └── version │ │ ├── command.go │ │ └── command_test.go ├── common │ ├── addresses.go │ ├── addresses_test.go │ ├── cli.go │ ├── flags.go │ ├── logger.go │ ├── mapper.go │ ├── service_names.go │ ├── service_names_test.go │ ├── tls.go │ └── writer.go ├── consul │ ├── auth.go │ ├── auth_test.go │ ├── certmanager.go │ ├── certmanager_test.go │ ├── common.go │ ├── config_entries.go │ ├── config_entries_test.go │ ├── connection.go │ ├── disco_chain_watcher.go │ ├── disco_chain_watcher_test.go │ ├── intentions.go │ ├── intentions_test.go │ ├── mocks │ │ ├── intentions.go │ │ └── peerings.go │ ├── namespaces.go │ ├── peerings.go │ ├── registration.go │ ├── registration_test.go │ └── test_client.go ├── core │ ├── http.go │ ├── interfaces.go │ ├── resolved.go │ ├── route.go │ └── tcp.go ├── envoy │ ├── handler.go │ ├── handler_test.go │ ├── manager.go │ ├── manager_test.go │ ├── middleware.go │ ├── mocks │ │ ├── middleware.go │ │ ├── sds.go │ │ └── secrets.go │ ├── sds.go │ ├── sds_test.go │ ├── secrets.go │ ├── secrets_test.go │ └── testdata │ │ ├── basic.golden.json │ │ ├── empty.golden.json │ │ ├── sds.golden.json │ │ └── tls.golden.json ├── grpc │ ├── logging.go │ └── logging_test.go ├── k8s │ ├── README.md │ ├── builder │ │ ├── builder.go │ │ ├── gateway.go │ │ ├── gateway_test.go │ │ └── testdata │ │ │ ├── clusterip.deployment.golden.yaml │ │ │ ├── clusterip.service.golden.yaml │ │ │ ├── clusterip.yaml │ │ │ ├── loadbalancer.deployment.golden.yaml │ │ │ ├── loadbalancer.service.golden.yaml │ │ │ ├── loadbalancer.yaml │ │ │ ├── max-instances.deployment.golden.yaml │ │ │ ├── max-instances.service.golden.yaml │ │ │ ├── max-instances.yaml │ │ │ ├── min-instances.deployment.golden.yaml │ │ │ ├── min-instances.service.golden.yaml │ │ │ ├── min-instances.yaml │ │ │ ├── multiple-instances.deployment.golden.yaml │ │ │ ├── multiple-instances.service.golden.yaml │ │ │ ├── multiple-instances.yaml │ │ │ ├── static-mapping.deployment.golden.yaml │ │ │ ├── static-mapping.service.golden.yaml │ │ │ ├── static-mapping.yaml │ │ │ ├── tls-cert.deployment.golden.yaml │ │ │ ├── tls-cert.service.golden.yaml │ │ │ └── tls-cert.yaml │ ├── certificates.go │ ├── config.go │ ├── controller.go │ ├── controllers │ │ ├── gateway_class_config_controller.go │ │ ├── gateway_class_config_controller_test.go │ │ ├── gateway_class_controller.go │ │ ├── gateway_class_controller_test.go │ │ ├── gateway_controller.go │ │ ├── gateway_controller_test.go │ │ ├── http_route_controller.go │ │ ├── http_route_controller_test.go │ │ ├── tcp_route_controller.go │ │ └── tcp_route_controller_test.go │ ├── gatewayclient │ │ ├── errors.go │ │ ├── gatewayclient.go │ │ ├── gatewayclient_test.go │ │ ├── middleware.go │ │ ├── middleware_test.go │ │ ├── mocks │ │ │ └── gatewayclient.go │ │ └── test_helpers.go │ ├── logger.go │ ├── reconciler │ │ ├── binder.go │ │ ├── binder_test.go │ │ ├── common │ │ │ └── utils.go │ │ ├── converter │ │ │ ├── http.go │ │ │ ├── http_test.go │ │ │ └── tcp.go │ │ ├── deployer.go │ │ ├── errors │ │ │ ├── errors.yaml │ │ │ ├── generator.go │ │ │ ├── zz_generated_errors.go │ │ │ └── zz_generated_errors_test.go │ │ ├── gateway.go │ │ ├── gateway_test.go │ │ ├── gatewayclass.go │ │ ├── gatewayclass_test.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── marshaler.go │ │ ├── marshaler_test.go │ │ ├── mocks │ │ │ ├── manager.go │ │ │ └── tracker.go │ │ ├── route.go │ │ ├── route_test.go │ │ ├── state │ │ │ ├── gateway.go │ │ │ └── route.go │ │ ├── status │ │ │ ├── equality.go │ │ │ ├── generator.go │ │ │ ├── route.go │ │ │ ├── route_test.go │ │ │ ├── statuses.yaml │ │ │ ├── zz_generated_status.go │ │ │ └── zz_generated_status_test.go │ │ ├── statuses.go │ │ ├── statuses_test.go │ │ ├── utils.go │ │ ├── utils_test.go │ │ └── validator │ │ │ ├── gateway.go │ │ │ ├── gateway_test.go │ │ │ ├── route.go │ │ │ ├── route_test.go │ │ │ ├── tls.go │ │ │ └── utils.go │ ├── service │ │ ├── mocks │ │ │ └── resolver.go │ │ ├── resolver.go │ │ ├── resolver_test.go │ │ └── rule.go │ └── utils │ │ ├── cert_file.go │ │ ├── consul.go │ │ ├── consul_test.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── labels.go │ │ ├── labels_test.go │ │ ├── reference.go │ │ ├── secrets.go │ │ └── versions.go ├── metrics │ ├── registry.go │ ├── server.go │ └── server_test.go ├── profiling │ ├── server.go │ └── server_test.go ├── store │ ├── interfaces.go │ ├── memory_backend.go │ ├── memory_backend_test.go │ ├── mocks │ │ └── interfaces.go │ └── store.go ├── testing │ ├── buffer.go │ ├── certificates.go │ ├── conformance │ │ ├── README.md │ │ ├── consul-config.yaml │ │ ├── kustomization.yaml │ │ ├── metallb-config.yaml │ │ └── proxydefaults.yaml │ ├── e2e │ │ ├── consul.go │ │ ├── doc.go │ │ ├── docker.go │ │ ├── environment.go │ │ ├── gateway.go │ │ ├── go.go │ │ ├── kind.go │ │ ├── kubernetes.go │ │ ├── kustomize.go │ │ ├── service.go │ │ └── stack.go │ └── strings.go ├── tools.go ├── vault │ ├── certificates.go │ ├── certificates_test.go │ ├── mocks │ │ └── certificates.go │ ├── secrets.go │ └── secrets_test.go └── version │ ├── version.go │ └── version_test.go ├── main.go ├── main_test.go ├── pkg └── apis │ └── v1alpha1 │ ├── doc.go │ ├── doc_test.go │ ├── types.go │ ├── types_test.go │ └── zz_generated.deepcopy.go └── scripts ├── changelog-check.sh └── e2e_local.sh /.changelog/101.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | changelog: add go-changelog templates and tooling 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/143.txt: -------------------------------------------------------------------------------- 1 | ```release-note:breaking-change 2 | Routes now require a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io%2fv1alpha2.ReferencePolicy) to permit references to services in other namespaces. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/154.txt: -------------------------------------------------------------------------------- 1 | ```release-note:breaking-change 2 | Gateway listener `certificateRefs` to secrets in a different namespace now require a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io%2fv1alpha2.ReferencePolicy) 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/156.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | k8s/controllers: watch for ReferencePolicy changes to reconcile and revalidate affected HTTPRoutes 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/162.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | k8s/controllers: watch for ReferencePolicy changes to reconcile and revalidate affected TCPRoutes 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/167.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | go: build with Go 1.18 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/187.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | k8s/reconciler: gateway addresses have invalid empty string when LoadBalancer services use a hostname for ExternalIP (like EKS) 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/195.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Added a new configuration option called deployment to GatewayClassConfig that allows the user to configure the number of instances that are deployed per gateway. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/197.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Clean up stale routes from gateway listeners when not able or allowed to bind, to prevent serving traffic for a detached route. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/200.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Clean up stale routes from gateway listeners when route no longer references the gateway. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/201.txt: -------------------------------------------------------------------------------- 1 | ```release-note:note 2 | Gateway IP address assignment logic updated to include the case when multiple different pod IPs exist 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/202.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Define anti-affinity rules so that the scheduler will attempt to evenly spread gateway pods across all available nodes 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/207.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | k8s/controllers: watch for ReferencePolicy changes to reconcile and revalidate affected Gateways 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/224.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | gateway-api: update to the [v0.5.0-rc1](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v0.5.0-rc1) release with v1beta1 resource support 3 | ``` 4 | ```release-note:deprecation 5 | gateway-api: ReferencePolicy is deprecated and will be removed in a future release. The functionally identical ReferenceGrant should be used instead. 6 | ``` 7 | -------------------------------------------------------------------------------- /.changelog/225.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix SPIFFE validation for connect certificates that have no URL (e.g., Vault connect certificates) 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/227.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Properly handle re-registration of deployed gateways when an agent no longer has the gateway in its catalog 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/247.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Revalidate HTTPRoutes and TCPRoutes and update status when the Kubernetes Service(s) that they reference are modified 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/278.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Sync in-memory store to Consul at a regular interval in the background 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/279.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | gateway-api: update to the [v0.5.0-rc2](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v0.5.0-rc2) release with v1beta1 resource support 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/282.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Support prefix replacement URLRewrite filter ([docs](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPPathModifier)) 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/283.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | gateway-api: update to the [v0.5.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v0.5.0) release with v1beta1 resource support 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/290.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Assign InvalidKind reason to ResolvedRefs condition on routes where the backend reference is an unknown or unsupported kind 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/291.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Assign BackendNotFound reason to ResolvedRefs condition on routes where the backend reference is a supported kind but does not exist 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/308.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix intentions syncing for multiple gateways bound to a single route. 3 | ``` -------------------------------------------------------------------------------- /.changelog/368.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix failing root certificate watch for controller when deployed in secondary federated datacenter. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/371.txt: -------------------------------------------------------------------------------- 1 | ```release-note:feature 2 | Switch deployed gateways to use TTL-based health checks to better support running with Consul servers that are not on the same network as a gateway 3 | ``` -------------------------------------------------------------------------------- /.changelog/377.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Delete gateway ACL tokens on shutdown so they are not orphaned after being provisioned at startup. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/384.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | api: add OpenAPI schema and stubs for bootstrap token CRUD 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/385.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | makefile: switch back to upstream go-changelog repo 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/391.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Support distroless Envoy images (with continued support for distroful images) 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/397.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | When a gateway is created in a namespace that doesn't exist in Consul and namespace mirroring is enabled, create the namespace in Consul. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/405.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Add optional configuration for maximum upstream connections to GatewayClassConfig CRD. If unset, behavior is unchanged and Envoy's default will be used. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/406.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Allow MeshService CRD to reference a Consul service imported from a peer by specifying the peer's name 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/412.txt: -------------------------------------------------------------------------------- 1 | ```release-note:note 2 | RefNotPermitted error is now returned instead of InvalidCertificateRef in the case where a cross namespace certificate is not allowed by a ReferenceGrant 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/424.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | go: update to Go v1.19 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/426.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Add support for tolerations to Consul API Gateway Controller and GatewayClassConfig. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/433.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Add optional `podSecurityPolicy` to GatewayClassConfig CRD. If set and "managed" ServiceAccounts are being used, a Role and RoleBinding are created to attach the named `PodSecurityPolicy` to the managed ServiceAccount. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/449.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Integrate consul-server-connection-manager to support Agentless consul server discovery 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/450.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Add optional `consul.partition` and `consul.serverName` to GatewayClassConfig CRD. If set these will be used to initialize the partition and server name used in TLS verification for communicating with Consul in a deployment. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/459.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix being able to use system-wide root certificates in deployments. 3 | ``` -------------------------------------------------------------------------------- /.changelog/470.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Add initial set of copyright headers to applicable files 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/474.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | go: build with Go v1.19.4 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/483.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | consul: fix Consul Enterprise gateway sync issue with Kubernetes namespace mirroring disabled and the Consul destination namespace set to "default" 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/505.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix `cross-namespace-policy` not being applied to namespaces created by the controller. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/521.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | Bump the default envoy image for consul 1.15 compatability when the image is not specified in a GatewayClassConfig 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/537.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix envoy deployments not properly identifying themselves when deployed to non-default partitions. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/547.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | add support for parsing SPIFFE paths for non-default partitions in Consul Enterprise 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/570.txt: -------------------------------------------------------------------------------- 1 | ```release-note:enhancement 2 | go: build with Go v1.19.9 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/595.txt: -------------------------------------------------------------------------------- 1 | ```release-note:bug 2 | Fix a nil pointer panic when Consul returns a literal "null" when checking catalog nodes. 3 | ``` 4 | -------------------------------------------------------------------------------- /.changelog/changelog.tmpl: -------------------------------------------------------------------------------- 1 | {{- if index .NotesByType "breaking-change" -}} 2 | BREAKING CHANGES: 3 | 4 | {{range index .NotesByType "breaking-change" -}} 5 | * {{ template "note" .}} 6 | {{ end -}} 7 | {{- end -}} 8 | 9 | {{- if .NotesByType.security }} 10 | SECURITY: 11 | 12 | {{range .NotesByType.security -}} 13 | * {{ template "note" .}} 14 | {{ end -}} 15 | {{- end -}} 16 | 17 | {{- if .NotesByType.deprecation -}} 18 | DEPRECATIONS: 19 | 20 | {{range .NotesByType.deprecation -}} 21 | * {{ template "note" .}} 22 | {{ end -}} 23 | {{- end -}} 24 | 25 | {{- if .NotesByType.feature }} 26 | FEATURES: 27 | 28 | {{range .NotesByType.feature -}} 29 | * {{ template "note" . }} 30 | {{ end -}} 31 | {{- end -}} 32 | 33 | {{- $improvements := combineTypes .NotesByType.improvement .NotesByType.enhancement -}} 34 | {{- if $improvements }} 35 | IMPROVEMENTS: 36 | 37 | {{range $improvements | sort -}} 38 | * {{ template "note" . }} 39 | {{ end -}} 40 | {{- end -}} 41 | 42 | {{- if .NotesByType.bug }} 43 | BUG FIXES: 44 | 45 | {{range .NotesByType.bug -}} 46 | * {{ template "note" . }} 47 | {{ end -}} 48 | {{- end -}} 49 | 50 | {{- if .NotesByType.note }} 51 | NOTES: 52 | 53 | {{range .NotesByType.note -}} 54 | * {{ template "note" .}} 55 | {{ end -}} 56 | {{- end -}} 57 | -------------------------------------------------------------------------------- /.changelog/release-note.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "note" -}} 2 | {{.Body}}{{if not (stringHasPrefix .Issue "_")}} [[GH-{{- .Issue -}}](https://github.com/hashicorp/consul-api-gateway/issues/{{- .Issue -}})]{{end}} 3 | {{- end -}} 4 | -------------------------------------------------------------------------------- /.changelog/types.txt: -------------------------------------------------------------------------------- 1 | breaking-change 2 | security 3 | deprecation 4 | feature 5 | enhancement 6 | bug 7 | note 8 | -------------------------------------------------------------------------------- /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2021 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright or license headers . 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | # Default: [] 11 | header_ignore = [ 12 | "config/rbac/role.yaml", 13 | "config/crd/bases/*.yaml", 14 | "**/testdata/**.golden.yaml" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/*zz_generated_*.go linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: You're experiencing an issue with the Consul API Gateway that is different than the documented behavior. 4 | labels: bug 5 | 6 | --- 7 | 8 | 15 | 16 | ### Overview of the Issue 17 | 18 | 19 | 20 | ### Reproduction Steps 21 | 22 | 37 | 38 | ### Logs 39 | 40 | 54 | 55 | ### Expected behavior 56 | 57 | 58 | 59 | ### Environment details 60 | 61 | 76 | 77 | 78 | ### Additional Context 79 | 80 | 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | blank_issues_enabled: false 5 | contact_links: 6 | - name: Consul Discuss Forum 7 | url: https://discuss.hashicorp.com/c/consul 8 | about: Please check out our discussion forum. Ask a question or see if yours has already been answered there. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: If you have something you think the Consul API Gateway could improve or add support for. 4 | labels: enhancement 5 | 6 | --- 7 | 8 | 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | 12 | 13 | 14 | ### Feature Description 15 | 16 | 17 | 18 | ### Use Case(s) 19 | 20 | 21 | 22 | ### Contributions 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | Issues on GitHub are intended to be related to bugs or feature requests. Questions should be directed to the [HashiCorp Discuss Forum](https://discuss.hashicorp.com/c/consul/29). 2 | -------------------------------------------------------------------------------- /.github/actions/goenv/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: "Setup Go Environment" 5 | description: "Setup a go environment with caching" 6 | inputs: 7 | go-version: 8 | description: "Go version to install" 9 | required: true 10 | gotestsum-version: 11 | description: "gotestsum version to install" 12 | required: false 13 | default: 1.7.0 14 | outputs: 15 | go-build-cache: 16 | description: "go build cache path" 17 | value: ${{ steps.go-cache-paths.outputs.go-build-cache }} 18 | go-mod-cache: 19 | description: "go mod cache path" 20 | value: ${{ steps.go-cache-paths.outputs.go-mod-cache }} 21 | runs: 22 | using: composite 23 | steps: 24 | - name: Setup Go 25 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 26 | with: 27 | go-version: ${{ inputs.go-version }} 28 | 29 | - name: Setup gotestsum 30 | shell: bash 31 | run: | 32 | url=https://github.com/gotestyourself/gotestsum/releases/download 33 | curl -sSL "${url}/v${{ inputs.gotestsum-version }}/gotestsum_${{ inputs.gotestsum-version }}_linux_amd64.tar.gz" | \ 34 | tar -xz --overwrite -C /usr/local/bin gotestsum 35 | gotestsum --version 36 | 37 | - id: go-cache-paths 38 | name: Setup Go Cache paths 39 | shell: bash 40 | run: | 41 | echo "::set-output name=go-build-cache::$(go env GOCACHE)" 42 | echo "::set-output name=go-mod-cache::$(go env GOMODCACHE)" 43 | 44 | - name: Go Build Cache 45 | uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 46 | with: 47 | path: ${{ steps.go-cache-paths.outputs.go-build-cache }} 48 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 49 | 50 | - name: Go Mod Cache 51 | uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 52 | with: 53 | path: ${{ steps.go-cache-paths.outputs.go-mod-cache }} 54 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} 55 | -------------------------------------------------------------------------------- /.github/actions/setup-eks/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: "Setup EKS" 5 | description: "Installs eksctl, configures AWS credentials in the workflow's environment and sets up an EKS cluster." 6 | inputs: 7 | access_key_id: 8 | description: "The AWS access key ID to use for creating the EKS cluster" 9 | required: true 10 | account_id: 11 | description: "The ID of the account to create the EKS cluster in" 12 | required: true 13 | cluster_name: 14 | description: "The name to assign to the EKS cluster" 15 | required: false 16 | default: "consul-api-gateway-test" 17 | region: 18 | description: "The AWS region to create the cluster in" 19 | required: false 20 | default: us-west-2 21 | secret_access_key: 22 | description: "The AWS secret access key to use for creating the EKS cluster" 23 | required: true 24 | 25 | runs: 26 | using: composite 27 | steps: 28 | - name: Install Terraform 29 | uses: hashicorp/setup-terraform@v2 30 | 31 | - name: Configure AWS credentials 32 | uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef # v2.0.0 33 | with: 34 | aws-region: ${{ inputs.region }} 35 | role-to-assume: "arn:aws:iam::${{ inputs.account_id }}:role/cicd-consul-apigateway-acceptance-tests" 36 | aws-access-key-id: ${{ inputs.access_key_id }} 37 | aws-secret-access-key: ${{ inputs.secret_access_key }} 38 | role-duration-seconds: 7200 39 | 40 | - name: Create EKS cluster 41 | shell: bash 42 | env: 43 | AWS_EC2_METADATA_DISABLED: true 44 | TF_VAR_cluster_name: ${{ inputs.cluster_name }} 45 | TF_VAR_region: ${{ inputs.region }} 46 | run: | 47 | terraform -chdir=$GITHUB_ACTION_PATH init 48 | terraform -chdir=$GITHUB_ACTION_PATH apply --auto-approve 49 | 50 | - name: Update kubeconfig 51 | shell: bash 52 | run: aws eks --region ${{ inputs.region }} update-kubeconfig --name ${{ inputs.cluster_name }} 53 | -------------------------------------------------------------------------------- /.github/actions/setup-eks/cluster.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | variable "cluster_name" { 5 | type = string 6 | nullable = false 7 | } 8 | 9 | variable "region" { 10 | type = string 11 | nullable = false 12 | } 13 | 14 | provider "kubernetes" { 15 | host = data.aws_eks_cluster.cluster.endpoint 16 | token = data.aws_eks_cluster_auth.cluster.token 17 | cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) 18 | } 19 | 20 | provider "aws" { 21 | region = var.region 22 | } 23 | 24 | data "aws_availability_zones" "available" {} 25 | 26 | data "aws_eks_cluster" "cluster" { 27 | name = module.eks.cluster_id 28 | } 29 | 30 | data "aws_eks_cluster_auth" "cluster" { 31 | name = module.eks.cluster_id 32 | } 33 | 34 | module "vpc" { 35 | source = "terraform-aws-modules/vpc/aws" 36 | version = "3.11.0" 37 | 38 | name = "${var.cluster_name}-vpc" 39 | cidr = "10.0.0.0/16" 40 | azs = data.aws_availability_zones.available.names 41 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 42 | public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] 43 | enable_nat_gateway = true 44 | single_nat_gateway = true 45 | enable_dns_hostnames = true 46 | 47 | tags = { 48 | "kubernetes.io/cluster/${var.cluster_name}" = "shared" 49 | } 50 | 51 | public_subnet_tags = { 52 | "kubernetes.io/cluster/${var.cluster_name}" = "shared" 53 | "kubernetes.io/role/elb" = "1" 54 | } 55 | 56 | private_subnet_tags = { 57 | "kubernetes.io/cluster/${var.cluster_name}" = "shared" 58 | "kubernetes.io/role/internal-elb" = "1" 59 | } 60 | } 61 | 62 | module "eks" { 63 | source = "terraform-aws-modules/eks/aws" 64 | version = "17.24.0" 65 | 66 | cluster_name = var.cluster_name 67 | cluster_version = "1.22" 68 | subnets = module.vpc.private_subnets 69 | 70 | vpc_id = module.vpc.vpc_id 71 | 72 | node_groups = { 73 | nodes = { 74 | name = "${var.cluster_name}-nodegroup" 75 | desired_capacity = 3 76 | max_capacity = 3 77 | min_capacity = 3 78 | 79 | instance_type = "m5.large" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.github/actions/setup-kind/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: "Setup Kind" 5 | description: "Setup a Kind cluster with MetalLB for ingress" 6 | inputs: 7 | cluster-name: 8 | description: "The name to assign to the Kind cluster" 9 | required: false 10 | default: "consul-api-gateway-test" 11 | load-docker-image: 12 | description: "A Docker image to load into Kind cluster, if any" 13 | required: false 14 | default: "" 15 | metallb-config-path: 16 | description: "The path to a config file for MetalLB" 17 | required: true 18 | runs: 19 | using: composite 20 | steps: 21 | - name: Create Kind cluster 22 | uses: helm/kind-action@d8ccf8fb623ce1bb360ae2f45f323d9d5c5e9f00 # v1.5.0 23 | with: 24 | cluster_name: ${{ inputs.cluster-name }} 25 | kubectl_version: "v1.22.0" 26 | node_image: "kindest/node:v1.24.6@sha256:97e8d00bc37a7598a0b32d1fabd155a96355c49fa0d4d4790aab0f161bf31be1" 27 | 28 | - name: Install MetalLB 29 | shell: bash 30 | run: | 31 | kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/namespace.yaml 32 | kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.12.1/manifests/metallb.yaml 33 | kubectl apply -f ${{ inputs.metallb-config-path }} 34 | kubectl wait --for=condition=Ready --timeout=60s --namespace=metallb-system pods --all 35 | 36 | - name: Load Docker image 37 | if: inputs.load-docker-image != '' 38 | shell: bash 39 | run: kind load docker-image ${{ inputs.load-docker-image }} --name ${{ inputs.cluster-name }} 40 | -------------------------------------------------------------------------------- /.github/actions/teardown-eks/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: "Tear down EKS" 5 | description: "Tears down an EKS cluster. Requires eksctl and credentials in the workflow's environment from the setup-eks action." 6 | inputs: 7 | cluster_name: 8 | description: "The name assigned to the EKS cluster" 9 | required: true 10 | region: 11 | description: "The AWS region that the cluster was created in" 12 | required: false 13 | default: us-west-2 14 | runs: 15 | using: composite 16 | steps: 17 | - name: Delete EKS cluster 18 | shell: bash 19 | env: 20 | AWS_EC2_METADATA_DISABLED: true 21 | TF_VAR_cluster_name: ${{ inputs.cluster_name }} 22 | TF_VAR_region: ${{ inputs.region }} 23 | run: | 24 | terraform -chdir=$GITHUB_ACTION_PATH/../setup-eks/ destroy --auto-approve 25 | 26 | #if unsuccessful, run destroy again to account for race condition 27 | if [[$? -eq 1]]; then 28 | terraform -chdir=$GITHUB_ACTION_PATH/../setup-eks/ destroy --auto-approve 29 | fi 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: gomod 7 | open-pull-requests-limit: 5 8 | directory: "/" 9 | labels: 10 | - "dependencies" 11 | - "pr/no-changelog" 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Changes proposed in this PR: 2 | 3 | ### How I've tested this PR: 4 | 5 | ### How I expect reviewers to test this PR: 6 | 7 | ### Checklist: 8 | - [ ] Tests added 9 | - [ ] CHANGELOG entry added 10 | > Run `make changelog-entry` for guidance in authoring a changelog entry, and 11 | > commit the resulting file, which should have a name matching your PR number. 12 | > Entries should use imperative present tense (e.g. Add support for...) 13 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Backport Assistant Runner 3 | 4 | on: 5 | pull_request: 6 | types: 7 | - closed 8 | - labeled 9 | 10 | jobs: 11 | backport: 12 | if: github.event.pull_request.merged 13 | runs-on: ubuntu-latest 14 | container: hashicorpdev/backport-assistant:0.2.5 15 | steps: 16 | - name: Run Backport Assistant 17 | run: backport-assistant backport 18 | env: 19 | BACKPORT_LABEL_REGEXP: "backport/(?P\\d+\\.\\d+\\.x)" 20 | BACKPORT_TARGET_TEMPLATE: "release/{{.target}}" 21 | GITHUB_TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks that there is either a 'pr/no-changelog' label applied to a PR 2 | # or there is a .changelog/.txt file containing a changelog entry with 3 | # one or more valid changelog notes 4 | name: changelog 5 | on: 6 | pull_request: 7 | types: [opened, synchronize, labeled] 8 | # Runs on PRs to main and all release branches 9 | branches: 10 | - main 11 | - release/* 12 | 13 | jobs: 14 | validate: 15 | # If there a `pr/no-changelog` label we ignore this check 16 | if: "!contains(github.event.pull_request.labels.*.name, 'pr/no-changelog')" 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 21 | with: 22 | ref: ${{ github.event.pull_request.head.sha }} 23 | fetch-depth: 0 # by default the checkout action doesn't checkout all branches 24 | - uses: ./.github/actions/goenv 25 | with: 26 | go-version: '1.19' 27 | - name: Check for changelog entry in diff 28 | run: | 29 | pull_request_base_main=$(expr "${{ github.event.pull_request.base.ref }}" = "main") 30 | 31 | # For PRs against the main branch, the changelog file name should match 32 | # the PR number 33 | if [ pull_request_base_main ]; then 34 | enforce_matching=1 35 | changelog_file_path=".changelog/${{ github.event.pull_request.number }}.txt" 36 | else 37 | changelog_file_path=".changelog/*(_)+([[:digit:]]).txt" 38 | fi 39 | 40 | # Fail status check if non-zero exit code is returned 41 | ./scripts/changelog-check.sh ${changelog_file_path} ${enforce_matching} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Procfile 2 | refresh.yml 3 | .DS_Store 4 | .overmind.sock 5 | /token 6 | /consul-api-gateway 7 | cover.out 8 | demo-deployment 9 | bin 10 | pkg/bin 11 | go-changelog/ 12 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - gofmt 8 | - govet 9 | - unconvert 10 | - staticcheck 11 | - ineffassign 12 | - unparam 13 | - unused 14 | 15 | issues: 16 | # Disable the default exclude list so that all excludes are explicitly 17 | # defined in this file. 18 | exclude-use-default: false 19 | 20 | exclude-rules: 21 | # maybe some stuff here 22 | 23 | linters-settings: 24 | gofmt: 25 | simplify: true 26 | 27 | run: 28 | concurrency: 2 29 | timeout: 10m 30 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | url_docker_registry_dockerhub = "https://hub.docker.com/r/hashicorp/consul-api-gateway" 5 | url_license = "https://github.com/hashicorp/consul-api-gateway/blob/main/LICENSE" 6 | url_project_website = "https://developer.hashicorp.com/consul/docs/api-gateway" 7 | url_release_notes = "https://developer.hashicorp.com/consul/docs/release-notes" 8 | url_source_repository = "https://github.com/hashicorp/consul-api-gateway" 9 | -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | container { 5 | dependencies = true 6 | alpine_secdb = true 7 | secrets = true 8 | } 9 | 10 | binary { 11 | secrets = true 12 | go_modules = true 13 | osv = true 14 | oss_index = true 15 | nvd = false 16 | } 17 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @hashicorp/consul-api-gateway 2 | 3 | # release configuration 4 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM golang:1.19.9-alpine as go-discover 2 | RUN CGO_ENABLED=0 go install github.com/hashicorp/go-discover/cmd/discover@49f60c093101c9c5f6b04d5b1c80164251a761a6 3 | 4 | FROM alpine:latest 5 | 6 | COPY --from=go-discover /go/bin/discover /bin/ 7 | COPY ./consul-api-gateway /bin/consul-api-gateway 8 | ENTRYPOINT ["/bin/consul-api-gateway"] 9 | CMD ["version"] 10 | -------------------------------------------------------------------------------- /config/base/gateway-class-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 6 | kind: GatewayClassConfig 7 | metadata: 8 | name: default-consul-gateway-class-config 9 | spec: 10 | serviceType: LoadBalancer 11 | consul: 12 | scheme: https 13 | ports: 14 | http: 8501 15 | grpc: 8502 16 | -------------------------------------------------------------------------------- /config/base/gateway-class.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: gateway.networking.k8s.io/v1alpha2 6 | kind: GatewayClass 7 | metadata: 8 | name: default-consul-gateway-class 9 | spec: 10 | controllerName: "hashicorp.com/consul-api-gateway-controller" 11 | parametersRef: 12 | group: api-gateway.consul.hashicorp.com 13 | kind: GatewayClassConfig 14 | name: default-consul-gateway-class-config -------------------------------------------------------------------------------- /config/crd/bases/api-gateway.consul.hashicorp.com_meshservices.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.11.4 7 | name: meshservices.api-gateway.consul.hashicorp.com 8 | spec: 9 | group: api-gateway.consul.hashicorp.com 10 | names: 11 | kind: MeshService 12 | listKind: MeshServiceList 13 | plural: meshservices 14 | singular: meshservice 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: MeshService holds a reference to an externally managed Consul 21 | Service Mesh service. 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: Spec defines the desired state of MeshService. 37 | properties: 38 | name: 39 | description: Name holds the service name for a Consul service. 40 | type: string 41 | peer: 42 | description: Peer optionally specifies the name of the peer exporting 43 | the Consul service. If not specified, the Consul service is assumed 44 | to be in the local datacenter. 45 | type: string 46 | type: object 47 | type: object 48 | served: true 49 | storage: true 50 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | resources: 8 | - github.com/kubernetes-sigs/gateway-api/config/crd/experimental?ref=v0.5.0 9 | - bases/api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml 10 | - bases/api-gateway.consul.hashicorp.com_meshservices.yaml 11 | -------------------------------------------------------------------------------- /config/deployment/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: ClusterRoleBinding 7 | metadata: 8 | name: consul-api-gateway-controller-binding 9 | roleRef: 10 | kind: ClusterRole 11 | name: consul-api-gateway-controller 12 | apiGroup: rbac.authorization.k8s.io 13 | subjects: 14 | - kind: ServiceAccount 15 | name: consul-api-gateway-controller 16 | namespace: default -------------------------------------------------------------------------------- /config/deployment/deployment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: apps/v1 6 | kind: Deployment 7 | metadata: 8 | labels: 9 | app: consul-api-gateway-controller 10 | name: consul-api-gateway-controller 11 | namespace: default 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: consul-api-gateway-controller 17 | template: 18 | metadata: 19 | labels: 20 | app: consul-api-gateway-controller 21 | annotations: 22 | 'consul.hashicorp.com/connect-inject': 'false' 23 | spec: 24 | serviceAccountName: consul-api-gateway-controller 25 | containers: 26 | - image: hashicorp/consul-api-gateway:0.5.0 27 | command: ["consul-api-gateway", "server", "-consul-address", "$(HOST_IP):8501", "-ca-file", "/ca/tls.crt", "-sds-server-host", "$(IP)", "-k8s-namespace", "$(CONSUL_K8S_NAMESPACE)", "-log-level", "$(LOG_LEVEL)"] 28 | name: consul-api-gateway-controller 29 | ports: 30 | - containerPort: 9090 31 | volumeMounts: 32 | - mountPath: /ca 33 | name: ca 34 | readOnly: true 35 | env: 36 | - name: LOG_LEVEL 37 | value: info 38 | - name: CONSUL_K8S_NAMESPACE 39 | value: default 40 | - name: IP 41 | valueFrom: 42 | fieldRef: 43 | fieldPath: status.podIP 44 | - name: HOST_IP 45 | valueFrom: 46 | fieldRef: 47 | fieldPath: status.hostIP 48 | volumes: 49 | - name: ca 50 | secret: 51 | secretName: consul-ca-cert 52 | -------------------------------------------------------------------------------- /config/deployment/service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: v1 6 | kind: Service 7 | metadata: 8 | labels: 9 | app: consul-api-gateway-controller 10 | name: consul-api-gateway-controller 11 | namespace: default 12 | spec: 13 | ports: 14 | - port: 9090 15 | name: sds 16 | protocol: TCP 17 | targetPort: 9090 18 | selector: 19 | app: consul-api-gateway-controller -------------------------------------------------------------------------------- /config/deployment/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: consul-api-gateway-controller 9 | namespace: default -------------------------------------------------------------------------------- /config/example/certificates.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: v1 6 | kind: Secret 7 | metadata: 8 | name: cloudflare-api-token-secret 9 | type: Opaque 10 | stringData: 11 | api-token: CLOUDFLARE_API_TOKEN 12 | --- 13 | apiVersion: cert-manager.io/v1 14 | kind: Issuer 15 | metadata: 16 | name: prod-issuer 17 | spec: 18 | acme: 19 | server: https://acme-v02.api.letsencrypt.org/directory 20 | privateKeySecretRef: 21 | name: account-key-prod 22 | email: CLOUDFLARE_EMAIL 23 | solvers: 24 | - dns01: 25 | cloudflare: 26 | email: CLOUDFLARE_EMAIL 27 | apiTokenSecretRef: 28 | name: cloudflare-api-token-secret 29 | key: api-token 30 | --- 31 | apiVersion: cert-manager.io/v1 32 | kind: Certificate 33 | metadata: 34 | name: gateway 35 | spec: 36 | secretName: gateway-production-certificate 37 | issuerRef: 38 | name: prod-issuer 39 | dnsNames: 40 | - DNS_HOSTNAME -------------------------------------------------------------------------------- /config/example/external-dns.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: external-dns 9 | namespace: default 10 | --- 11 | apiVersion: rbac.authorization.k8s.io/v1 12 | kind: ClusterRole 13 | metadata: 14 | name: external-dns 15 | rules: 16 | - apiGroups: [""] 17 | resources: ["services","endpoints","pods"] 18 | verbs: ["get","watch","list"] 19 | - apiGroups: [""] 20 | resources: ["nodes"] 21 | verbs: ["list", "watch"] 22 | - apiGroups: ["externaldns.k8s.io"] 23 | resources: ["dnsendpoints"] 24 | verbs: ["get","watch","list"] 25 | - apiGroups: ["externaldns.k8s.io"] 26 | resources: ["dnsendpoints/status"] 27 | verbs: ["*"] 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: ClusterRoleBinding 31 | metadata: 32 | name: external-dns-viewer 33 | namespace: default 34 | roleRef: 35 | apiGroup: rbac.authorization.k8s.io 36 | kind: ClusterRole 37 | name: external-dns 38 | subjects: 39 | - kind: ServiceAccount 40 | name: external-dns 41 | namespace: default 42 | --- 43 | apiVersion: apps/v1 44 | kind: Deployment 45 | metadata: 46 | name: external-dns 47 | spec: 48 | strategy: 49 | type: Recreate 50 | selector: 51 | matchLabels: 52 | app: external-dns 53 | template: 54 | metadata: 55 | labels: 56 | app: external-dns 57 | spec: 58 | serviceAccountName: external-dns 59 | containers: 60 | - name: external-dns 61 | image: k8s.gcr.io/external-dns/external-dns:v0.7.6 62 | args: 63 | - --source=service 64 | - --provider=cloudflare 65 | env: 66 | - name: CF_API_TOKEN 67 | value: CLOUDFLARE_API_TOKEN 68 | - name: CF_API_EMAIL 69 | value: CLOUDFLARE_EMAIL -------------------------------------------------------------------------------- /config/example/gateway.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: gateway.networking.k8s.io/v1alpha2 6 | kind: Gateway 7 | metadata: 8 | name: example-gateway 9 | annotations: 10 | "external-dns.alpha.kubernetes.io/hostname": DNS_HOSTNAME 11 | spec: 12 | gatewayClassName: default-consul-gateway-class 13 | listeners: 14 | - protocol: HTTPS 15 | hostname: DNS_HOSTNAME 16 | port: 443 17 | name: https 18 | allowedRoutes: 19 | namespaces: 20 | from: Same 21 | tls: 22 | certificateRefs: 23 | - name: gateway-production-certificate -------------------------------------------------------------------------------- /config/example/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | resources: 8 | - certificates.yaml 9 | - external-dns.yaml 10 | - gateway.yaml 11 | - service.yaml 12 | - route.yaml -------------------------------------------------------------------------------- /config/example/route.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: gateway.networking.k8s.io/v1alpha2 6 | kind: HTTPRoute 7 | metadata: 8 | name: example-route 9 | spec: 10 | parentRefs: 11 | - name: example-gateway 12 | rules: 13 | - backendRefs: 14 | - kind: Service 15 | name: echo 16 | port: 8080 -------------------------------------------------------------------------------- /config/example/service.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | apiVersion: consul.hashicorp.com/v1alpha1 6 | kind: ServiceDefaults 7 | metadata: 8 | name: echo 9 | spec: 10 | protocol: http 11 | --- 12 | apiVersion: v1 13 | kind: Service 14 | metadata: 15 | labels: 16 | app: echo 17 | name: echo 18 | spec: 19 | ports: 20 | - port: 8080 21 | name: high 22 | protocol: TCP 23 | targetPort: 8080 24 | selector: 25 | app: echo 26 | --- 27 | apiVersion: apps/v1 28 | kind: Deployment 29 | metadata: 30 | labels: 31 | app: echo 32 | name: echo 33 | spec: 34 | replicas: 1 35 | selector: 36 | matchLabels: 37 | app: echo 38 | template: 39 | metadata: 40 | labels: 41 | app: echo 42 | annotations: 43 | 'consul.hashicorp.com/connect-inject': 'true' 44 | spec: 45 | containers: 46 | - image: gcr.io/kubernetes-e2e-test-images/echoserver:2.2 47 | name: echo 48 | ports: 49 | - containerPort: 8080 50 | env: 51 | - name: NODE_NAME 52 | valueFrom: 53 | fieldRef: 54 | fieldPath: spec.nodeName 55 | - name: POD_NAME 56 | valueFrom: 57 | fieldRef: 58 | fieldPath: metadata.name 59 | - name: POD_NAMESPACE 60 | valueFrom: 61 | fieldRef: 62 | fieldPath: metadata.namespace 63 | - name: POD_IP 64 | valueFrom: 65 | fieldRef: 66 | fieldPath: status.podIP -------------------------------------------------------------------------------- /config/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | 7 | resources: 8 | - rbac/role.yaml 9 | - deployment/serviceaccount.yaml 10 | - deployment/clusterrolebinding.yaml 11 | - deployment/deployment.yaml 12 | - deployment/service.yaml 13 | - base/gateway-class-config.yaml 14 | - base/gateway-class.yaml -------------------------------------------------------------------------------- /dev/config/consul/acl-policy.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | node_prefix "" { policy = "write" } 5 | service_prefix "" { policy = "write" } 6 | agent_prefix "" { policy = "write" } 7 | event_prefix "" { policy = "write" } 8 | query_prefix "" { policy = "write" } 9 | session_prefix "" { policy = "write" } 10 | operator = "write" 11 | acl = "write" 12 | keyring = "write" 13 | -------------------------------------------------------------------------------- /dev/config/helm/consul.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | global: 5 | name: consul 6 | tls: 7 | enabled: true 8 | serverAdditionalDNSSANs: 9 | - host.docker.internal 10 | - localhost 11 | - consul-server.default.svc.cluster.local 12 | connectInject: 13 | enabled: true 14 | controller: 15 | enabled: true 16 | server: 17 | replicas: 1 18 | extraConfig: | 19 | { 20 | "log_level": "trace", 21 | "acl": { 22 | "enabled": true, 23 | "default_policy": "allow", 24 | "enable_token_persistence": true 25 | }, 26 | "connect": { 27 | "enabled": true 28 | } 29 | } 30 | ui: 31 | enabled: true 32 | ingress: 33 | enabled: true 34 | hosts: 35 | - host: "host.docker.internal" 36 | paths: 37 | - "/" 38 | - host: "localhost" 39 | paths: 40 | - "/" 41 | annotations: | 42 | "kubernetes.io/ingress.class": "nginx" 43 | "nginx.ingress.kubernetes.io/ssl-passthrough": "true" 44 | -------------------------------------------------------------------------------- /dev/config/k8s/consul-api-gateway.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | useHostPorts: true 10 | logLevel: trace 11 | image: 12 | consulAPIGateway: "consul-api-gateway:1" 13 | consul: 14 | scheme: https 15 | ports: 16 | http: 8501 17 | grpc: 8502 18 | --- 19 | apiVersion: gateway.networking.k8s.io/v1alpha2 20 | kind: GatewayClass 21 | metadata: 22 | name: test-gateway-class 23 | spec: 24 | controllerName: "hashicorp.com/consul-api-gateway-controller" 25 | parametersRef: 26 | group: api-gateway.consul.hashicorp.com 27 | kind: GatewayClassConfig 28 | name: test-gateway-class-config 29 | --- 30 | apiVersion: gateway.networking.k8s.io/v1alpha2 31 | kind: Gateway 32 | metadata: 33 | name: test-gateway 34 | spec: 35 | gatewayClassName: test-gateway-class 36 | listeners: 37 | - protocol: HTTPS 38 | hostname: localhost 39 | port: 8443 40 | name: https 41 | allowedRoutes: 42 | namespaces: 43 | from: Same 44 | tls: 45 | certificateRefs: 46 | - name: consul-server-cert 47 | --- 48 | apiVersion: consul.hashicorp.com/v1alpha1 49 | kind: ServiceDefaults 50 | metadata: 51 | name: echo 52 | spec: 53 | protocol: http 54 | --- 55 | apiVersion: v1 56 | kind: Service 57 | metadata: 58 | labels: 59 | app: echo 60 | name: echo 61 | spec: 62 | ports: 63 | - port: 8080 64 | name: high 65 | protocol: TCP 66 | targetPort: 8080 67 | selector: 68 | app: echo 69 | --- 70 | apiVersion: apps/v1 71 | kind: Deployment 72 | metadata: 73 | labels: 74 | app: echo 75 | name: echo 76 | spec: 77 | replicas: 1 78 | selector: 79 | matchLabels: 80 | app: echo 81 | template: 82 | metadata: 83 | labels: 84 | app: echo 85 | annotations: 86 | 'consul.hashicorp.com/connect-inject': 'true' 87 | spec: 88 | containers: 89 | - image: gcr.io/kubernetes-e2e-test-images/echoserver:2.2 90 | name: echo 91 | ports: 92 | - containerPort: 8080 93 | env: 94 | - name: NODE_NAME 95 | valueFrom: 96 | fieldRef: 97 | fieldPath: spec.nodeName 98 | - name: POD_NAME 99 | valueFrom: 100 | fieldRef: 101 | fieldPath: metadata.name 102 | - name: POD_NAMESPACE 103 | valueFrom: 104 | fieldRef: 105 | fieldPath: metadata.namespace 106 | - name: POD_IP 107 | valueFrom: 108 | fieldRef: 109 | fieldPath: status.podIP 110 | --- 111 | apiVersion: gateway.networking.k8s.io/v1alpha2 112 | kind: HTTPRoute 113 | metadata: 114 | name: test-route 115 | spec: 116 | parentRefs: 117 | - name: test-gateway 118 | rules: 119 | - backendRefs: 120 | - kind: Service 121 | name: echo 122 | port: 8080 123 | -------------------------------------------------------------------------------- /dev/config/k8s/consul-ui/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | resources: 7 | - https://github.com/kubernetes/ingress-nginx/deploy/static/provider/kind 8 | patchesJSON6902: 9 | - target: 10 | group: apps 11 | version: v1 12 | kind: Deployment 13 | name: ingress-nginx-controller 14 | patch: |- 15 | - op: add 16 | path: "/spec/template/spec/containers/0/args/-" 17 | value: "--enable-ssl-passthrough" 18 | -------------------------------------------------------------------------------- /dev/config/k8s/rbac.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: rbac.authorization.k8s.io/v1 5 | kind: ClusterRoleBinding 6 | metadata: 7 | name: consul-api-gateway-tokenreview-binding 8 | namespace: default 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: system:auth-delegator 13 | subjects: 14 | - kind: ServiceAccount 15 | name: consul-api-gateway 16 | namespace: default 17 | --- 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | kind: ClusterRole 20 | metadata: 21 | namespace: default 22 | name: consul-api-gateway-auth 23 | rules: 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - serviceaccounts 28 | verbs: ["get"] 29 | --- 30 | apiVersion: rbac.authorization.k8s.io/v1 31 | kind: ClusterRoleBinding 32 | metadata: 33 | name: consul-api-gateway-auth-binding 34 | namespace: default 35 | roleRef: 36 | apiGroup: rbac.authorization.k8s.io 37 | kind: ClusterRole 38 | name: consul-api-gateway-auth 39 | subjects: 40 | - kind: ServiceAccount 41 | name: consul-api-gateway 42 | namespace: default 43 | --- 44 | apiVersion: rbac.authorization.k8s.io/v1 45 | kind: ClusterRoleBinding 46 | metadata: 47 | name: consul-auth-binding 48 | namespace: default 49 | roleRef: 50 | apiGroup: rbac.authorization.k8s.io 51 | kind: ClusterRole 52 | name: consul-api-gateway-auth 53 | subjects: 54 | - kind: ServiceAccount 55 | name: consul-server 56 | namespace: default 57 | -------------------------------------------------------------------------------- /dev/config/k8s/service-account-secret.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: consul-api-gateway 8 | annotations: 9 | kubernetes.io/service-account.name: consul-api-gateway 10 | type: kubernetes.io/service-account-token 11 | -------------------------------------------------------------------------------- /dev/config/k8s/service-account.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: consul-api-gateway 8 | -------------------------------------------------------------------------------- /dev/config/kind/cluster.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | kind: Cluster 5 | apiVersion: kind.x-k8s.io/v1alpha4 6 | nodes: 7 | - role: control-plane 8 | kubeadmConfigPatches: 9 | - | 10 | kind: InitConfiguration 11 | nodeRegistration: 12 | kubeletExtraArgs: 13 | node-labels: "ingress-ready=true" 14 | extraPortMappings: 15 | - containerPort: 443 16 | hostPort: 443 17 | protocol: TCP 18 | - containerPort: 8501 19 | hostPort: 8501 20 | protocol: TCP 21 | - containerPort: 8502 22 | hostPort: 8502 23 | protocol: TCP 24 | - containerPort: 8443 25 | hostPort: 8443 26 | protocol: TCP 27 | -------------------------------------------------------------------------------- /dev/docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | - [Getting Started](./getting-started.md) 4 | - [Demo Setup with Cert Manager](./example-setup.md) 5 | - [High-level Components](./components.md) 6 | - [Gateway Deployment Interactions](./deployment.md) 7 | -------------------------------------------------------------------------------- /dev/docs/assets/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/consul-api-gateway/1928f364feedbb64f93243a77f609b35c820461f/dev/docs/assets/components.png -------------------------------------------------------------------------------- /dev/docs/assets/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/consul-api-gateway/1928f364feedbb64f93243a77f609b35c820461f/dev/docs/assets/deployment.png -------------------------------------------------------------------------------- /dev/docs/components.md: -------------------------------------------------------------------------------- 1 | # High-level Components 2 | 3 | [Table of Contents](./README.md) 4 | 5 | ![High-level Components](./assets/components.png "High-level Components") -------------------------------------------------------------------------------- /dev/docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment Interactions 2 | 3 | [Table of Contents](./README.md) 4 | 5 | ![Gateway Deployment Interactions](./assets/deployment.png "Gateway Deployment Interactions") 6 | -------------------------------------------------------------------------------- /dev/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | [Table of Contents](./README.md) 4 | 5 | ## Quick Start (Mac) 6 | 7 | This setup assumes using [Homebrew](https://brew.sh/) as a package manager and the [official HashiCorp tap](https://github.com/hashicorp/homebrew-tap). Consul will be installed in a Kubernetes cluster using the Helm chart, but the standalone binary is currently used for bootstrapping ACLs. 8 | 9 | ```bash 10 | brew tap hashicorp/tap 11 | brew cask install docker 12 | brew install go jq kubectl kustomize kind helm hashicorp/tap/consul 13 | ``` 14 | 15 | Ensure Docker for Mac is running (enabling the Kubernetes single-node cluster is not necessary, as `kind` will build its own cluster), clone this repo, navigate to the root directory, then run: 16 | 17 | ```bash 18 | ./dev/run 19 | ``` 20 | 21 | Test out the Gateway controller: 22 | 23 | ```bash 24 | kubectl apply -f dev/config/k8s/consul-api-gateway.yaml 25 | ``` 26 | 27 | Make sure that the echo container is routable: 28 | 29 | ```bash 30 | curl https://localhost:8443 -k 31 | ``` 32 | 33 | You should expect to see output including a hostname and pod information - if the response if "no healthy upstream", the resources may not have finished being created yet. 34 | 35 | Clean up the gateway you just created: 36 | 37 | ```bash 38 | kubectl delete -f dev/config/k8s/consul-api-gateway.yaml 39 | ``` 40 | 41 | ## Deploying a custom Docker image on a development cluster 42 | 43 | - Create a Docker image from your local branch with `make docker` 44 | 45 | - *Optional*: Some k8s setups require loading a custom image into the runtime environment for it to get pulled by worker nodes. For a local kind cluster you can load the image using the subcommand [`kind load docker-image`](https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster) 46 | 47 | - In [`config/deployment/deployment.yaml`](https://github.com/hashicorp/consul-api-gateway/blob/main/config/deployment/deployment.yaml#L23), edit `image` to point to the name of the image you uploaded to the cluster. 48 | 49 | - Apply the version of the CRDs and Consul API Gateway deployment config from your local branch. 50 | ``` 51 | kubectl apply -k config/crd 52 | kubectl apply -k config 53 | ``` 54 | 55 | ## Running tests 56 | 57 | `go test ./...` will run the default set of tests. Note that some of these tests use the [`consul/sdk`](https://github.com/hashicorp/consul/tree/main/sdk) test helper package, which shells out to the Consul binary on your `$PATH`. You'll want to ensure `which consul` and `consul -v` are pointing to the Consul binary you expect - either a sufficiently recent version, or a custom build if your feature work requires upstream changes in Consul core. 58 | 59 | #### End-to-end tests 60 | 61 | The end-to-end test suite uses [kind](https://kind.sigs.k8s.io/) to spin up a local Kubernetes cluster, deploy the Consul API Gateway controller, and check that gateways and routes are created, attached and succesfully routable. These tests can be included when running the test suite by passing a tag, `go test ./... -tags e2e` 62 | -------------------------------------------------------------------------------- /dev/version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | version_file=$1 4 | version=$(awk '$1 == "Version" && $2 == "=" { gsub(/"/, "", $3); print $3 }' < "${version_file}") 5 | prerelease=$(awk '$1 == "VersionPrerelease" && $2 == "=" { gsub(/"/, "", $3); print $3 }' < "${version_file}") 6 | 7 | if [ -n "$prerelease" ]; then 8 | echo "${version}-${prerelease}" 9 | else 10 | echo "${version}" 11 | fi -------------------------------------------------------------------------------- /internal/adapters/consul/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/hashicorp/consul-api-gateway/internal/core" 13 | ) 14 | 15 | // TestRouteConsolidator verifies that various combinations of hostnames and rules 16 | // are consolidated into a list with one route per hostname and all rules for hostname. 17 | func TestRouteConsolidator(t *testing.T) { 18 | c := newRouteConsolidator() 19 | 20 | g := core.ResolvedGateway{ 21 | ID: core.GatewayID{}, 22 | Meta: map[string]string{`name`: t.Name()}, 23 | Listeners: []core.ResolvedListener{ 24 | {Name: t.Name()}, 25 | }, 26 | } 27 | 28 | basePathMatch := core.HTTPMatch{Path: core.HTTPPathMatch{Type: core.HTTPPathMatchPrefixType, Value: "/"}} 29 | v1HeaderMatch := core.HTTPMatch{Headers: []core.HTTPHeaderMatch{{Name: "version", Value: "one"}}} 30 | v2PathMatch := core.HTTPMatch{Path: core.HTTPPathMatch{Type: core.HTTPPathMatchPrefixType, Value: "/v2"}} 31 | v2HeaderMatch := core.HTTPMatch{Headers: []core.HTTPHeaderMatch{{Name: "version", Value: "two"}}} 32 | 33 | route1 := core.HTTPRoute{ 34 | Hostnames: []string{`example.com`, `example.net`}, 35 | Rules: []core.HTTPRouteRule{ 36 | { 37 | Matches: []core.HTTPMatch{basePathMatch, v1HeaderMatch}, 38 | }, 39 | }, 40 | } 41 | 42 | route2 := core.HTTPRoute{ 43 | Hostnames: []string{`example.com`}, 44 | Rules: []core.HTTPRouteRule{ 45 | { 46 | Matches: []core.HTTPMatch{v2PathMatch, v2HeaderMatch}, 47 | }, 48 | }, 49 | } 50 | 51 | c.add(route1) 52 | c.add(route2) 53 | routes := c.consolidate(g) 54 | 55 | // We should have 2 routes, each w/ one hostname 56 | require.Len(t, routes, 2) 57 | require.Len(t, routes[0].Hostnames, 1) 58 | require.Len(t, routes[1].Hostnames, 1) 59 | 60 | comRoute, netRoute := routes[0], routes[1] 61 | if comRoute.Hostnames[0] != "example.com" { 62 | netRoute, comRoute = routes[0], routes[1] 63 | } 64 | 65 | // example.net has a subset of example.com's matches 66 | assert.Equal(t, "example.net", netRoute.Hostnames[0]) 67 | require.Len(t, netRoute.Rules, 2) 68 | assert.Equal(t, []core.HTTPMatch{basePathMatch}, comRoute.Rules[1].Matches) 69 | assert.Equal(t, []core.HTTPMatch{v1HeaderMatch}, comRoute.Rules[2].Matches) 70 | 71 | // example.com has a couple of extra matches 72 | assert.Equal(t, "example.com", comRoute.Hostnames[0]) 73 | require.Len(t, comRoute.Rules, 4) 74 | assert.Equal(t, []core.HTTPMatch{v2PathMatch}, comRoute.Rules[0].Matches) 75 | assert.Equal(t, []core.HTTPMatch{basePathMatch}, comRoute.Rules[1].Matches) 76 | assert.Equal(t, []core.HTTPMatch{v1HeaderMatch}, comRoute.Rules[2].Matches) 77 | assert.Equal(t, []core.HTTPMatch{v2HeaderMatch}, comRoute.Rules[3].Matches) 78 | } 79 | -------------------------------------------------------------------------------- /internal/adapters/consul/testdata/multiple-services.golden.json: -------------------------------------------------------------------------------- 1 | { 2 | "Router": { 3 | "Kind": "service-router", 4 | "Name": "multiple-services", 5 | "Namespace": "k8s", 6 | "Routes": [ 7 | { 8 | "Match": { 9 | "HTTP": { 10 | "PathExact": "/prefix" 11 | } 12 | }, 13 | "Destination": { 14 | "Service": "multiple-services-0", 15 | "Namespace": "k8s", 16 | "PrefixRewrite": "/", 17 | "RequestHeaders": { 18 | "Add": { 19 | "x-add": "2", 20 | "x-add-too": "2" 21 | }, 22 | "Set": { 23 | "x-set": "1", 24 | "x-set-too": "1" 25 | }, 26 | "Remove": [ 27 | "x-remove" 28 | ] 29 | } 30 | } 31 | } 32 | ], 33 | "CreateIndex": 0, 34 | "ModifyIndex": 0 35 | }, 36 | "Splitters": [ 37 | { 38 | "Kind": "service-splitter", 39 | "Name": "multiple-services-0", 40 | "Namespace": "k8s", 41 | "Splits": [ 42 | { 43 | "Weight": 50, 44 | "Service": "service", 45 | "Namespace": "namespace", 46 | "RequestHeaders": { 47 | "Add": { 48 | "x-add": "4" 49 | }, 50 | "Set": { 51 | "x-set": "3" 52 | }, 53 | "Remove": [ 54 | "x-remove-a" 55 | ] 56 | } 57 | }, 58 | { 59 | "Weight": 50, 60 | "Service": "another-service", 61 | "Namespace": "namespace", 62 | "RequestHeaders": { 63 | "Add": { 64 | "x-add": "4" 65 | }, 66 | "Remove": [ 67 | "x-remove-b" 68 | ] 69 | } 70 | } 71 | ], 72 | "CreateIndex": 0, 73 | "ModifyIndex": 0 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /internal/adapters/consul/testdata/multiple-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "multiple-services", 3 | "Namespace": "k8s", 4 | "Hostnames": [ 5 | "example.com" 6 | ], 7 | "Rules": [ 8 | { 9 | "Matches": [ 10 | { 11 | "Type": 1, 12 | "Path": { 13 | "Type": "HTTPPathMatchExact", 14 | "Value": "/prefix" 15 | } 16 | } 17 | ], 18 | "Filters": [ 19 | { 20 | "Type": "HTTPHeaderFilter", 21 | "Header": { 22 | "Set": { 23 | "x-set": "1" 24 | }, 25 | "Add": { 26 | "x-add": "2" 27 | }, 28 | "Remove": [ 29 | "x-remove" 30 | ] 31 | } 32 | }, 33 | { 34 | "Type": "HTTPHeaderFilter", 35 | "Header": { 36 | "Set": { 37 | "x-set-too": "1" 38 | }, 39 | "Add": { 40 | "x-add-too": "2" 41 | } 42 | } 43 | }, 44 | { 45 | "Type": "HTTPURLRewriteFilter", 46 | "URLRewrite": { 47 | "Type": "URLRewriteReplacePrefixMatch", 48 | "ReplacePrefixMatch": "/" 49 | } 50 | } 51 | ], 52 | "Services": [ 53 | { 54 | "Service": { 55 | "ConsulNamespace": "namespace", 56 | "Service": "service" 57 | }, 58 | "Weight": 1, 59 | "Filters": [ 60 | { 61 | "Type": "HTTPHeaderFilter", 62 | "Header": { 63 | "Set": { 64 | "x-set": "3" 65 | }, 66 | "Add": { 67 | "x-add": "4" 68 | }, 69 | "Remove": [ 70 | "x-remove-a" 71 | ] 72 | } 73 | } 74 | ] 75 | }, 76 | { 77 | "Service": { 78 | "ConsulNamespace": "namespace", 79 | "Service": "another-service" 80 | }, 81 | "Weight": 1, 82 | "Filters": [ 83 | { 84 | "Type": "HTTPHeaderFilter", 85 | "Header": { 86 | "Add": { 87 | "x-add": "4" 88 | }, 89 | "Remove": [ 90 | "x-remove-b" 91 | ] 92 | } 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /internal/adapters/consul/testdata/single-service.golden.json: -------------------------------------------------------------------------------- 1 | { 2 | "Router": { 3 | "Kind": "service-router", 4 | "Name": "single-service", 5 | "Namespace": "k8s", 6 | "Routes": [ 7 | { 8 | "Match": { 9 | "HTTP": { 10 | "PathExact": "/prefix" 11 | } 12 | }, 13 | "Destination": { 14 | "Service": "service", 15 | "Namespace": "namespace", 16 | "RequestHeaders": { 17 | "Add": { 18 | "x-add": "4" 19 | }, 20 | "Set": { 21 | "x-set": "3" 22 | }, 23 | "Remove": [ 24 | "x-remove", 25 | "x-remove-too" 26 | ] 27 | } 28 | } 29 | } 30 | ], 31 | "CreateIndex": 0, 32 | "ModifyIndex": 0 33 | }, 34 | "Splitters": null 35 | } -------------------------------------------------------------------------------- /internal/adapters/consul/testdata/single-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "single-service", 3 | "Namespace": "k8s", 4 | "Hostnames": [ 5 | "example.com" 6 | ], 7 | "Rules": [ 8 | { 9 | "Matches": [ 10 | { 11 | "Type": 1, 12 | "Path": { 13 | "Type": "HTTPPathMatchExact", 14 | "Value": "/prefix" 15 | } 16 | } 17 | ], 18 | "Filters": [ 19 | { 20 | "Type": "HTTPHeaderFilter", 21 | "Header": { 22 | "Set": { 23 | "x-set": "1" 24 | }, 25 | "Add": { 26 | "x-add": "2" 27 | }, 28 | "Remove": [ 29 | "x-remove" 30 | ] 31 | } 32 | } 33 | ], 34 | "Services": [ 35 | { 36 | "Service": { 37 | "ConsulNamespace": "namespace", 38 | "Service": "service" 39 | }, 40 | "Weight": 1, 41 | "Filters": [ 42 | { 43 | "Type": "HTTPHeaderFilter", 44 | "Header": { 45 | "Set": { 46 | "x-set": "3" 47 | }, 48 | "Add": { 49 | "x-add": "4" 50 | }, 51 | "Remove": [ 52 | "x-remove-too" 53 | ] 54 | } 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /internal/commands/server/command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package server 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/mitchellh/cli" 11 | "github.com/stretchr/testify/require" 12 | 13 | gwTesting "github.com/hashicorp/consul-api-gateway/internal/testing" 14 | ) 15 | 16 | func TestServerHelpSynopsis(t *testing.T) { 17 | t.Parallel() 18 | 19 | ctx := context.Background() 20 | ui := cli.NewMockUi() 21 | var buffer gwTesting.Buffer 22 | cmd := New(ctx, ui, &buffer) 23 | cmd.isTest = true 24 | 25 | require.Equal(t, "Starts the consul-api-gateway control plane server", cmd.Synopsis()) 26 | require.NotEmpty(t, cmd.Help()) 27 | } 28 | 29 | func TestExec(t *testing.T) { 30 | t.Parallel() 31 | 32 | for _, test := range []struct { 33 | name string 34 | args []string 35 | retVal int 36 | output string 37 | }{{ 38 | name: "flag-parse-error", 39 | args: []string{ 40 | "-not-a-flag", 41 | }, 42 | retVal: 1, 43 | output: "flag provided but not defined: -not-a-flag", 44 | }, { 45 | name: "invalid-context", 46 | args: []string{ 47 | "-ca-file", "file", 48 | "-ca-secret-namespace", "namespace", 49 | "-ca-secret", "secret", 50 | "-consul-address", "localhost", 51 | "-k8s-context", "thiscontextdoesnotexist", 52 | }, 53 | retVal: 1, 54 | output: "error getting kubernetes configuration", 55 | }} { 56 | t.Run(test.name, func(t *testing.T) { 57 | ctx := context.Background() 58 | ui := cli.NewMockUi() 59 | var buffer gwTesting.Buffer 60 | cmd := New(ctx, ui, &buffer) 61 | cmd.isTest = true 62 | 63 | require.Equal(t, test.retVal, cmd.Run(test.args)) 64 | require.Contains(t, buffer.String(), test.output) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/commands/server/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package server 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | 14 | "github.com/hashicorp/consul-api-gateway/internal/k8s" 15 | gwTesting "github.com/hashicorp/consul-api-gateway/internal/testing" 16 | ) 17 | 18 | func TestServerK8sInitializationError(t *testing.T) { 19 | t.Parallel() 20 | 21 | var buffer gwTesting.Buffer 22 | logger := hclog.New(&hclog.LoggerOptions{ 23 | Output: &buffer, 24 | }) 25 | require.Equal(t, 1, RunServer(ServerConfig{ 26 | Context: context.Background(), 27 | Logger: logger, 28 | isTest: true, 29 | K8sConfig: k8s.Defaults(), 30 | })) 31 | require.Contains(t, buffer.String(), "error initializing the kubernetes secret fetcher") 32 | } 33 | -------------------------------------------------------------------------------- /internal/commands/version/command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/mitchellh/cli" 10 | ) 11 | 12 | type Command struct { 13 | UI cli.Ui 14 | Version string 15 | } 16 | 17 | func (c *Command) Run(_ []string) int { 18 | c.UI.Output(fmt.Sprintf("consul-api-gateway %s", c.Version)) 19 | return 0 20 | } 21 | 22 | func (c *Command) Synopsis() string { 23 | return "Prints the version" 24 | } 25 | 26 | func (c *Command) Help() string { 27 | return ` 28 | Usage: consul-api-gateway version [options] 29 | ` 30 | } 31 | -------------------------------------------------------------------------------- /internal/commands/version/command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/mitchellh/cli" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestVersion(t *testing.T) { 14 | ui := cli.NewMockUi() 15 | cmd := &Command{ 16 | UI: ui, 17 | Version: "1", 18 | } 19 | require.NotEmpty(t, cmd.Help()) 20 | require.Equal(t, "Prints the version", cmd.Synopsis()) 21 | 22 | require.Equal(t, 0, cmd.Run(nil)) 23 | require.Equal(t, "consul-api-gateway 1\n", ui.OutputWriter.String()) 24 | } 25 | -------------------------------------------------------------------------------- /internal/common/addresses.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "net" 8 | ) 9 | 10 | // AddressTypeForAddress returns whether envoy should 11 | // treat the given address as a static ip or as a DNS name 12 | func AddressTypeForAddress(address string) string { 13 | if net.ParseIP(address) != nil { 14 | return "STATIC" 15 | } 16 | return "STRICT_DNS" 17 | } 18 | -------------------------------------------------------------------------------- /internal/common/addresses_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestAddressTypeForAddress(t *testing.T) { 13 | require.Equal(t, "STATIC", AddressTypeForAddress("127.0.0.1")) 14 | require.Equal(t, "STATIC", AddressTypeForAddress("::")) 15 | require.Equal(t, "STRICT_DNS", AddressTypeForAddress("test.com")) 16 | } 17 | -------------------------------------------------------------------------------- /internal/common/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import "strings" 7 | 8 | type ArrayFlag []string 9 | 10 | func (i *ArrayFlag) String() string { 11 | return strings.Join(*i, ", ") 12 | } 13 | 14 | func (i *ArrayFlag) Set(value string) error { 15 | *i = append(*i, value) 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /internal/common/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "io" 8 | "os" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | ) 12 | 13 | func CreateLogger(output io.Writer, logLevel string, asJSON bool, name string) hclog.Logger { 14 | return hclog.New(&hclog.LoggerOptions{ 15 | Level: hclog.LevelFromString(logLevel), 16 | Output: output, 17 | JSONFormat: asJSON, 18 | IncludeLocation: true, 19 | }).Named(name) 20 | } 21 | 22 | func GetConsulTokenOr(tokenFlag string) string { 23 | if tokenFlag != "" { 24 | return tokenFlag 25 | } 26 | return os.Getenv("CONSUL_HTTP_TOKEN") 27 | } 28 | -------------------------------------------------------------------------------- /internal/common/mapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | type ConsulNamespaceMapper func(namespace string) string 7 | -------------------------------------------------------------------------------- /internal/common/service_names.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "github.com/hashicorp/consul/api" 8 | ) 9 | 10 | type ServiceNameIndex struct { 11 | idx map[api.CompoundServiceName]struct{} 12 | } 13 | 14 | func NewServiceNameIndex() *ServiceNameIndex { 15 | return &ServiceNameIndex{idx: map[api.CompoundServiceName]struct{}{}} 16 | } 17 | 18 | func (i *ServiceNameIndex) Exists(name api.CompoundServiceName) bool { 19 | _, ok := i.idx[name] 20 | return ok 21 | } 22 | 23 | func (i *ServiceNameIndex) Add(names ...api.CompoundServiceName) { 24 | for _, name := range names { 25 | i.idx[name] = struct{}{} 26 | } 27 | } 28 | 29 | func (i *ServiceNameIndex) Remove(names ...api.CompoundServiceName) { 30 | for _, name := range names { 31 | delete(i.idx, name) 32 | } 33 | } 34 | 35 | func (i *ServiceNameIndex) Diff(other *ServiceNameIndex) (added []api.CompoundServiceName, removed []api.CompoundServiceName) { 36 | for name := range other.idx { 37 | if _, ok := i.idx[name]; !ok { 38 | // other has added name 39 | added = append(added, name) 40 | } 41 | } 42 | 43 | for name := range i.idx { 44 | if _, ok := other.idx[name]; !ok { 45 | // other is removing name 46 | removed = append(removed, name) 47 | } 48 | } 49 | 50 | return added, removed 51 | } 52 | 53 | func (i *ServiceNameIndex) All() []api.CompoundServiceName { 54 | var result []api.CompoundServiceName 55 | for name := range i.idx { 56 | result = append(result, name) 57 | } 58 | return result 59 | } 60 | -------------------------------------------------------------------------------- /internal/common/service_names_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/hashicorp/consul/api" 12 | ) 13 | 14 | func Test_ServiceNamesIndex(t *testing.T) { 15 | require := require.New(t) 16 | require.NotNil(NewServiceNameIndex()) 17 | 18 | idx1 := NewServiceNameIndex() 19 | idx2 := NewServiceNameIndex() 20 | 21 | n := func(name, namespace string) api.CompoundServiceName { 22 | return api.CompoundServiceName{Name: name, Namespace: namespace} 23 | } 24 | 25 | idx1.Add(n("name1", ""), n("name2", ""), n("name1", "namespace1")) 26 | require.Len(idx1.idx, 3) 27 | idx1.Add(n("name1", "namespace1"), n("name2", "namespace2")) 28 | require.Len(idx1.idx, 4) 29 | idx1.Remove(n("name1", ""), n("name1", "")) 30 | require.Len(idx1.idx, 3) 31 | require.False(idx1.Exists(n("name1", ""))) 32 | require.True(idx1.Exists(n("name2", ""))) 33 | 34 | idx2.Add(n("name1", ""), n("name1", "namespace1")) 35 | added, removed := idx1.Diff(idx2) 36 | require.Len(added, 1) 37 | require.Contains(added, n("name1", "")) 38 | require.Len(removed, 2) 39 | require.Contains(removed, n("name2", "")) 40 | require.Contains(removed, n("name2", "namespace2")) 41 | } 42 | -------------------------------------------------------------------------------- /internal/common/tls.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | var defaultTLSCipherSuites = []string{ 7 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 8 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 9 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 10 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 11 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 12 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 13 | } 14 | 15 | func DefaultTLSCipherSuites() []string { 16 | return defaultTLSCipherSuites 17 | } 18 | 19 | // NOTE: the following cipher suites are currently supported by Envoy but insecure and 20 | // pending removal 21 | var extraTLSCipherSuites = []string{ 22 | // https://github.com/envoyproxy/envoy/issues/5399 23 | "TLS_RSA_WITH_AES_128_GCM_SHA256", 24 | "TLS_RSA_WITH_AES_128_CBC_SHA", 25 | "TLS_RSA_WITH_AES_256_GCM_SHA384", 26 | "TLS_RSA_WITH_AES_256_CBC_SHA", 27 | 28 | // https://github.com/envoyproxy/envoy/issues/5400 29 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 30 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 31 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 32 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 33 | } 34 | 35 | var supportedTLSCipherSuites = (func() map[string]struct{} { 36 | cipherSuites := make(map[string]struct{}) 37 | 38 | for _, c := range append(defaultTLSCipherSuites, extraTLSCipherSuites...) { 39 | cipherSuites[c] = struct{}{} 40 | } 41 | 42 | return cipherSuites 43 | })() 44 | 45 | func SupportedTLSCipherSuite(cipherSuite string) bool { 46 | _, ok := supportedTLSCipherSuites[cipherSuite] 47 | return ok 48 | } 49 | 50 | var SupportedTLSVersions = map[string]struct{}{ 51 | "TLS_AUTO": {}, 52 | "TLSv1_0": {}, 53 | "TLSv1_1": {}, 54 | "TLSv1_2": {}, 55 | "TLSv1_3": {}, 56 | } 57 | 58 | var TLSVersionsWithConfigurableCipherSuites = map[string]struct{}{ 59 | // Remove these two if Envoy ever sets TLS 1.3 as default minimum 60 | "": {}, 61 | "TLS_AUTO": {}, 62 | 63 | "TLSv1_0": {}, 64 | "TLSv1_1": {}, 65 | "TLSv1_2": {}, 66 | } 67 | -------------------------------------------------------------------------------- /internal/common/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "io" 8 | "sync" 9 | ) 10 | 11 | type synchronizedWriter struct { 12 | io.Writer 13 | mutex sync.Mutex 14 | } 15 | 16 | func SynchronizeWriter(writer io.Writer) io.Writer { 17 | return &synchronizedWriter{Writer: writer} 18 | } 19 | 20 | func (w *synchronizedWriter) Write(p []byte) (n int, err error) { 21 | w.mutex.Lock() 22 | defer w.mutex.Unlock() 23 | return w.Writer.Write(p) 24 | } 25 | -------------------------------------------------------------------------------- /internal/consul/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/cenkalti/backoff" 12 | 13 | "github.com/hashicorp/consul/api" 14 | "github.com/hashicorp/go-hclog" 15 | ) 16 | 17 | const ( 18 | authMetaKey = "consul-api-gateway" 19 | ) 20 | 21 | // Authenticator handles Consul auth login logic. 22 | type Authenticator struct { 23 | consul *api.Client 24 | logger hclog.Logger 25 | 26 | method string 27 | namespace string 28 | tries uint64 29 | backoffInterval time.Duration 30 | } 31 | 32 | // NewAuthenticator initializes a new Authenticator instance. 33 | func NewAuthenticator(logger hclog.Logger, consul *api.Client, method, namespace string) *Authenticator { 34 | return &Authenticator{ 35 | consul: consul, 36 | logger: logger, 37 | method: method, 38 | namespace: namespace, 39 | tries: defaultMaxAttempts, 40 | backoffInterval: defaultBackoffInterval, 41 | } 42 | } 43 | 44 | func (a *Authenticator) WithTries(tries uint64) *Authenticator { 45 | a.tries = tries 46 | return a 47 | } 48 | 49 | // Authenticate logs into Consul using the given auth method and returns the generated 50 | // token. 51 | func (a *Authenticator) Authenticate(ctx context.Context, service, bearerToken string) (string, error) { 52 | var token string 53 | var err error 54 | 55 | err = backoff.Retry(func() error { 56 | token, err = a.authenticate(ctx, service, bearerToken) 57 | if err != nil { 58 | a.logger.Error("error authenticating", "error", err) 59 | } 60 | return err 61 | }, backoff.WithContext(backoff.WithMaxRetries(backoff.NewConstantBackOff(a.backoffInterval), a.tries), ctx)) 62 | return token, err 63 | } 64 | 65 | func (a *Authenticator) authenticate(ctx context.Context, service, bearerToken string) (string, error) { 66 | gwName := service 67 | 68 | opts := &api.WriteOptions{} 69 | if a.namespace != "" && a.namespace != "default" { 70 | opts.Namespace = a.namespace 71 | gwName = fmt.Sprintf("%s/%s", a.namespace, service) 72 | } 73 | 74 | token, _, err := a.consul.ACL().Login(&api.ACLLoginParams{ 75 | AuthMethod: a.method, 76 | BearerToken: bearerToken, 77 | Meta: map[string]string{authMetaKey: gwName}, 78 | }, opts.WithContext(ctx)) 79 | if err != nil { 80 | return "", err 81 | } 82 | return token.SecretID, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/consul/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | const ( 11 | defaultMaxAttempts = uint64(30) 12 | defaultBackoffInterval = 1 * time.Second 13 | ) 14 | -------------------------------------------------------------------------------- /internal/consul/config_entries.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "github.com/hashicorp/consul/api" 8 | ) 9 | 10 | type ConfigEntryIndex struct { 11 | kind string 12 | idx map[string]api.ConfigEntry 13 | } 14 | 15 | func NewConfigEntryIndex(kind string) *ConfigEntryIndex { 16 | return &ConfigEntryIndex{ 17 | kind: kind, 18 | idx: map[string]api.ConfigEntry{}, 19 | } 20 | } 21 | 22 | func (i *ConfigEntryIndex) Add(entry api.ConfigEntry) { 23 | if entry == nil { 24 | return 25 | } 26 | if entry.GetKind() != i.kind { 27 | return 28 | } 29 | i.idx[entry.GetName()] = entry 30 | } 31 | 32 | func (i *ConfigEntryIndex) Merge(other *ConfigEntryIndex) { 33 | if other == nil { 34 | return 35 | } 36 | if i.kind != other.kind { 37 | return 38 | } 39 | for k, v := range other.idx { 40 | i.idx[k] = v 41 | } 42 | } 43 | 44 | func (i *ConfigEntryIndex) Get(name string) (api.ConfigEntry, bool) { 45 | c, ok := i.idx[name] 46 | return c, ok 47 | } 48 | 49 | func (i *ConfigEntryIndex) Count() int { 50 | return len(i.idx) 51 | } 52 | 53 | // Difference will return an ConfigEntryIndex with entries that not found in the current ConfigEntryIndex 54 | func (i *ConfigEntryIndex) Difference(other *ConfigEntryIndex) *ConfigEntryIndex { 55 | return i.filter(other, false) 56 | } 57 | 58 | func (i *ConfigEntryIndex) Intersection(other *ConfigEntryIndex) *ConfigEntryIndex { 59 | return i.filter(other, true) 60 | } 61 | 62 | func (i *ConfigEntryIndex) filter(other *ConfigEntryIndex, include bool) *ConfigEntryIndex { 63 | result := NewConfigEntryIndex(i.kind) 64 | for _, c := range other.idx { 65 | if _, ok := i.idx[c.GetName()]; ok && include { 66 | // we're looking for the set that is in i 67 | result.Add(c) 68 | } else if !ok && !include { 69 | // we're looking for the set that isn't in i 70 | result.Add(c) 71 | } 72 | } 73 | return result 74 | } 75 | 76 | func (i *ConfigEntryIndex) ToArray() []api.ConfigEntry { 77 | result := make([]api.ConfigEntry, len(i.idx)) 78 | var j int 79 | for _, c := range i.idx { 80 | result[j] = c 81 | j++ 82 | } 83 | return result 84 | } 85 | -------------------------------------------------------------------------------- /internal/consul/disco_chain_watcher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/hashicorp/consul-api-gateway/internal/common" 11 | "github.com/hashicorp/consul/api" 12 | "github.com/hashicorp/go-hclog" 13 | ) 14 | 15 | // discoChainWatcher uses blocking queries to poll for changes in a services discovery chain 16 | type discoChainWatcher struct { 17 | name api.CompoundServiceName 18 | 19 | ctx context.Context 20 | cancel context.CancelFunc 21 | 22 | prevResults *common.ServiceNameIndex 23 | results chan *discoChainWatchResult 24 | disco consulDiscoveryChains 25 | 26 | logger hclog.Logger 27 | } 28 | 29 | func newDiscoChainWatcher(ctx context.Context, service api.CompoundServiceName, results chan *discoChainWatchResult, disco consulDiscoveryChains, logger hclog.Logger) *discoChainWatcher { 30 | child, cancel := context.WithCancel(ctx) 31 | w := &discoChainWatcher{ 32 | ctx: child, 33 | cancel: cancel, 34 | prevResults: common.NewServiceNameIndex(), 35 | results: results, 36 | disco: disco, 37 | name: service, 38 | logger: logger, 39 | } 40 | go w.watchLoop() 41 | return w 42 | } 43 | 44 | func (w *discoChainWatcher) Cancel() { 45 | w.cancel() 46 | } 47 | 48 | type discoChainWatchResult struct { 49 | name api.CompoundServiceName 50 | added []api.CompoundServiceName 51 | removed []api.CompoundServiceName 52 | } 53 | 54 | func (w *discoChainWatcher) watchLoop() { 55 | var index uint64 56 | for { 57 | opts := &api.QueryOptions{WaitIndex: index, Namespace: w.name.Namespace} 58 | resp, meta, err := w.disco.Get(w.name.Name, nil, opts.WithContext(w.ctx)) 59 | if err != nil { 60 | w.logger.Warn("blocking query for gateway discovery chain failed", "error", err) 61 | select { 62 | case <-w.ctx.Done(): 63 | return 64 | case <-time.After(time.Second): 65 | // avoid hot looping on error 66 | } 67 | continue 68 | } 69 | 70 | if meta.LastIndex < index { 71 | index = 0 72 | } else { 73 | index = meta.LastIndex 74 | } 75 | 76 | names := compiledDiscoChainToServiceNames(resp.Chain) 77 | added, removed := w.prevResults.Diff(names) 78 | result := &discoChainWatchResult{ 79 | name: w.name, 80 | added: added, 81 | removed: removed, 82 | } 83 | 84 | select { 85 | case <-w.ctx.Done(): 86 | return 87 | case w.results <- result: 88 | w.prevResults = names 89 | } 90 | } 91 | } 92 | 93 | func compiledDiscoChainToServiceNames(chain *api.CompiledDiscoveryChain) *common.ServiceNameIndex { 94 | names := common.NewServiceNameIndex() 95 | for _, target := range chain.Targets { 96 | if target.Service == chain.ServiceName && chain.Protocol != "tcp" { 97 | // chain targets include the chain service name 98 | // this service should only be included if the protocol is tcp 99 | continue 100 | } 101 | names.Add(api.CompoundServiceName{ 102 | Name: target.Service, 103 | Namespace: target.Namespace, 104 | }) 105 | } 106 | return names 107 | } 108 | -------------------------------------------------------------------------------- /internal/consul/disco_chain_watcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/consul/api" 14 | "github.com/hashicorp/consul/sdk/testutil" 15 | ) 16 | 17 | func TestIntentionsReconciler_watchDiscoveryChain(t *testing.T) { 18 | require := require.New(t) 19 | consulSrv, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { 20 | c.Peering = nil 21 | }) 22 | require.NoError(err) 23 | consulSrv.WaitForServiceIntentions(t) 24 | cfg := api.DefaultConfig() 25 | cfg.Address = consulSrv.HTTPAddr 26 | c, err := api.NewClient(cfg) 27 | require.NoError(err) 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | results := make(chan *discoChainWatchResult) 30 | t.Cleanup(func() { 31 | cancel() 32 | _ = consulSrv.Stop() 33 | close(results) 34 | }) 35 | 36 | for _, name := range []string{"router", "upstream1", "upstream2"} { 37 | ok, _, err := c.ConfigEntries().Set(&api.ServiceConfigEntry{ 38 | Kind: api.ServiceDefaults, 39 | Name: name, 40 | Protocol: "http", 41 | }, nil) 42 | require.True(ok) 43 | require.NoError(err) 44 | } 45 | 46 | ok, _, err := c.ConfigEntries().Set(&api.ServiceRouterConfigEntry{ 47 | Kind: api.ServiceRouter, 48 | Name: "router", 49 | Routes: []api.ServiceRoute{ 50 | { 51 | Match: &api.ServiceRouteMatch{ 52 | HTTP: &api.ServiceRouteHTTPMatch{ 53 | PathPrefix: "/1", 54 | }, 55 | }, 56 | Destination: &api.ServiceRouteDestination{ 57 | Service: "upstream1", 58 | }, 59 | }, 60 | { 61 | Match: &api.ServiceRouteMatch{ 62 | HTTP: &api.ServiceRouteHTTPMatch{ 63 | PathPrefix: "/2", 64 | }, 65 | }, 66 | Destination: &api.ServiceRouteDestination{ 67 | Service: "upstream2", 68 | }, 69 | }, 70 | }, 71 | }, nil) 72 | require.True(ok) 73 | require.NoError(err) 74 | 75 | err = c.Agent().ServiceRegister(&api.AgentServiceRegistration{ 76 | Name: "upstream1", 77 | Port: 9991, 78 | Address: "127.0.0.1", 79 | }) 80 | require.NoError(err) 81 | err = c.Agent().ServiceRegister(&api.AgentServiceRegistration{ 82 | Name: "upstream2", 83 | Port: 9992, 84 | Address: "127.0.0.1", 85 | }) 86 | require.NoError(err) 87 | w := newDiscoChainWatcher(ctx, api.CompoundServiceName{Name: "router"}, results, c.DiscoveryChain(), testutil.Logger(t)) 88 | 89 | var result *discoChainWatchResult 90 | require.Eventually(func() bool { 91 | var ok bool 92 | result, ok = <-results 93 | return ok 94 | }, 5*time.Second, 500*time.Millisecond) 95 | require.NotNil(results) 96 | require.Equal(w.name, result.name) 97 | require.Len(result.added, 2) 98 | require.Len(result.removed, 0) 99 | 100 | w.Cancel() 101 | } 102 | -------------------------------------------------------------------------------- /internal/consul/mocks/peerings.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./peerings.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | api "github.com/hashicorp/consul/api" 13 | ) 14 | 15 | // MockPeerings is a mock of Peerings interface. 16 | type MockPeerings struct { 17 | ctrl *gomock.Controller 18 | recorder *MockPeeringsMockRecorder 19 | } 20 | 21 | // MockPeeringsMockRecorder is the mock recorder for MockPeerings. 22 | type MockPeeringsMockRecorder struct { 23 | mock *MockPeerings 24 | } 25 | 26 | // NewMockPeerings creates a new mock instance. 27 | func NewMockPeerings(ctrl *gomock.Controller) *MockPeerings { 28 | mock := &MockPeerings{ctrl: ctrl} 29 | mock.recorder = &MockPeeringsMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockPeerings) EXPECT() *MockPeeringsMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Read mocks base method. 39 | func (m *MockPeerings) Read(arg0 context.Context, arg1 string, arg2 *api.QueryOptions) (*api.Peering, *api.QueryMeta, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Read", arg0, arg1, arg2) 42 | ret0, _ := ret[0].(*api.Peering) 43 | ret1, _ := ret[1].(*api.QueryMeta) 44 | ret2, _ := ret[2].(error) 45 | return ret0, ret1, ret2 46 | } 47 | 48 | // Read indicates an expected call of Read. 49 | func (mr *MockPeeringsMockRecorder) Read(arg0, arg1, arg2 interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockPeerings)(nil).Read), arg0, arg1, arg2) 52 | } 53 | -------------------------------------------------------------------------------- /internal/consul/peerings.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/consul/api" 10 | ) 11 | 12 | //go:generate mockgen -source ./peerings.go -destination ./mocks/peerings.go -package mocks Peerings 13 | 14 | type Peerings interface { 15 | Read(context.Context, string, *api.QueryOptions) (*api.Peering, *api.QueryMeta, error) 16 | } 17 | -------------------------------------------------------------------------------- /internal/consul/test_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package consul 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/hashicorp/consul/api" 11 | 12 | "github.com/hashicorp/consul-api-gateway/internal/consul/mocks" 13 | ) 14 | 15 | type TestClient struct { 16 | *api.Client 17 | 18 | peerings *mocks.MockPeerings 19 | } 20 | 21 | func NewTestClient(c *api.Client) *TestClient { 22 | return &TestClient{ 23 | Client: c, 24 | } 25 | } 26 | 27 | func (c *TestClient) WatchServers(ctx context.Context) error { 28 | return nil 29 | } 30 | 31 | func (c *TestClient) Token() string { 32 | return "" 33 | } 34 | 35 | func (c *TestClient) Wait(time.Duration) error { 36 | return nil 37 | } 38 | 39 | func (c *TestClient) Internal() *api.Client { 40 | return c.Client 41 | } 42 | 43 | func (c *TestClient) Peerings() PeeringClient { 44 | if c.peerings == nil { 45 | return c.Client.Peerings() 46 | } 47 | return c.peerings 48 | } 49 | 50 | func (c *TestClient) SetPeerings(peerings *mocks.MockPeerings) { 51 | c.peerings = peerings 52 | } 53 | -------------------------------------------------------------------------------- /internal/core/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | import ( 7 | "context" 8 | ) 9 | 10 | // SyncAdapter is used for synchronizing store state to 11 | // an external system 12 | type SyncAdapter interface { 13 | Sync(ctx context.Context, gateway ResolvedGateway) (bool, error) 14 | Clear(ctx context.Context, id GatewayID) error 15 | } 16 | -------------------------------------------------------------------------------- /internal/core/route.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | type CommonRoute struct { 7 | Meta map[string]string 8 | Name string 9 | Namespace string 10 | } 11 | 12 | func (c CommonRoute) GetMeta() map[string]string { 13 | return c.Meta 14 | } 15 | 16 | func (c CommonRoute) GetName() string { 17 | return c.Name 18 | } 19 | 20 | func (c CommonRoute) GetNamespace() string { 21 | return c.Namespace 22 | } 23 | -------------------------------------------------------------------------------- /internal/core/tcp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package core 5 | 6 | type TCPRoute struct { 7 | CommonRoute 8 | Service ResolvedService 9 | } 10 | 11 | func (r TCPRoute) GetType() ResolvedRouteType { 12 | return ResolvedTCPRouteType 13 | } 14 | 15 | type TCPRouteBuilder struct { 16 | meta map[string]string 17 | name string 18 | namespace string 19 | service ResolvedService 20 | } 21 | 22 | func (b *TCPRouteBuilder) WithMeta(meta map[string]string) *TCPRouteBuilder { 23 | b.meta = meta 24 | return b 25 | } 26 | 27 | func (b *TCPRouteBuilder) WithName(name string) *TCPRouteBuilder { 28 | b.name = name 29 | return b 30 | } 31 | 32 | func (b *TCPRouteBuilder) WithNamespace(namespace string) *TCPRouteBuilder { 33 | b.namespace = namespace 34 | return b 35 | } 36 | 37 | func (b *TCPRouteBuilder) WithService(service ResolvedService) *TCPRouteBuilder { 38 | b.service = service 39 | return b 40 | } 41 | 42 | func (b *TCPRouteBuilder) Build() ResolvedRoute { 43 | return TCPRoute{ 44 | CommonRoute: CommonRoute{ 45 | Meta: b.meta, 46 | Name: b.name, 47 | Namespace: b.namespace, 48 | }, 49 | Service: b.service, 50 | } 51 | } 52 | 53 | func NewTCPRouteBuilder() *TCPRouteBuilder { 54 | return &TCPRouteBuilder{} 55 | } 56 | -------------------------------------------------------------------------------- /internal/envoy/mocks/middleware.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./middleware.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | -------------------------------------------------------------------------------- /internal/envoy/mocks/sds.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./sds.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | tls "crypto/tls" 9 | x509 "crypto/x509" 10 | reflect "reflect" 11 | 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockCertificateFetcher is a mock of CertificateFetcher interface. 16 | type MockCertificateFetcher struct { 17 | ctrl *gomock.Controller 18 | recorder *MockCertificateFetcherMockRecorder 19 | } 20 | 21 | // MockCertificateFetcherMockRecorder is the mock recorder for MockCertificateFetcher. 22 | type MockCertificateFetcherMockRecorder struct { 23 | mock *MockCertificateFetcher 24 | } 25 | 26 | // NewMockCertificateFetcher creates a new mock instance. 27 | func NewMockCertificateFetcher(ctrl *gomock.Controller) *MockCertificateFetcher { 28 | mock := &MockCertificateFetcher{ctrl: ctrl} 29 | mock.recorder = &MockCertificateFetcherMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockCertificateFetcher) EXPECT() *MockCertificateFetcherMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // RootPool mocks base method. 39 | func (m *MockCertificateFetcher) RootPool() *x509.CertPool { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "RootPool") 42 | ret0, _ := ret[0].(*x509.CertPool) 43 | return ret0 44 | } 45 | 46 | // RootPool indicates an expected call of RootPool. 47 | func (mr *MockCertificateFetcherMockRecorder) RootPool() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RootPool", reflect.TypeOf((*MockCertificateFetcher)(nil).RootPool)) 50 | } 51 | 52 | // TLSCertificate mocks base method. 53 | func (m *MockCertificateFetcher) TLSCertificate() *tls.Certificate { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "TLSCertificate") 56 | ret0, _ := ret[0].(*tls.Certificate) 57 | return ret0 58 | } 59 | 60 | // TLSCertificate indicates an expected call of TLSCertificate. 61 | func (mr *MockCertificateFetcherMockRecorder) TLSCertificate() *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSCertificate", reflect.TypeOf((*MockCertificateFetcher)(nil).TLSCertificate)) 64 | } 65 | -------------------------------------------------------------------------------- /internal/k8s/README.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | ## Lifecycle 4 | 5 | Every time we enter a reconciliation loop we do essentially the same 5 things in the following order for logical 6 | consistency: 7 | 8 | 1. See where we are in the lifecycle of an object -- if it's being deleted, clean it up, if it needs finalizer 9 | additions or deletions, do them -- this happens in the Controller/reconciler itself. 10 | 2. Validate and resolve any references to external objects -- this happens in the reconciliation manager. 11 | 3. Attempt to upsert the object into our state tree -- this happens in the store that the manager invokes. 12 | 4. Synchronize any changed objects from the upsert into Consul's state -- this happens in a synchronization 13 | adapter used by the store. 14 | 5. Synchronize any status changes back to Kubernetes -- this happens as callbacks invoked by the store after 15 | synchronization either fails or completes. 16 | 17 | ## Error Handling 18 | 19 | Generally we've followed the following major design principles for error handling in our reconciler loop code: 20 | 21 | 1. Any unexpected Kubernetes API errors should return an error from the controller `Reconcile` method which will 22 | immediately reschedule (in a ratelimited fashion) the event on the loop. 23 | 2. Any errors that are non-retryable in nature due to bad data i.e. validation errors, should be swallowed and 24 | Kubernetes status conditions used to give user feedback. 25 | 3. Errors from synchronizing a resolved Gateway-Route tree, whether they're Consul network-connectivity issues 26 | or data validation issues due to external modification or invalid Consul state should be retried, however the 27 | reconciliation loop ***should not*** be blocked, instead the reconciliation attempt should be requeued with a 28 | delay, allowing subsequent state changes to still be processed, causing whatever guards against out-of-date 29 | configuration (generation checks) to eventually drop the bad event. 30 | 4. In order to facillitate the above, wrap all controller `Reconcile` methods in `gatewayclient.NewRequeueingMiddleware` 31 | and make sure any errors returned from the `gatewayclient` api calls are wrapped in `gatewayclient.K8sError`. -------------------------------------------------------------------------------- /internal/k8s/builder/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package builder 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | 12 | v1 "k8s.io/api/apps/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | 15 | "github.com/hashicorp/consul-api-gateway/internal/version" 16 | ) 17 | 18 | var ( 19 | defaultImage string 20 | defaultServiceAnnotations = []string{ 21 | "external-dns.alpha.kubernetes.io/hostname", 22 | } 23 | ) 24 | 25 | func init() { 26 | imageVersion := version.Version 27 | if version.VersionPrerelease != "" { 28 | imageVersion += "-" + version.VersionPrerelease 29 | } 30 | defaultImage = fmt.Sprintf("hashicorp/consul-api-gateway:%s", imageVersion) 31 | } 32 | 33 | const ( 34 | defaultEnvoyImage = "envoyproxy/envoy:v1.24-latest" 35 | defaultLogLevel = "info" 36 | defaultConsulAddress = "$(HOST_IP)" 37 | defaultConsulHTTPPort = "8500" 38 | defaultConsulXDSPort = "8502" 39 | defaultInstances int32 = 1 40 | 41 | consulCALocalPath = "/consul/tls" 42 | consulCAFilename = "ca.pem" 43 | 44 | k8sHostnameTopologyKey = "kubernetes.io/hostname" 45 | ) 46 | 47 | var ( 48 | defaultPartition = os.Getenv("CONSUL_PARTITION") 49 | defaultServerName = os.Getenv("CONSUL_TLS_SERVER_NAME") 50 | ) 51 | 52 | var consulCALocalFile = filepath.Join(consulCALocalPath, consulCAFilename) 53 | 54 | type Builder interface { 55 | Validate() error 56 | } 57 | 58 | type DeploymentBuilder interface { 59 | Builder 60 | Build(*int32) *v1.Deployment 61 | } 62 | 63 | type ServiceBuilder interface { 64 | Builder 65 | Build() *corev1.Service 66 | } 67 | 68 | func orDefault(value, defaultValue string) string { 69 | if value != "" { 70 | return value 71 | } 72 | return defaultValue 73 | } 74 | 75 | func orDefaultIntString(value int, defaultValue string) string { 76 | if value != 0 { 77 | return strconv.Itoa(value) 78 | } 79 | return defaultValue 80 | } 81 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/clusterip.service.golden.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | creationTimestamp: null 3 | labels: 4 | api-gateway.consul.hashicorp.com/created: "-62135596800" 5 | api-gateway.consul.hashicorp.com/managed: "true" 6 | api-gateway.consul.hashicorp.com/name: test-clusterip 7 | api-gateway.consul.hashicorp.com/namespace: "" 8 | name: test-clusterip 9 | spec: 10 | ports: 11 | - name: http 12 | port: 8080 13 | protocol: TCP 14 | targetPort: 0 15 | - name: https 16 | port: 8443 17 | protocol: TCP 18 | targetPort: 0 19 | selector: 20 | api-gateway.consul.hashicorp.com/created: "-62135596800" 21 | api-gateway.consul.hashicorp.com/managed: "true" 22 | api-gateway.consul.hashicorp.com/name: test-clusterip 23 | api-gateway.consul.hashicorp.com/namespace: "" 24 | type: ClusterIP 25 | status: 26 | loadBalancer: {} 27 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/clusterip.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | serviceType: "ClusterIP" 10 | --- 11 | apiVersion: gateway.networking.k8s.io/v1alpha2 12 | kind: GatewayClass 13 | metadata: 14 | name: test-gateway-class 15 | spec: 16 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 17 | parametersRef: 18 | group: api-gateway.consul.hashicorp.com 19 | kind: GatewayClassConfig 20 | name: test-gateway-class-config 21 | --- 22 | apiVersion: gateway.networking.k8s.io/v1alpha2 23 | kind: Gateway 24 | metadata: 25 | name: test-clusterip 26 | spec: 27 | gatewayClassName: test-gateway-class 28 | listeners: 29 | - protocol: HTTP 30 | port: 8080 31 | name: http 32 | allowedRoutes: 33 | namespaces: 34 | from: Same 35 | - protocol: HTTPS 36 | port: 8443 37 | name: https 38 | allowedRoutes: 39 | namespaces: 40 | from: Same 41 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/loadbalancer.service.golden.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | annotations: 3 | external-dns.alpha.kubernetes.io/hostname: test.example.com 4 | creationTimestamp: null 5 | labels: 6 | api-gateway.consul.hashicorp.com/created: "-62135596800" 7 | api-gateway.consul.hashicorp.com/managed: "true" 8 | api-gateway.consul.hashicorp.com/name: test-loadbalancer 9 | api-gateway.consul.hashicorp.com/namespace: "" 10 | name: test-loadbalancer 11 | spec: 12 | ports: 13 | - name: http 14 | port: 8080 15 | protocol: TCP 16 | targetPort: 0 17 | - name: https 18 | port: 8443 19 | protocol: TCP 20 | targetPort: 0 21 | selector: 22 | api-gateway.consul.hashicorp.com/created: "-62135596800" 23 | api-gateway.consul.hashicorp.com/managed: "true" 24 | api-gateway.consul.hashicorp.com/name: test-loadbalancer 25 | api-gateway.consul.hashicorp.com/namespace: "" 26 | type: LoadBalancer 27 | status: 28 | loadBalancer: {} 29 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | serviceType: "LoadBalancer" 10 | --- 11 | apiVersion: gateway.networking.k8s.io/v1alpha2 12 | kind: GatewayClass 13 | metadata: 14 | name: test-gateway-class 15 | spec: 16 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 17 | parametersRef: 18 | group: api-gateway.consul.hashicorp.com 19 | kind: GatewayClassConfig 20 | name: test-gateway-class-config 21 | --- 22 | apiVersion: gateway.networking.k8s.io/v1alpha2 23 | kind: Gateway 24 | metadata: 25 | name: test-loadbalancer 26 | annotations: 27 | "external-dns.alpha.kubernetes.io/hostname": "test.example.com" 28 | spec: 29 | gatewayClassName: test-gateway-class 30 | listeners: 31 | - protocol: HTTP 32 | port: 8080 33 | name: http 34 | allowedRoutes: 35 | namespaces: 36 | from: Same 37 | - protocol: HTTPS 38 | port: 8443 39 | name: https 40 | allowedRoutes: 41 | namespaces: 42 | from: Same 43 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/max-instances.service.golden.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | creationTimestamp: null 3 | labels: 4 | api-gateway.consul.hashicorp.com/created: "-62135596800" 5 | api-gateway.consul.hashicorp.com/managed: "true" 6 | api-gateway.consul.hashicorp.com/name: test-max-instances 7 | api-gateway.consul.hashicorp.com/namespace: "" 8 | name: test-max-instances 9 | spec: 10 | ports: 11 | - name: http 12 | port: 8080 13 | protocol: TCP 14 | targetPort: 0 15 | - name: https 16 | port: 8443 17 | protocol: TCP 18 | targetPort: 0 19 | selector: 20 | api-gateway.consul.hashicorp.com/created: "-62135596800" 21 | api-gateway.consul.hashicorp.com/managed: "true" 22 | api-gateway.consul.hashicorp.com/name: test-max-instances 23 | api-gateway.consul.hashicorp.com/namespace: "" 24 | type: ClusterIP 25 | status: 26 | loadBalancer: {} 27 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/max-instances.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | image: 10 | consulAPIGateway: hashicorp/consul-api-gateway:0.2.1 11 | serviceType: "ClusterIP" 12 | deployment: 13 | defaultInstances: 8 14 | minInstances: 2 15 | maxInstances: 5 16 | --- 17 | apiVersion: gateway.networking.k8s.io/v1alpha2 18 | kind: GatewayClass 19 | metadata: 20 | name: test-gateway-class 21 | spec: 22 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 23 | parametersRef: 24 | group: api-gateway.consul.hashicorp.com 25 | kind: GatewayClassConfig 26 | name: test-gateway-class-config 27 | --- 28 | apiVersion: gateway.networking.k8s.io/v1alpha2 29 | kind: Gateway 30 | metadata: 31 | name: test-max-instances 32 | spec: 33 | gatewayClassName: test-gateway-class 34 | listeners: 35 | - protocol: HTTP 36 | port: 8080 37 | name: http 38 | allowedRoutes: 39 | namespaces: 40 | from: Same 41 | - protocol: HTTPS 42 | port: 8443 43 | name: https 44 | allowedRoutes: 45 | namespaces: 46 | from: Same 47 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/min-instances.service.golden.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | creationTimestamp: null 3 | labels: 4 | api-gateway.consul.hashicorp.com/created: "-62135596800" 5 | api-gateway.consul.hashicorp.com/managed: "true" 6 | api-gateway.consul.hashicorp.com/name: test-min-instances 7 | api-gateway.consul.hashicorp.com/namespace: "" 8 | name: test-min-instances 9 | spec: 10 | ports: 11 | - name: http 12 | port: 8080 13 | protocol: TCP 14 | targetPort: 0 15 | - name: https 16 | port: 8443 17 | protocol: TCP 18 | targetPort: 0 19 | selector: 20 | api-gateway.consul.hashicorp.com/created: "-62135596800" 21 | api-gateway.consul.hashicorp.com/managed: "true" 22 | api-gateway.consul.hashicorp.com/name: test-min-instances 23 | api-gateway.consul.hashicorp.com/namespace: "" 24 | type: ClusterIP 25 | status: 26 | loadBalancer: {} 27 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/min-instances.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | image: 10 | consulAPIGateway: hashicorp/consul-api-gateway:0.2.1 11 | serviceType: "ClusterIP" 12 | deployment: 13 | defaultInstances: 1 14 | minInstances: 5 15 | maxInstances: 8 16 | --- 17 | apiVersion: gateway.networking.k8s.io/v1alpha2 18 | kind: GatewayClass 19 | metadata: 20 | name: test-gateway-class 21 | spec: 22 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 23 | parametersRef: 24 | group: api-gateway.consul.hashicorp.com 25 | kind: GatewayClassConfig 26 | name: test-gateway-class-config 27 | --- 28 | apiVersion: gateway.networking.k8s.io/v1alpha2 29 | kind: Gateway 30 | metadata: 31 | name: test-min-instances 32 | spec: 33 | gatewayClassName: test-gateway-class 34 | listeners: 35 | - protocol: HTTP 36 | port: 8080 37 | name: http 38 | allowedRoutes: 39 | namespaces: 40 | from: Same 41 | - protocol: HTTPS 42 | port: 8443 43 | name: https 44 | allowedRoutes: 45 | namespaces: 46 | from: Same 47 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/multiple-instances.service.golden.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | creationTimestamp: null 3 | labels: 4 | api-gateway.consul.hashicorp.com/created: "-62135596800" 5 | api-gateway.consul.hashicorp.com/managed: "true" 6 | api-gateway.consul.hashicorp.com/name: test-multiple-instances 7 | api-gateway.consul.hashicorp.com/namespace: "" 8 | name: test-multiple-instances 9 | spec: 10 | ports: 11 | - name: http 12 | port: 8080 13 | protocol: TCP 14 | targetPort: 0 15 | - name: https 16 | port: 8443 17 | protocol: TCP 18 | targetPort: 0 19 | selector: 20 | api-gateway.consul.hashicorp.com/created: "-62135596800" 21 | api-gateway.consul.hashicorp.com/managed: "true" 22 | api-gateway.consul.hashicorp.com/name: test-multiple-instances 23 | api-gateway.consul.hashicorp.com/namespace: "" 24 | type: ClusterIP 25 | status: 26 | loadBalancer: {} 27 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/multiple-instances.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | image: 10 | consulAPIGateway: hashicorp/consul-api-gateway:0.2.1 11 | serviceType: "ClusterIP" 12 | deployment: 13 | defaultInstances: 8 14 | --- 15 | apiVersion: gateway.networking.k8s.io/v1alpha2 16 | kind: GatewayClass 17 | metadata: 18 | name: test-gateway-class 19 | spec: 20 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 21 | parametersRef: 22 | group: api-gateway.consul.hashicorp.com 23 | kind: GatewayClassConfig 24 | name: test-gateway-class-config 25 | --- 26 | apiVersion: gateway.networking.k8s.io/v1alpha2 27 | kind: Gateway 28 | metadata: 29 | name: test-multiple-instances 30 | spec: 31 | gatewayClassName: test-gateway-class 32 | listeners: 33 | - protocol: HTTP 34 | port: 8080 35 | name: http 36 | allowedRoutes: 37 | namespaces: 38 | from: Same 39 | - protocol: HTTPS 40 | port: 8443 41 | name: https 42 | allowedRoutes: 43 | namespaces: 44 | from: Same 45 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/static-mapping.service.golden.yaml: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/static-mapping.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | logLevel: info 10 | consul: 11 | authentication: 12 | account: consul-api-gateway 13 | method: consul-api-gateway 14 | address: 15 | host.docker.internal 16 | ports: 17 | http: 443 18 | useHostPorts: true 19 | images: 20 | consul-api-gateway: "consul-api-gateway:1" 21 | envoy: "envoy:1" 22 | nodeSelector: 23 | "ingress-ready": "true" 24 | tolerations: 25 | - key: "key1" 26 | operator: "Equal" 27 | value: "value1" 28 | effect: "NoSchedule" 29 | --- 30 | apiVersion: gateway.networking.k8s.io/v1alpha2 31 | kind: GatewayClass 32 | metadata: 33 | name: test-gateway-class 34 | spec: 35 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 36 | parametersRef: 37 | group: api-gateway.consul.hashicorp.com 38 | kind: GatewayClassConfig 39 | name: test-gateway-class-config 40 | --- 41 | apiVersion: gateway.networking.k8s.io/v1alpha2 42 | kind: Gateway 43 | metadata: 44 | name: test-static-mapping 45 | spec: 46 | gatewayClassName: test-gateway-class 47 | listeners: 48 | - protocol: HTTP 49 | port: 8083 50 | name: http 51 | allowedRoutes: 52 | namespaces: 53 | from: Same 54 | - protocol: HTTPS 55 | port: 8443 56 | name: https 57 | allowedRoutes: 58 | namespaces: 59 | from: Same 60 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/tls-cert.service.golden.yaml: -------------------------------------------------------------------------------- 1 | null 2 | -------------------------------------------------------------------------------- /internal/k8s/builder/testdata/tls-cert.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 5 | kind: GatewayClassConfig 6 | metadata: 7 | name: test-gateway-class-config 8 | spec: 9 | consul: 10 | scheme: https 11 | --- 12 | apiVersion: gateway.networking.k8s.io/v1alpha2 13 | kind: GatewayClass 14 | metadata: 15 | name: test-gateway-class 16 | spec: 17 | controller: "hashicorp.com/consul-api-gateway-gateway-controller" 18 | parametersRef: 19 | group: api-gateway.consul.hashicorp.com 20 | kind: GatewayClassConfig 21 | name: test-gateway-class-config 22 | --- 23 | apiVersion: gateway.networking.k8s.io/v1alpha2 24 | kind: Gateway 25 | metadata: 26 | name: tls-cert-test 27 | spec: 28 | gatewayClassName: test-gateway-class 29 | listeners: 30 | - protocol: HTTP 31 | port: 80 32 | name: http 33 | allowedRoutes: 34 | namespaces: 35 | from: Same 36 | -------------------------------------------------------------------------------- /internal/k8s/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package k8s 5 | 6 | import ( 7 | "github.com/hashicorp/go-hclog" 8 | 9 | "github.com/hashicorp/consul-api-gateway/internal/consul" 10 | "github.com/hashicorp/consul-api-gateway/internal/core" 11 | "github.com/hashicorp/consul-api-gateway/internal/k8s/gatewayclient" 12 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler" 13 | "github.com/hashicorp/consul-api-gateway/internal/store" 14 | ) 15 | 16 | func StoreConfig(adapter core.SyncAdapter, client gatewayclient.Client, consulClient consul.Client, logger hclog.Logger, config Config) store.Config { 17 | marshaler := reconciler.NewMarshaler() 18 | binder := reconciler.NewBinder(client) 19 | deployer := reconciler.NewDeployer(reconciler.DeployerConfig{ 20 | ConsulCA: config.CACert, 21 | SDSHost: config.SDSServerHost, 22 | SDSPort: config.SDSServerPort, 23 | Logger: logger, 24 | Client: client, 25 | Consul: consulClient, 26 | ConsulNamespaceMirroring: config.ConsulNamespaceConfig.MirrorKubernetesNamespaces, 27 | ConsulPartitionInfo: config.ConsulNamespaceConfig.PartitionInfo, 28 | }) 29 | updater := reconciler.NewStatusUpdater(logger, client, deployer, ControllerName) 30 | backend := store.NewMemoryBackend() 31 | 32 | return store.Config{ 33 | Logger: logger, 34 | Adapter: adapter, 35 | Backend: backend, 36 | Marshaler: marshaler, 37 | StatusUpdater: updater, 38 | Binder: binder, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/k8s/gatewayclient/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package gatewayclient 5 | 6 | // K8sError is an error type that should wrap any Kubernetes API 7 | // errors that the gatewayclient returns -- they're caught in 8 | // the requeueing middleware to be retried immediately rather 9 | // than with a delayed requeue. 10 | type K8sError struct { 11 | inner error 12 | } 13 | 14 | func NewK8sError(err error) K8sError { 15 | return K8sError{inner: err} 16 | } 17 | 18 | func (s K8sError) Error() string { 19 | return s.inner.Error() 20 | } 21 | -------------------------------------------------------------------------------- /internal/k8s/gatewayclient/middleware.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package gatewayclient 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "time" 10 | 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | 14 | "github.com/hashicorp/go-hclog" 15 | ) 16 | 17 | const ( 18 | requeueAfter = 100 * time.Millisecond 19 | ) 20 | 21 | // NewSyncRequeuingMiddleware handles delaying requeues in the case of synchronization 22 | // errors in order to not block the reconciliation loop 23 | func NewRequeueingMiddleware(logger hclog.Logger, reconciler reconcile.Reconciler) reconcile.Reconciler { 24 | return reconcile.Func(func(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 25 | result, err := reconciler.Reconcile(ctx, req) 26 | return RescheduleK8sError(logger, result, err) 27 | }) 28 | } 29 | 30 | // RescheduleK8sError allows us to reschedule all non-Kubernetes 31 | // errors while not blocking the reconciliation loop because of 32 | // immediate rescheduling 33 | func RescheduleK8sError(logger hclog.Logger, result ctrl.Result, err error) (ctrl.Result, error) { 34 | if err == nil { 35 | return result, nil 36 | } 37 | 38 | var k8sErr K8sError 39 | if errors.As(err, &k8sErr) { 40 | return result, err 41 | } 42 | 43 | logger.Warn("received non-Kubernetes error, delaying requeue", "error", err) 44 | 45 | // clobber the result that was passed since it'll be 46 | // ignored anyway because of the returned error 47 | return ctrl.Result{RequeueAfter: requeueAfter}, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/k8s/gatewayclient/middleware_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package gatewayclient 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | ) 15 | 16 | func TestRescheduleK8sError(t *testing.T) { 17 | inner := errors.New("test") 18 | 19 | result, err := RescheduleK8sError(hclog.NewNullLogger(), ctrl.Result{}, inner) 20 | require.NoError(t, err) 21 | require.Equal(t, ctrl.Result{RequeueAfter: requeueAfter}, result) 22 | 23 | result, err = RescheduleK8sError(hclog.NewNullLogger(), ctrl.Result{}, NewK8sError(inner)) 24 | require.Error(t, err) 25 | require.Equal(t, ctrl.Result{}, result) 26 | } 27 | -------------------------------------------------------------------------------- /internal/k8s/gatewayclient/test_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package gatewayclient 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/runtime" 8 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 9 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 12 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 13 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 14 | 15 | apigwv1alpha1 "github.com/hashicorp/consul-api-gateway/pkg/apis/v1alpha1" 16 | ) 17 | 18 | func NewTestClient(list client.ObjectList, objects ...client.Object) Client { 19 | scheme := runtime.NewScheme() 20 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 21 | utilruntime.Must(gwv1beta1.AddToScheme(scheme)) 22 | utilruntime.Must(gwv1alpha2.AddToScheme(scheme)) 23 | apigwv1alpha1.RegisterTypes(scheme) 24 | 25 | builder := fake. 26 | NewClientBuilder(). 27 | WithScheme(scheme). 28 | WithObjects(objects...) 29 | 30 | if list != nil { 31 | builder = builder.WithLists(list) 32 | } 33 | 34 | return New(builder.Build(), scheme, "") 35 | } 36 | -------------------------------------------------------------------------------- /internal/k8s/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package k8s 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | ) 11 | 12 | func fromHCLogger(log hclog.Logger) logr.Logger { 13 | return logr.New(&logger{log}) 14 | } 15 | 16 | // logger is a LogSink that wraps hclog 17 | type logger struct { 18 | hclog.Logger 19 | } 20 | 21 | // Verify that it actually implements the interface 22 | var _ logr.LogSink = logger{} 23 | 24 | func (l logger) Init(logr.RuntimeInfo) { 25 | } 26 | 27 | func (l logger) Enabled(_ int) bool { 28 | return true 29 | } 30 | 31 | func (l logger) Info(_ int, msg string, keysAndValues ...interface{}) { 32 | keysAndValues = append([]interface{}{"info", msg}, keysAndValues...) 33 | l.Logger.Info(msg, keysAndValues...) 34 | } 35 | 36 | func (l logger) Error(err error, msg string, keysAndValues ...interface{}) { 37 | keysAndValues = append([]interface{}{"error", err}, keysAndValues...) 38 | l.Logger.Error(msg, keysAndValues...) 39 | } 40 | 41 | func (l logger) WithValues(keysAndValues ...interface{}) logr.LogSink { 42 | return &logger{l.With(keysAndValues...)} 43 | } 44 | 45 | func (l logger) WithName(name string) logr.LogSink { 46 | return &logger{l.Named(name)} 47 | } 48 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/common/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package common 5 | 6 | import ( 7 | "encoding/json" 8 | 9 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 10 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 11 | ) 12 | 13 | var ( 14 | supportedKindsForProtocol = map[gwv1beta1.ProtocolType][]gwv1beta1.RouteGroupKind{ 15 | gwv1beta1.HTTPProtocolType: {{ 16 | Group: (*gwv1beta1.Group)(&gwv1beta1.GroupVersion.Group), 17 | Kind: "HTTPRoute", 18 | }}, 19 | gwv1beta1.HTTPSProtocolType: {{ 20 | Group: (*gwv1beta1.Group)(&gwv1beta1.GroupVersion.Group), 21 | Kind: "HTTPRoute", 22 | }}, 23 | gwv1beta1.TCPProtocolType: {{ 24 | Group: (*gwv1beta1.Group)(&gwv1beta1.GroupVersion.Group), 25 | Kind: "TCPRoute", 26 | }}, 27 | } 28 | ) 29 | 30 | // SupportedKindsFor returns the list of xRoute Kinds that support a given protocol 31 | func SupportedKindsFor(protocol gwv1beta1.ProtocolType) []gwv1beta1.RouteGroupKind { 32 | return supportedKindsForProtocol[protocol] 33 | } 34 | 35 | // AsJSON serializes a given item into a JSON string. The item is assumed to be 36 | // JSON-serializable as this function will panic otherwise. 37 | func AsJSON(item interface{}) string { 38 | data, err := json.Marshal(item) 39 | if err != nil { 40 | // everything passed to this internally should be 41 | // serializable, if something is passed to it that 42 | // isn't, just panic since it's a usage error at 43 | // that point 44 | panic(err) 45 | } 46 | return string(data) 47 | } 48 | 49 | // ParseParent deserializes a JSON string into a gwv1alpha2.ParentReference. The string 50 | // is assumed to be a valid JSON representation as this function will panic otherwise. 51 | func ParseParent(stringified string) gwv1alpha2.ParentReference { 52 | var ref gwv1alpha2.ParentReference 53 | if err := json.Unmarshal([]byte(stringified), &ref); err != nil { 54 | // everything passed to this internally should be 55 | // deserializable, if something is passed to it that 56 | // isn't, just panic since it's a usage error at 57 | // that point 58 | panic(err) 59 | } 60 | return ref 61 | } 62 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/converter/tcp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package converter 5 | 6 | import ( 7 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 8 | 9 | "github.com/hashicorp/consul-api-gateway/internal/core" 10 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/state" 11 | "github.com/hashicorp/consul-api-gateway/internal/k8s/service" 12 | ) 13 | 14 | type TCPRouteConverter struct { 15 | namespace string 16 | hostname string 17 | meta map[string]string 18 | route *gwv1alpha2.TCPRoute 19 | state *state.RouteState 20 | } 21 | 22 | type TCPRouteConverterConfig struct { 23 | Namespace string 24 | Hostname string 25 | Prefix string 26 | Meta map[string]string 27 | Route *gwv1alpha2.TCPRoute 28 | State *state.RouteState 29 | } 30 | 31 | func NewTCPRouteConverter(config TCPRouteConverterConfig) *TCPRouteConverter { 32 | return &TCPRouteConverter{ 33 | namespace: config.Namespace, 34 | hostname: config.Hostname, 35 | meta: config.Meta, 36 | route: config.Route, 37 | state: config.State, 38 | } 39 | } 40 | 41 | func (c *TCPRouteConverter) Convert() core.ResolvedRoute { 42 | return core.NewTCPRouteBuilder(). 43 | WithName(c.route.Name). 44 | // we always use the listener namespace for the routes 45 | // themselves, while the services they route to might 46 | // be in different namespaces 47 | WithNamespace(c.namespace). 48 | WithMeta(c.meta). 49 | WithService(tcpReferencesToService(c.state.References)). 50 | Build() 51 | } 52 | 53 | func tcpReferencesToService(referenceMap service.RouteRuleReferenceMap) core.ResolvedService { 54 | for _, references := range referenceMap { 55 | for _, reference := range references { 56 | switch reference.Type { 57 | case service.ConsulServiceReference: 58 | // at this point there should only be a single resolved service in the reference map 59 | return core.ResolvedService{ 60 | ConsulNamespace: reference.Consul.Namespace, 61 | Service: reference.Consul.Name, 62 | } 63 | default: 64 | continue 65 | } 66 | } 67 | } 68 | return core.ResolvedService{} 69 | } 70 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/errors/errors.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | - name: CertificateResolution 5 | types: ["NotFound","NotPermitted","Unsupported"] 6 | - name: Bind 7 | types: ["RouteKind","ListenerNamespacePolicy","HostnameMismatch","RouteInvalid"] -------------------------------------------------------------------------------- /internal/k8s/reconciler/errors/zz_generated_errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package errors 5 | 6 | // GENERATED from errors.yaml, DO NOT EDIT DIRECTLY 7 | 8 | type CertificateResolutionErrorType string 9 | 10 | const ( 11 | CertificateResolutionErrorTypeNotFound CertificateResolutionErrorType = "NotFoundError" 12 | CertificateResolutionErrorTypeNotPermitted CertificateResolutionErrorType = "NotPermittedError" 13 | CertificateResolutionErrorTypeUnsupported CertificateResolutionErrorType = "UnsupportedError" 14 | ) 15 | 16 | type CertificateResolutionError struct { 17 | inner string 18 | errorType CertificateResolutionErrorType 19 | } 20 | 21 | func NewCertificateResolutionErrorNotFound(inner string) CertificateResolutionError { 22 | return CertificateResolutionError{inner, CertificateResolutionErrorTypeNotFound} 23 | } 24 | func NewCertificateResolutionErrorNotPermitted(inner string) CertificateResolutionError { 25 | return CertificateResolutionError{inner, CertificateResolutionErrorTypeNotPermitted} 26 | } 27 | func NewCertificateResolutionErrorUnsupported(inner string) CertificateResolutionError { 28 | return CertificateResolutionError{inner, CertificateResolutionErrorTypeUnsupported} 29 | } 30 | 31 | func (r CertificateResolutionError) Error() string { 32 | return r.inner 33 | } 34 | 35 | func (r CertificateResolutionError) Kind() CertificateResolutionErrorType { 36 | return r.errorType 37 | } 38 | 39 | type BindErrorType string 40 | 41 | const ( 42 | BindErrorTypeRouteKind BindErrorType = "RouteKindError" 43 | BindErrorTypeListenerNamespacePolicy BindErrorType = "ListenerNamespacePolicyError" 44 | BindErrorTypeHostnameMismatch BindErrorType = "HostnameMismatchError" 45 | BindErrorTypeRouteInvalid BindErrorType = "RouteInvalidError" 46 | ) 47 | 48 | type BindError struct { 49 | inner string 50 | errorType BindErrorType 51 | } 52 | 53 | func NewBindErrorRouteKind(inner string) BindError { 54 | return BindError{inner, BindErrorTypeRouteKind} 55 | } 56 | func NewBindErrorListenerNamespacePolicy(inner string) BindError { 57 | return BindError{inner, BindErrorTypeListenerNamespacePolicy} 58 | } 59 | func NewBindErrorHostnameMismatch(inner string) BindError { 60 | return BindError{inner, BindErrorTypeHostnameMismatch} 61 | } 62 | func NewBindErrorRouteInvalid(inner string) BindError { 63 | return BindError{inner, BindErrorTypeRouteInvalid} 64 | } 65 | 66 | func (r BindError) Error() string { 67 | return r.inner 68 | } 69 | 70 | func (r BindError) Kind() BindErrorType { 71 | return r.errorType 72 | } 73 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/errors/zz_generated_errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package errors 5 | 6 | // GENERATED from errors.yaml, DO NOT EDIT DIRECTLY 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCertificateResolutionErrorType(t *testing.T) { 15 | t.Parallel() 16 | 17 | expected := "expected" 18 | 19 | require.Equal(t, expected, NewCertificateResolutionErrorNotFound(expected).Error()) 20 | require.Equal(t, CertificateResolutionErrorTypeNotFound, NewCertificateResolutionErrorNotFound(expected).Kind()) 21 | require.Equal(t, expected, NewCertificateResolutionErrorNotPermitted(expected).Error()) 22 | require.Equal(t, CertificateResolutionErrorTypeNotPermitted, NewCertificateResolutionErrorNotPermitted(expected).Kind()) 23 | require.Equal(t, expected, NewCertificateResolutionErrorUnsupported(expected).Error()) 24 | require.Equal(t, CertificateResolutionErrorTypeUnsupported, NewCertificateResolutionErrorUnsupported(expected).Kind()) 25 | } 26 | 27 | func TestBindErrorType(t *testing.T) { 28 | t.Parallel() 29 | 30 | expected := "expected" 31 | 32 | require.Equal(t, expected, NewBindErrorRouteKind(expected).Error()) 33 | require.Equal(t, BindErrorTypeRouteKind, NewBindErrorRouteKind(expected).Kind()) 34 | require.Equal(t, expected, NewBindErrorListenerNamespacePolicy(expected).Error()) 35 | require.Equal(t, BindErrorTypeListenerNamespacePolicy, NewBindErrorListenerNamespacePolicy(expected).Kind()) 36 | require.Equal(t, expected, NewBindErrorHostnameMismatch(expected).Error()) 37 | require.Equal(t, BindErrorTypeHostnameMismatch, NewBindErrorHostnameMismatch(expected).Kind()) 38 | require.Equal(t, expected, NewBindErrorRouteInvalid(expected).Error()) 39 | require.Equal(t, BindErrorTypeRouteInvalid, NewBindErrorRouteInvalid(expected).Kind()) 40 | } 41 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/gateway_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/utils/pointer" 13 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 14 | 15 | internalCore "github.com/hashicorp/consul-api-gateway/internal/core" 16 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/state" 17 | apigwv1alpha1 "github.com/hashicorp/consul-api-gateway/pkg/apis/v1alpha1" 18 | ) 19 | 20 | func TestGatewayID(t *testing.T) { 21 | t.Parallel() 22 | 23 | gw := &gwv1beta1.Gateway{ 24 | ObjectMeta: meta.ObjectMeta{ 25 | Name: "name", 26 | Namespace: "namespace", 27 | }, 28 | } 29 | 30 | gwState := state.InitialGatewayState(gw) 31 | gwState.ConsulNamespace = "consul" 32 | 33 | gateway := newK8sGateway(apigwv1alpha1.GatewayClassConfig{}, gw, gwState) 34 | require.Equal(t, internalCore.GatewayID{Service: "name", ConsulNamespace: "consul"}, gateway.ID()) 35 | } 36 | 37 | func TestK8sGateway_Resolve(t *testing.T) { 38 | t.Parallel() 39 | 40 | gcc := apigwv1alpha1.GatewayClassConfig{} 41 | 42 | gw := &gwv1beta1.Gateway{ 43 | ObjectMeta: meta.ObjectMeta{Name: "name", Namespace: "namespace"}, 44 | } 45 | 46 | gwState := state.InitialGatewayState(gw) 47 | gwState.ConsulNamespace = "consul" 48 | 49 | // Verify max connections not set if unset on GatewayClassConfig 50 | resolvedGateway := newK8sGateway(gcc, gw, gwState).Resolve() 51 | assert.Nil(t, resolvedGateway.MaxConnections) 52 | assert.Equal(t, "consul-api-gateway", resolvedGateway.Meta[gatewayMetaExternalSource]) 53 | assert.Equal(t, "name", resolvedGateway.Meta[gatewayMetaName]) 54 | assert.Equal(t, "namespace", resolvedGateway.Meta[gatewayMetaNamespace]) 55 | 56 | // Verify max connections set if set on GatewayClassConfig 57 | gcc.Spec.ConnectionManagement.MaxConnections = pointer.Int32(100) 58 | resolvedGateway = newK8sGateway(gcc, gw, gwState).Resolve() 59 | require.NotNil(t, resolvedGateway.MaxConnections) 60 | assert.EqualValues(t, 100, *resolvedGateway.MaxConnections) 61 | } 62 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/marshaler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "encoding/json" 8 | 9 | "k8s.io/apimachinery/pkg/runtime" 10 | jsonruntime "k8s.io/apimachinery/pkg/runtime/serializer/json" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 13 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 14 | 15 | "github.com/hashicorp/consul-api-gateway/internal/store" 16 | ) 17 | 18 | var ( 19 | _ store.Marshaler = (*Marshaler)(nil) 20 | 21 | scheme = runtime.NewScheme() 22 | 23 | serializerOptions = jsonruntime.SerializerOptions{ //nolint:unused 24 | Yaml: false, 25 | Pretty: false, 26 | Strict: false, 27 | } 28 | ) 29 | 30 | func init() { 31 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 32 | utilruntime.Must(gwv1alpha2.AddToScheme(scheme)) 33 | } 34 | 35 | type Marshaler struct{} 36 | 37 | func NewMarshaler() *Marshaler { 38 | return &Marshaler{} 39 | } 40 | 41 | func (m *Marshaler) UnmarshalRoute(data []byte) (store.Route, error) { 42 | route := &K8sRoute{} 43 | if err := json.Unmarshal(data, route); err != nil { 44 | return nil, err 45 | } 46 | return route, nil 47 | } 48 | 49 | func (m *Marshaler) MarshalRoute(route store.Route) ([]byte, error) { 50 | return json.Marshal(route) 51 | } 52 | 53 | func (m *Marshaler) UnmarshalGateway(data []byte) (store.Gateway, error) { 54 | gateway := &K8sGateway{} 55 | if err := json.Unmarshal(data, gateway); err != nil { 56 | return nil, err 57 | } 58 | return gateway, nil 59 | } 60 | 61 | func (m *Marshaler) MarshalGateway(gateway store.Gateway) ([]byte, error) { 62 | return json.Marshal(gateway) 63 | } 64 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/marshaler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/utils/pointer" 13 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 14 | 15 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/state" 16 | "github.com/hashicorp/consul-api-gateway/pkg/apis/v1alpha1" 17 | ) 18 | 19 | func TestMarshalRoute(t *testing.T) { 20 | r := &gwv1beta1.HTTPRoute{ 21 | Spec: gwv1beta1.HTTPRouteSpec{ 22 | Rules: []gwv1beta1.HTTPRouteRule{{ 23 | Filters: []gwv1beta1.HTTPRouteFilter{ 24 | { 25 | Type: gwv1beta1.HTTPRouteFilterURLRewrite, 26 | URLRewrite: &gwv1beta1.HTTPURLRewriteFilter{ 27 | Path: &gwv1beta1.HTTPPathModifier{ 28 | Type: gwv1beta1.PrefixMatchHTTPPathModifier, 29 | ReplacePrefixMatch: pointer.String("/api/v1"), 30 | }, 31 | }, 32 | }, 33 | }}, 34 | }, 35 | }, 36 | } 37 | r.SetGroupVersionKind(schema.GroupVersionKind{ 38 | Kind: "HTTPRoute", 39 | }) 40 | 41 | route := newK8sRoute(r, state.NewRouteState()) 42 | 43 | data, err := NewMarshaler().MarshalRoute(route) 44 | require.NoError(t, err) 45 | require.NotEmpty(t, data) 46 | 47 | unmarshaled, err := NewMarshaler().UnmarshalRoute(data) 48 | require.NoError(t, err) 49 | require.NotNil(t, unmarshaled) 50 | 51 | route, ok := unmarshaled.(*K8sRoute) 52 | require.True(t, ok) 53 | assert.NotNil(t, route) 54 | } 55 | 56 | func TestMarshalGateway(t *testing.T) { 57 | g := &gwv1beta1.Gateway{} 58 | 59 | gcc := v1alpha1.GatewayClassConfig{ 60 | Spec: v1alpha1.GatewayClassConfigSpec{ 61 | ConnectionManagement: v1alpha1.ConnectionManagementSpec{ 62 | MaxConnections: pointer.Int32(4096)}}, 63 | } 64 | 65 | gateway := newK8sGateway(gcc, g, state.InitialGatewayState(g)) 66 | 67 | data, err := NewMarshaler().MarshalGateway(gateway) 68 | require.NoError(t, err) 69 | assert.NotEmpty(t, data) 70 | 71 | unmarshaled, err := NewMarshaler().UnmarshalGateway(data) 72 | require.NoError(t, err) 73 | require.NotNil(t, unmarshaled) 74 | 75 | gateway, ok := unmarshaled.(*K8sGateway) 76 | require.True(t, ok) 77 | require.NotNil(t, gateway) 78 | require.NotNil(t, gateway.Config.Spec.ConnectionManagement.MaxConnections) 79 | assert.EqualValues(t, 4096, *gateway.Config.Spec.ConnectionManagement.MaxConnections) 80 | } 81 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/mocks/tracker.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./tracker.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | v1 "k8s.io/api/core/v1" 12 | v10 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | types "k8s.io/apimachinery/pkg/types" 14 | ) 15 | 16 | // MockGatewayStatusTracker is a mock of GatewayStatusTracker interface. 17 | type MockGatewayStatusTracker struct { 18 | ctrl *gomock.Controller 19 | recorder *MockGatewayStatusTrackerMockRecorder 20 | } 21 | 22 | // MockGatewayStatusTrackerMockRecorder is the mock recorder for MockGatewayStatusTracker. 23 | type MockGatewayStatusTrackerMockRecorder struct { 24 | mock *MockGatewayStatusTracker 25 | } 26 | 27 | // NewMockGatewayStatusTracker creates a new mock instance. 28 | func NewMockGatewayStatusTracker(ctrl *gomock.Controller) *MockGatewayStatusTracker { 29 | mock := &MockGatewayStatusTracker{ctrl: ctrl} 30 | mock.recorder = &MockGatewayStatusTrackerMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockGatewayStatusTracker) EXPECT() *MockGatewayStatusTrackerMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // DeleteStatus mocks base method. 40 | func (m *MockGatewayStatusTracker) DeleteStatus(name types.NamespacedName) { 41 | m.ctrl.T.Helper() 42 | m.ctrl.Call(m, "DeleteStatus", name) 43 | } 44 | 45 | // DeleteStatus indicates an expected call of DeleteStatus. 46 | func (mr *MockGatewayStatusTrackerMockRecorder) DeleteStatus(name interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStatus", reflect.TypeOf((*MockGatewayStatusTracker)(nil).DeleteStatus), name) 49 | } 50 | 51 | // UpdateStatus mocks base method. 52 | func (m *MockGatewayStatusTracker) UpdateStatus(name types.NamespacedName, pod *v1.Pod, conditions []v10.Condition, force bool, cb func() error) error { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "UpdateStatus", name, pod, conditions, force, cb) 55 | ret0, _ := ret[0].(error) 56 | return ret0 57 | } 58 | 59 | // UpdateStatus indicates an expected call of UpdateStatus. 60 | func (mr *MockGatewayStatusTrackerMockRecorder) UpdateStatus(name, pod, conditions, force, cb interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockGatewayStatusTracker)(nil).UpdateStatus), name, pod, conditions, force, cb) 63 | } 64 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/state/route.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package state 5 | 6 | import ( 7 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 8 | 9 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/common" 10 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/status" 11 | "github.com/hashicorp/consul-api-gateway/internal/k8s/service" 12 | ) 13 | 14 | // RouteState holds ephemeral state for routes 15 | type RouteState struct { 16 | References service.RouteRuleReferenceMap 17 | ResolutionErrors *service.ResolutionErrors 18 | ParentStatuses status.RouteStatuses 19 | } 20 | 21 | func NewRouteState() *RouteState { 22 | return &RouteState{ 23 | References: make(service.RouteRuleReferenceMap), 24 | ResolutionErrors: service.NewResolutionErrors(), 25 | ParentStatuses: make(status.RouteStatuses), 26 | } 27 | } 28 | 29 | func (r *RouteState) BindFailed(err error, ref gwv1alpha2.ParentReference) { 30 | r.ParentStatuses.BindFailed(r.ResolutionErrors, err, common.AsJSON(ref)) 31 | } 32 | 33 | func (r *RouteState) Bound(ref gwv1alpha2.ParentReference) { 34 | r.ParentStatuses.Bound(common.AsJSON(ref)) 35 | } 36 | 37 | func (r *RouteState) Remove(ref gwv1alpha2.ParentReference) { 38 | r.ParentStatuses.Remove(common.AsJSON(ref)) 39 | } 40 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/status/equality.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package status 5 | 6 | import ( 7 | "reflect" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 11 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 12 | 13 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/common" 14 | ) 15 | 16 | func conditionEqual(a, b metav1.Condition) bool { 17 | if a.Type != b.Type || 18 | a.ObservedGeneration != b.ObservedGeneration || 19 | a.Status != b.Status || 20 | a.Reason != b.Reason || 21 | a.Message != b.Message { 22 | return false 23 | } 24 | return true 25 | } 26 | 27 | func ConditionsEqual(a, b []metav1.Condition) bool { 28 | if len(a) != len(b) { 29 | // we have a different number of conditions, so they aren't the same 30 | return false 31 | } 32 | 33 | for i, newCondition := range a { 34 | if !conditionEqual(newCondition, b[i]) { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | func listenerStatusEqual(a, b gwv1beta1.ListenerStatus) bool { 42 | if a.Name != b.Name { 43 | return false 44 | } 45 | if !reflect.DeepEqual(a.SupportedKinds, b.SupportedKinds) { 46 | return false 47 | } 48 | if a.AttachedRoutes != b.AttachedRoutes { 49 | return false 50 | } 51 | return ConditionsEqual(a.Conditions, b.Conditions) 52 | } 53 | 54 | func ListenerStatusesEqual(a, b []gwv1beta1.ListenerStatus) bool { 55 | if len(a) != len(b) { 56 | // we have a different number of conditions, so they aren't the same 57 | return false 58 | } 59 | for i, newStatus := range a { 60 | if !listenerStatusEqual(newStatus, b[i]) { 61 | return false 62 | } 63 | } 64 | return true 65 | } 66 | 67 | func parentStatusEqual(a, b gwv1alpha2.RouteParentStatus) bool { 68 | if a.ControllerName != b.ControllerName { 69 | return false 70 | } 71 | if common.AsJSON(a.ParentRef) != common.AsJSON(b.ParentRef) { 72 | return false 73 | } 74 | 75 | return ConditionsEqual(a.Conditions, b.Conditions) 76 | } 77 | 78 | func RouteStatusEqual(a, b gwv1alpha2.RouteStatus) bool { 79 | if len(a.Parents) != len(b.Parents) { 80 | return false 81 | } 82 | 83 | for i, oldParent := range a.Parents { 84 | if !parentStatusEqual(oldParent, b.Parents[i]) { 85 | return false 86 | } 87 | } 88 | return true 89 | } 90 | 91 | func GatewayStatusEqual(a, b gwv1beta1.GatewayStatus) bool { 92 | if !ConditionsEqual(a.Conditions, b.Conditions) { 93 | return false 94 | } 95 | 96 | if !ListenerStatusesEqual(a.Listeners, b.Listeners) { 97 | return false 98 | } 99 | 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/statuses.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | 13 | "github.com/hashicorp/consul-api-gateway/internal/k8s/gatewayclient" 14 | "github.com/hashicorp/consul-api-gateway/internal/k8s/reconciler/status" 15 | "github.com/hashicorp/consul-api-gateway/internal/store" 16 | ) 17 | 18 | var _ store.StatusUpdater = (*StatusUpdater)(nil) 19 | 20 | type StatusUpdater struct { 21 | logger hclog.Logger 22 | client gatewayclient.Client 23 | deployer *GatewayDeployer 24 | controllerName string 25 | } 26 | 27 | func NewStatusUpdater(logger hclog.Logger, client gatewayclient.Client, deployer *GatewayDeployer, controllerName string) *StatusUpdater { 28 | return &StatusUpdater{ 29 | logger: logger, 30 | client: client, 31 | deployer: deployer, 32 | controllerName: controllerName, 33 | } 34 | } 35 | 36 | func (s *StatusUpdater) UpdateGatewayStatusOnSync(ctx context.Context, gateway store.Gateway, sync func() (bool, error)) error { 37 | g := gateway.(*K8sGateway) 38 | 39 | // we've done all but synced our state, so ensure our deployments are up-to-date 40 | if err := s.deployer.Deploy(ctx, g); err != nil { 41 | return err 42 | } 43 | 44 | didSync, err := sync() 45 | if err != nil { 46 | g.GatewayState.Status.InSync.SyncError = err 47 | } else if didSync { 48 | // clear out any old synchronization error statuses 49 | g.GatewayState.Status.InSync = status.GatewayInSyncStatus{} 50 | } 51 | 52 | gatewayStatus := g.GatewayState.GetStatus(g.Gateway) 53 | if !status.GatewayStatusEqual(gatewayStatus, g.Gateway.Status) { 54 | g.Gateway.Status = gatewayStatus 55 | if s.logger.IsTrace() { 56 | data, err := json.MarshalIndent(gatewayStatus, "", " ") 57 | if err == nil { 58 | s.logger.Trace("setting gateway status", "status", string(data)) 59 | } 60 | } 61 | if err := s.client.UpdateStatus(ctx, g.Gateway); err != nil { 62 | // make sure we return an error immediately that's unwrapped 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func (s *StatusUpdater) UpdateRouteStatus(ctx context.Context, route store.Route) error { 70 | r := route.(*K8sRoute) 71 | if status, ok := r.RouteState.ParentStatuses.NeedsUpdate(r.routeStatus(), s.controllerName, r.GetGeneration()); ok { 72 | r.setStatus(status) 73 | 74 | if s.logger.IsTrace() { 75 | status, err := json.MarshalIndent(status, "", " ") 76 | if err == nil { 77 | s.logger.Trace("syncing route status", "status", string(status)) 78 | } 79 | } 80 | if err := s.client.UpdateStatus(ctx, r.Route); err != nil { 81 | return fmt.Errorf("error updating route status: %w", err) 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "strings" 8 | 9 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 10 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 11 | ) 12 | 13 | func routeMatchesListenerHostname(listenerHostname *gwv1beta1.Hostname, hostnames []gwv1alpha2.Hostname) bool { 14 | if listenerHostname == nil || len(hostnames) == 0 { 15 | return true 16 | } 17 | 18 | for _, name := range hostnames { 19 | if hostnamesMatch(name, *listenerHostname) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func hostnamesMatch(a gwv1alpha2.Hostname, b gwv1beta1.Hostname) bool { 27 | if a == "" || a == "*" || b == "" || b == "*" { 28 | // any wildcard always matches 29 | return true 30 | } 31 | 32 | if strings.HasPrefix(string(a), "*.") || strings.HasPrefix(string(b), "*.") { 33 | aLabels, bLabels := strings.Split(string(a), "."), strings.Split(string(b), ".") 34 | if len(aLabels) != len(bLabels) { 35 | return false 36 | } 37 | 38 | for i := 1; i < len(aLabels); i++ { 39 | if !strings.EqualFold(aLabels[i], bLabels[i]) { 40 | return false 41 | } 42 | } 43 | return true 44 | } 45 | 46 | return string(a) == string(b) 47 | } 48 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package reconciler 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 11 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 12 | ) 13 | 14 | func TestRouteMatchesListenerHostname(t *testing.T) { 15 | t.Parallel() 16 | 17 | hostname := gwv1beta1.Hostname("name") 18 | require.True(t, routeMatchesListenerHostname(nil, nil)) 19 | require.True(t, routeMatchesListenerHostname(&hostname, nil)) 20 | require.True(t, routeMatchesListenerHostname(&hostname, []gwv1alpha2.Hostname{"*"})) 21 | require.False(t, routeMatchesListenerHostname(&hostname, []gwv1alpha2.Hostname{"other"})) 22 | } 23 | 24 | func TestHostnamesMatch(t *testing.T) { 25 | t.Parallel() 26 | 27 | require.True(t, hostnamesMatch("*", "*")) 28 | require.True(t, hostnamesMatch("", "*")) 29 | require.True(t, hostnamesMatch("*", "")) 30 | require.True(t, hostnamesMatch("", "")) 31 | require.True(t, hostnamesMatch("*.test", "*.test")) 32 | require.True(t, hostnamesMatch("a.test", "*.test")) 33 | require.True(t, hostnamesMatch("*.test", "a.test")) 34 | require.False(t, hostnamesMatch("*.test", "a.b.test")) 35 | require.False(t, hostnamesMatch("a.b.test", "*.test")) 36 | require.True(t, hostnamesMatch("a.b.test", "*.b.test")) 37 | require.True(t, hostnamesMatch("*.b.test", "a.b.test")) 38 | require.False(t, hostnamesMatch("*.b.test", "a.c.test")) 39 | require.True(t, hostnamesMatch("a.b.test", "a.b.test")) 40 | } 41 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/validator/tls.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validator 5 | 6 | const ( 7 | annotationKeyPrefix = "api-gateway.consul.hashicorp.com/" 8 | tlsMinVersionAnnotationKey = annotationKeyPrefix + "tls_min_version" 9 | tlsMaxVersionAnnotationKey = annotationKeyPrefix + "tls_max_version" 10 | tlsCipherSuitesAnnotationKey = annotationKeyPrefix + "tls_cipher_suites" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/k8s/reconciler/validator/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package validator 5 | 6 | import ( 7 | "context" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 12 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 13 | 14 | "github.com/hashicorp/consul-api-gateway/internal/k8s/gatewayclient" 15 | ) 16 | 17 | // referenceAllowed checks to see if a reference between resources is allowed. 18 | // In particular, references from one namespace to a resource in a different namespace 19 | // require an applicable ReferenceGrant be found in the namespace containing the resource 20 | // being referred to. 21 | // 22 | // For example, a Gateway in namespace "foo" may only reference a Secret in namespace "bar" 23 | // if a ReferenceGrant in namespace "bar" allows references from namespace "foo". 24 | func referenceAllowed(ctx context.Context, fromGK metav1.GroupKind, fromNamespace string, toGK metav1.GroupKind, toNamespace, toName string, c gatewayclient.Client) (bool, error) { 25 | // Reference does not cross namespaces 26 | if toNamespace == "" || toNamespace == fromNamespace { 27 | return true, nil 28 | } 29 | 30 | // Fetch all ReferenceGrants in the referenced namespace 31 | refGrants, err := c.GetReferenceGrantsInNamespace(ctx, toNamespace) 32 | if err != nil || len(refGrants) == 0 { 33 | return false, err 34 | } 35 | 36 | for _, refGrant := range refGrants { 37 | // Check for a From that applies 38 | fromMatch := false 39 | for _, from := range refGrant.Spec.From { 40 | if fromGK.Group == string(from.Group) && fromGK.Kind == string(from.Kind) && fromNamespace == string(from.Namespace) { 41 | fromMatch = true 42 | break 43 | } 44 | } 45 | 46 | if !fromMatch { 47 | continue 48 | } 49 | 50 | // Check for a To that applies 51 | for _, to := range refGrant.Spec.To { 52 | if toGK.Group == string(to.Group) && toGK.Kind == string(to.Kind) { 53 | if to.Name == nil || *to.Name == "" { 54 | // No name specified is treated as a wildcard within the namespace 55 | return true, nil 56 | } 57 | 58 | if gwv1alpha2.ObjectName(toName) == *to.Name { 59 | // The ReferenceGrant specifically targets this object 60 | return true, nil 61 | } 62 | } 63 | } 64 | } 65 | 66 | // No ReferenceGrant was found which allows this cross-namespace reference 67 | return false, nil 68 | } 69 | 70 | type gwObjectName interface { 71 | gwv1beta1.ObjectName | gwv1alpha2.ObjectName 72 | } 73 | 74 | type gwNamespace interface { 75 | gwv1beta1.Namespace | gwv1alpha2.Namespace 76 | } 77 | 78 | // getNamespacedName returns types.NamespacedName defaulted to a parent 79 | // namespace in the case where the provided namespace is nil. 80 | func getNamespacedName[O gwObjectName, N gwNamespace](name O, namespace *N, parentNamespace string) types.NamespacedName { 81 | if namespace != nil { 82 | return types.NamespacedName{Namespace: string(*namespace), Name: string(name)} 83 | } 84 | return types.NamespacedName{Namespace: parentNamespace, Name: string(name)} 85 | } 86 | -------------------------------------------------------------------------------- /internal/k8s/service/mocks/resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./resolver.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | service "github.com/hashicorp/consul-api-gateway/internal/k8s/service" 13 | v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 14 | ) 15 | 16 | // MockBackendResolver is a mock of BackendResolver interface. 17 | type MockBackendResolver struct { 18 | ctrl *gomock.Controller 19 | recorder *MockBackendResolverMockRecorder 20 | } 21 | 22 | // MockBackendResolverMockRecorder is the mock recorder for MockBackendResolver. 23 | type MockBackendResolverMockRecorder struct { 24 | mock *MockBackendResolver 25 | } 26 | 27 | // NewMockBackendResolver creates a new mock instance. 28 | func NewMockBackendResolver(ctrl *gomock.Controller) *MockBackendResolver { 29 | mock := &MockBackendResolver{ctrl: ctrl} 30 | mock.recorder = &MockBackendResolverMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockBackendResolver) EXPECT() *MockBackendResolverMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Resolve mocks base method. 40 | func (m *MockBackendResolver) Resolve(ctx context.Context, namespace string, ref v1alpha2.BackendObjectReference) (*service.ResolvedReference, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Resolve", ctx, namespace, ref) 43 | ret0, _ := ret[0].(*service.ResolvedReference) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // Resolve indicates an expected call of Resolve. 49 | func (mr *MockBackendResolverMockRecorder) Resolve(ctx, namespace, ref interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockBackendResolver)(nil).Resolve), ctx, namespace, ref) 52 | } 53 | -------------------------------------------------------------------------------- /internal/k8s/service/rule.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package service 5 | 6 | import ( 7 | "encoding/json" 8 | 9 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 10 | ) 11 | 12 | type RouteRule struct { 13 | HTTPRule *gwv1alpha2.HTTPRouteRule 14 | TCPRule *gwv1alpha2.TCPRouteRule 15 | } 16 | 17 | func NewRouteRule(rule interface{}) RouteRule { 18 | r := RouteRule{} 19 | switch routeRule := rule.(type) { 20 | case *gwv1alpha2.HTTPRouteRule: 21 | r.HTTPRule = routeRule 22 | case *gwv1alpha2.TCPRouteRule: 23 | r.TCPRule = routeRule 24 | } 25 | return r 26 | } 27 | 28 | type RouteRuleReferenceMap map[RouteRule][]ResolvedReference 29 | 30 | func (r RouteRuleReferenceMap) Add(rule RouteRule, resolved ResolvedReference) { 31 | refs, found := r[rule] 32 | if found { 33 | r[rule] = append(refs, resolved) 34 | return 35 | } 36 | r[rule] = []ResolvedReference{resolved} 37 | } 38 | 39 | func (r RouteRuleReferenceMap) MarshalJSON() ([]byte, error) { 40 | data := map[string][]ResolvedReference{} 41 | 42 | for k, v := range r { 43 | key, err := json.Marshal(k) 44 | if err != nil { 45 | return nil, err 46 | } 47 | data[string(key)] = v 48 | } 49 | return json.Marshal(data) 50 | } 51 | 52 | func (r *RouteRuleReferenceMap) UnmarshalJSON(b []byte) error { 53 | *r = map[RouteRule][]ResolvedReference{} 54 | data := map[string][]ResolvedReference{} 55 | if err := json.Unmarshal(b, &data); err != nil { 56 | return err 57 | } 58 | for k, v := range data { 59 | rule := RouteRule{} 60 | if err := json.Unmarshal([]byte(k), &rule); err != nil { 61 | return err 62 | } 63 | (*r)[rule] = v 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/k8s/utils/cert_file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | core "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/runtime" 13 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 14 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 15 | "k8s.io/client-go/rest" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | ) 18 | 19 | var ( 20 | scheme = runtime.NewScheme() 21 | ) 22 | 23 | func init() { 24 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 25 | } 26 | 27 | // WriteSecretCertFile retrieves a consul CA file stored in a K8s secret 28 | func WriteSecretCertFile(restConfig *rest.Config, secret, file, namespace string) error { 29 | k8sClient, err := client.New(restConfig, client.Options{ 30 | Scheme: scheme, 31 | }) 32 | if err != nil { 33 | return fmt.Errorf("failed to get k8s client: %w", err) 34 | } 35 | found := &core.Secret{} 36 | err = k8sClient.Get(context.Background(), client.ObjectKey{ 37 | Namespace: namespace, 38 | Name: secret, 39 | }, found) 40 | if err != nil { 41 | return fmt.Errorf("unable to pull Consul CA cert from secret: %w", err) 42 | } 43 | cert := found.Data[core.TLSCertKey] 44 | if err := os.WriteFile(file, cert, 0444); err != nil { 45 | return fmt.Errorf("unable to write CA cert: %w", err) 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/k8s/utils/consul.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 8 | ) 9 | 10 | func ProtocolToConsul(protocolType gwv1beta1.ProtocolType) (proto string, tls bool) { 11 | switch protocolType { 12 | case gwv1beta1.TCPProtocolType, gwv1beta1.TLSProtocolType: 13 | return "tcp", protocolType == gwv1beta1.TLSProtocolType 14 | case gwv1beta1.HTTPProtocolType, gwv1beta1.HTTPSProtocolType: 15 | return "http", protocolType == gwv1beta1.HTTPSProtocolType 16 | case gwv1beta1.UDPProtocolType: 17 | return "", false // unsupported 18 | default: 19 | return "", false // unknown/unsupported 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/k8s/utils/consul_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 11 | ) 12 | 13 | func TestProtocolToConsul(t *testing.T) { 14 | t.Parallel() 15 | 16 | proto, tls := ProtocolToConsul(gwv1beta1.TCPProtocolType) 17 | require.Equal(t, proto, "tcp") 18 | require.False(t, tls) 19 | 20 | proto, tls = ProtocolToConsul(gwv1beta1.TLSProtocolType) 21 | require.Equal(t, proto, "tcp") 22 | require.True(t, tls) 23 | 24 | proto, tls = ProtocolToConsul(gwv1beta1.HTTPProtocolType) 25 | require.Equal(t, proto, "http") 26 | require.False(t, tls) 27 | 28 | proto, tls = ProtocolToConsul(gwv1beta1.HTTPSProtocolType) 29 | require.Equal(t, proto, "http") 30 | require.True(t, tls) 31 | 32 | proto, tls = ProtocolToConsul(gwv1beta1.UDPProtocolType) 33 | require.Equal(t, proto, "") 34 | require.False(t, tls) 35 | 36 | proto, tls = ProtocolToConsul(gwv1beta1.ProtocolType("unknown")) 37 | require.Equal(t, proto, "") 38 | require.False(t, tls) 39 | } 40 | -------------------------------------------------------------------------------- /internal/k8s/utils/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/types" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | func NamespacedName(o client.Object) types.NamespacedName { 12 | return types.NamespacedName{ 13 | Namespace: o.GetNamespace(), 14 | Name: o.GetName(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/k8s/utils/helpers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | core "k8s.io/api/core/v1" 11 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func TestNamespacedName(t *testing.T) { 15 | t.Parallel() 16 | 17 | namespacedName := NamespacedName(&core.Pod{ 18 | ObjectMeta: meta.ObjectMeta{ 19 | Name: "pod", 20 | Namespace: "default", 21 | }, 22 | }) 23 | require.Equal(t, "pod", namespacedName.Name) 24 | require.Equal(t, "default", namespacedName.Namespace) 25 | } 26 | -------------------------------------------------------------------------------- /internal/k8s/utils/labels.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 12 | ) 13 | 14 | const ( 15 | ManagedLabel = "api-gateway.consul.hashicorp.com/managed" 16 | nameLabel = "api-gateway.consul.hashicorp.com/name" 17 | namespaceLabel = "api-gateway.consul.hashicorp.com/namespace" 18 | createdAtLabel = "api-gateway.consul.hashicorp.com/created" 19 | ) 20 | 21 | func LabelsForGateway(gw *gwv1beta1.Gateway) map[string]string { 22 | return map[string]string{ 23 | nameLabel: gw.Name, 24 | namespaceLabel: gw.Namespace, 25 | createdAtLabel: fmt.Sprintf("%d", gw.CreationTimestamp.Unix()), 26 | ManagedLabel: "true", 27 | } 28 | } 29 | 30 | func GatewayByLabels(object client.Object) types.NamespacedName { 31 | labels := object.GetLabels() 32 | return types.NamespacedName{ 33 | Name: labels[nameLabel], 34 | Namespace: labels[namespaceLabel], 35 | } 36 | } 37 | 38 | func IsManagedGateway(labels map[string]string) (string, bool) { 39 | managed, ok := labels[ManagedLabel] 40 | 41 | if !ok || managed != "true" { 42 | return "", false 43 | } 44 | name, ok := labels[nameLabel] 45 | if !ok { 46 | return "", false 47 | } 48 | return name, true 49 | } 50 | -------------------------------------------------------------------------------- /internal/k8s/utils/labels_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 12 | ) 13 | 14 | func TestIsManagedGateway(t *testing.T) { 15 | t.Parallel() 16 | 17 | for _, test := range []struct { 18 | name string 19 | labels map[string]string 20 | expected bool 21 | gateway string 22 | }{{ 23 | name: "unmanaged", 24 | labels: map[string]string{ 25 | nameLabel: "unmanaged", 26 | }, 27 | expected: false, 28 | }, { 29 | name: "unnamed", 30 | labels: map[string]string{ 31 | ManagedLabel: "true", 32 | }, 33 | expected: false, 34 | }, { 35 | name: "valid", 36 | labels: map[string]string{ 37 | ManagedLabel: "true", 38 | nameLabel: "test-gateway", 39 | }, 40 | expected: true, 41 | gateway: "test-gateway", 42 | }} { 43 | t.Run(test.name, func(t *testing.T) { 44 | actualGateway, actual := IsManagedGateway(test.labels) 45 | require.Equal(t, test.expected, actual) 46 | require.Equal(t, test.gateway, actualGateway) 47 | }) 48 | } 49 | } 50 | 51 | func TestLabelsForGateway(t *testing.T) { 52 | t.Parallel() 53 | 54 | labels := LabelsForGateway(&gwv1beta1.Gateway{ 55 | ObjectMeta: meta.ObjectMeta{ 56 | Name: "gateway", 57 | Namespace: "default", 58 | }, 59 | }) 60 | require.Equal(t, map[string]string{ 61 | nameLabel: "gateway", 62 | namespaceLabel: "default", 63 | ManagedLabel: "true", 64 | createdAtLabel: "-62135596800", 65 | }, labels) 66 | } 67 | -------------------------------------------------------------------------------- /internal/k8s/utils/reference.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/types" 8 | gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 9 | gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 10 | ) 11 | 12 | var ( 13 | gatewayGroup = gwv1alpha2.Group(gwv1beta1.GroupName) 14 | gatewayKind = gwv1alpha2.Kind("Gateway") 15 | ) 16 | 17 | func ReferencesGateway(namespace string, ref gwv1alpha2.ParentReference) (types.NamespacedName, bool) { 18 | if ref.Group != nil && *ref.Group != gatewayGroup { 19 | return types.NamespacedName{}, false 20 | } 21 | if ref.Kind != nil && *ref.Kind != gatewayKind { 22 | return types.NamespacedName{}, false 23 | } 24 | 25 | if ref.Namespace != nil { 26 | namespace = string(*ref.Namespace) 27 | } 28 | return types.NamespacedName{Name: string(ref.Name), Namespace: namespace}, true 29 | } 30 | -------------------------------------------------------------------------------- /internal/k8s/utils/secrets.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net/url" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | ErrNotK8sSecret = errors.New("not a kubernetes secret") 15 | ErrInvalidK8sSecret = errors.New("invalid kubernetes secret") 16 | ) 17 | 18 | const ( 19 | K8sSecretScheme = "k8s" 20 | ) 21 | 22 | // K8sSecret is a wrapper to a Kubernetes certificate secret 23 | type K8sSecret struct { 24 | Namespace string 25 | Name string 26 | } 27 | 28 | // NewK8sSecret returns an K8sSecret object 29 | func NewK8sSecret(namespace, name string) K8sSecret { 30 | return K8sSecret{ 31 | Namespace: namespace, 32 | Name: name, 33 | } 34 | } 35 | 36 | // ParseK8sSecret parses an encoded kubernetes secret. 37 | // If the encoded secret is not a Kubernetes secret or 38 | // it can't be parsed, it returns an error. 39 | func ParseK8sSecret(encoded string) (K8sSecret, error) { 40 | parsed, err := url.Parse(encoded) 41 | if err != nil { 42 | return K8sSecret{}, err 43 | } 44 | if parsed.Scheme != K8sSecretScheme { 45 | return K8sSecret{}, ErrNotK8sSecret 46 | } 47 | if !strings.HasPrefix(parsed.Path, "/") { 48 | return K8sSecret{}, ErrInvalidK8sSecret 49 | } 50 | path := strings.TrimPrefix(parsed.Path, "/") 51 | tokens := strings.SplitN(path, "/", 2) 52 | if len(tokens) != 2 { 53 | return K8sSecret{}, ErrInvalidK8sSecret 54 | } 55 | return NewK8sSecret(tokens[0], tokens[1]), nil 56 | } 57 | 58 | // String returns a kubernetes secret encoded as a string 59 | func (k K8sSecret) String() string { 60 | path := fmt.Sprintf("/%s/%s", k.Namespace, k.Name) 61 | return (&url.URL{ 62 | Scheme: K8sSecretScheme, 63 | Path: path, 64 | }).String() 65 | } 66 | -------------------------------------------------------------------------------- /internal/k8s/utils/versions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package utils 5 | 6 | import "strconv" 7 | 8 | func ResourceVersionGreater(a, b string) bool { 9 | aVal, err := strconv.Atoi(a) 10 | if err != nil { 11 | // a isn't numeric, return that b is greater 12 | return false 13 | } 14 | bVal, err := strconv.Atoi(b) 15 | if err != nil { 16 | // b isn't numeric, return that a is greater 17 | return true 18 | } 19 | return aVal > bVal 20 | } 21 | -------------------------------------------------------------------------------- /internal/metrics/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package metrics 5 | 6 | import ( 7 | "github.com/armon/go-metrics" 8 | "github.com/armon/go-metrics/prometheus" 9 | ) 10 | 11 | var ( 12 | SDSActiveStreams = []string{"sds_active_streams"} 13 | SDSCachedResources = []string{"sds_cached_resources"} 14 | SDSCertificateFetches = []string{"sds_certificate_fetches"} 15 | K8sGateways = []string{"k8s_gateways"} 16 | K8sNewGatewayDeployments = []string{"k8s_new_gateway_deployments"} 17 | ConsulLeafCertificateFetches = []string{"consul_leaf_certificate_fetches"} 18 | ) 19 | 20 | var Registry metrics.MetricSink 21 | 22 | func init() { 23 | sink, err := prometheus.NewPrometheusSinkFrom(prometheus.PrometheusOpts{ 24 | GaugeDefinitions: []prometheus.GaugeDefinition{{ 25 | Name: SDSActiveStreams, 26 | Help: "The total number of active SDS streams", 27 | }, { 28 | Name: SDSCachedResources, 29 | Help: "The total number of resources in the certificate cache", 30 | }, { 31 | Name: K8sGateways, 32 | Help: "The number of gateways the kubernetes controller is tracking", 33 | }}, 34 | CounterDefinitions: []prometheus.CounterDefinition{{ 35 | Name: SDSCertificateFetches, 36 | Help: "The total number of fetches per certificate segmented by fetcher", 37 | }, { 38 | Name: K8sNewGatewayDeployments, 39 | Help: "The number of gateways the kubernetes controller has deployed", 40 | }, { 41 | Name: ConsulLeafCertificateFetches, 42 | Help: "The number of times a leaf certificate has been fetched from Consul", 43 | }}, 44 | }) 45 | if err != nil { 46 | panic(err) 47 | } 48 | Registry = sink 49 | } 50 | -------------------------------------------------------------------------------- /internal/metrics/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package metrics 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | "sync" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | 15 | "github.com/hashicorp/go-hclog" 16 | ) 17 | 18 | // RunServer runs a prometheus metrics server 19 | func RunServer(ctx context.Context, logger hclog.Logger, address string) error { 20 | mux := http.NewServeMux() 21 | mux.Handle("/metrics", promhttp.Handler()) 22 | server := &http.Server{ 23 | Addr: address, 24 | Handler: mux, 25 | } 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(1) 29 | go func() { 30 | defer wg.Done() 31 | <-ctx.Done() 32 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 33 | defer cancel() 34 | if err := server.Shutdown(ctx); err != nil { 35 | // graceful shutdown failed, exit 36 | logger.Error("error shutting down metrics server", "error", err) 37 | } 38 | }() 39 | defer wg.Wait() 40 | 41 | if err := server.ListenAndServe(); err != nil { 42 | if !errors.Is(err, http.ErrServerClosed) { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/metrics/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package metrics 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | ) 15 | 16 | func TestServerShutdown(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 18 | defer cancel() 19 | 20 | errs := make(chan error, 1) 21 | go func() { 22 | errs <- RunServer(ctx, hclog.NewNullLogger(), "127.0.0.1:0") 23 | }() 24 | 25 | require.NoError(t, <-errs) 26 | } 27 | -------------------------------------------------------------------------------- /internal/profiling/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package profiling 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net/http" 10 | "net/http/pprof" 11 | "sync" 12 | "time" 13 | 14 | "github.com/hashicorp/go-hclog" 15 | ) 16 | 17 | // RunServer runs a server with pprof debugging endpoints 18 | func RunServer(ctx context.Context, logger hclog.Logger, address string) error { 19 | mux := http.NewServeMux() 20 | mux.HandleFunc("/debug/pprof/", pprof.Index) 21 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 22 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 23 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 24 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 25 | server := &http.Server{ 26 | Addr: address, 27 | Handler: mux, 28 | } 29 | 30 | var wg sync.WaitGroup 31 | wg.Add(1) 32 | go func() { 33 | defer wg.Done() 34 | <-ctx.Done() 35 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 36 | defer cancel() 37 | if err := server.Shutdown(ctx); err != nil { 38 | // graceful shutdown failed, exit 39 | logger.Error("error shutting down metrics server", "error", err) 40 | } 41 | }() 42 | defer wg.Wait() 43 | 44 | if err := server.ListenAndServe(); err != nil { 45 | if !errors.Is(err, http.ErrServerClosed) { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/profiling/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package profiling 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | ) 15 | 16 | func TestServerShutdown(t *testing.T) { 17 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) 18 | defer cancel() 19 | 20 | errs := make(chan error, 1) 21 | go func() { 22 | errs <- RunServer(ctx, hclog.NewNullLogger(), "127.0.0.1:0") 23 | }() 24 | 25 | require.NoError(t, <-errs) 26 | } 27 | -------------------------------------------------------------------------------- /internal/store/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package store 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/consul-api-gateway/internal/core" 10 | ) 11 | 12 | //go:generate mockgen -source ./interfaces.go -destination ./mocks/interfaces.go -package mocks StatusTrackingGateway,Gateway,StatusTrackingRoute,Route,Store 13 | 14 | type CompareResult int 15 | 16 | // Gateway describes a gateway. 17 | type Gateway interface { 18 | ID() core.GatewayID 19 | Resolve() core.ResolvedGateway 20 | CanFetchSecrets(secrets []string) (bool, error) 21 | } 22 | 23 | // Route should be implemented by all route 24 | // source integrations 25 | type Route interface { 26 | ID() string 27 | } 28 | 29 | type ReadStore interface { 30 | GetGateway(ctx context.Context, id core.GatewayID) (Gateway, error) 31 | ListGateways(ctx context.Context) ([]Gateway, error) 32 | 33 | GetRoute(ctx context.Context, id string) (Route, error) 34 | ListRoutes(ctx context.Context) ([]Route, error) 35 | } 36 | 37 | type WriteStore interface { 38 | UpsertGateway(ctx context.Context, gateway Gateway, updateConditionFn func(current Gateway) bool) error 39 | DeleteGateway(ctx context.Context, id core.GatewayID) error 40 | 41 | UpsertRoute(ctx context.Context, route Route, updateConditionFn func(current Route) bool) error 42 | DeleteRoute(ctx context.Context, id string) error 43 | 44 | SyncAllAtInterval(ctx context.Context) 45 | } 46 | 47 | // Store is used for persisting and querying gateways and routes 48 | type Store interface { 49 | ReadStore 50 | WriteStore 51 | } 52 | 53 | // Backend is used for persisting and querying gateways and routes 54 | type Backend interface { 55 | GetGateway(ctx context.Context, id core.GatewayID) ([]byte, error) 56 | ListGateways(ctx context.Context) ([][]byte, error) 57 | DeleteGateway(ctx context.Context, id core.GatewayID) error 58 | UpsertGateways(ctx context.Context, gateways ...GatewayRecord) error 59 | GetRoute(ctx context.Context, id string) ([]byte, error) 60 | ListRoutes(ctx context.Context) ([][]byte, error) 61 | DeleteRoute(ctx context.Context, id string) error 62 | UpsertRoutes(ctx context.Context, routes ...RouteRecord) error 63 | } 64 | 65 | type Marshaler interface { 66 | UnmarshalRoute(data []byte) (Route, error) 67 | MarshalRoute(Route) ([]byte, error) 68 | UnmarshalGateway(data []byte) (Gateway, error) 69 | MarshalGateway(Gateway) ([]byte, error) 70 | } 71 | 72 | type Binder interface { 73 | Bind(ctx context.Context, gateway Gateway, route Route) bool 74 | Unbind(ctx context.Context, gateway Gateway, route Route) bool 75 | } 76 | 77 | type StatusUpdater interface { 78 | UpdateGatewayStatusOnSync(ctx context.Context, gateway Gateway, sync func() (bool, error)) error 79 | UpdateRouteStatus(ctx context.Context, route Route) error 80 | } 81 | 82 | // GatewayRecord represents a serialized Gateway 83 | type GatewayRecord struct { 84 | ID core.GatewayID 85 | Data []byte 86 | } 87 | 88 | // RouteRecord represents a serialized Route 89 | type RouteRecord struct { 90 | ID string 91 | Data []byte 92 | } 93 | -------------------------------------------------------------------------------- /internal/store/memory_backend.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package store 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | 10 | "github.com/hashicorp/consul-api-gateway/internal/core" 11 | ) 12 | 13 | type memoryBackend struct { 14 | gateways map[core.GatewayID][]byte 15 | routes map[string][]byte 16 | 17 | mutex sync.RWMutex 18 | } 19 | 20 | var _ Backend = &memoryBackend{} 21 | 22 | func NewMemoryBackend() *memoryBackend { 23 | return &memoryBackend{ 24 | gateways: make(map[core.GatewayID][]byte), 25 | routes: make(map[string][]byte), 26 | } 27 | } 28 | 29 | func (b *memoryBackend) GetGateway(_ context.Context, id core.GatewayID) ([]byte, error) { 30 | b.mutex.RLock() 31 | defer b.mutex.RUnlock() 32 | 33 | if data, found := b.gateways[id]; found { 34 | return data, nil 35 | } 36 | return nil, ErrNotFound 37 | } 38 | 39 | func (b *memoryBackend) ListGateways(_ context.Context) ([][]byte, error) { 40 | b.mutex.RLock() 41 | defer b.mutex.RUnlock() 42 | 43 | gateways := make([][]byte, 0, len(b.gateways)) 44 | for _, data := range b.gateways { 45 | gateways = append(gateways, data) 46 | } 47 | return gateways, nil 48 | } 49 | 50 | func (b *memoryBackend) DeleteGateway(_ context.Context, id core.GatewayID) error { 51 | b.mutex.Lock() 52 | defer b.mutex.Unlock() 53 | 54 | delete(b.gateways, id) 55 | return nil 56 | } 57 | 58 | func (b *memoryBackend) UpsertGateways(_ context.Context, gateways ...GatewayRecord) error { 59 | b.mutex.Lock() 60 | defer b.mutex.Unlock() 61 | 62 | for _, gateway := range gateways { 63 | b.gateways[gateway.ID] = gateway.Data 64 | } 65 | return nil 66 | } 67 | 68 | func (b *memoryBackend) GetRoute(_ context.Context, id string) ([]byte, error) { 69 | b.mutex.RLock() 70 | defer b.mutex.RUnlock() 71 | 72 | if data, found := b.routes[id]; found { 73 | return data, nil 74 | } 75 | return nil, ErrNotFound 76 | } 77 | 78 | func (b *memoryBackend) ListRoutes(_ context.Context) ([][]byte, error) { 79 | b.mutex.RLock() 80 | defer b.mutex.RUnlock() 81 | 82 | routes := make([][]byte, 0, len(b.routes)) 83 | for _, data := range b.routes { 84 | routes = append(routes, data) 85 | } 86 | return routes, nil 87 | } 88 | 89 | func (b *memoryBackend) DeleteRoute(_ context.Context, id string) error { 90 | b.mutex.Lock() 91 | defer b.mutex.Unlock() 92 | 93 | delete(b.routes, id) 94 | return nil 95 | } 96 | 97 | func (b *memoryBackend) UpsertRoutes(_ context.Context, routes ...RouteRecord) error { 98 | b.mutex.Lock() 99 | defer b.mutex.Unlock() 100 | 101 | for _, route := range routes { 102 | b.routes[route.ID] = route.Data 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /internal/testing/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package testing 5 | 6 | import ( 7 | "bytes" 8 | "sync" 9 | ) 10 | 11 | type Buffer struct { 12 | buffer bytes.Buffer 13 | mutex sync.RWMutex 14 | } 15 | 16 | func (s *Buffer) Write(p []byte) (n int, err error) { 17 | s.mutex.Lock() 18 | defer s.mutex.Unlock() 19 | return s.buffer.Write(p) 20 | } 21 | 22 | func (s *Buffer) String() string { 23 | s.mutex.RLock() 24 | defer s.mutex.RUnlock() 25 | return s.buffer.String() 26 | } 27 | 28 | func (s *Buffer) Reset() { 29 | s.mutex.Lock() 30 | defer s.mutex.Unlock() 31 | s.buffer.Reset() 32 | } 33 | -------------------------------------------------------------------------------- /internal/testing/conformance/README.md: -------------------------------------------------------------------------------- 1 | # Conformance Testing 2 | 3 | The resources here facilitate the running of the conformance tests defined upstream in [kubernetes-sigs/gateway-api](https://github.com/kubernetes-sigs/gateway-api). 4 | 5 | ## Special Considerations 6 | 7 | The framework defines its own set of Kubernetes resources using kustomization yaml. These should generally work with any implementation; 8 | however, we currently have to make a few patches in order for things to work with Consul. Our goal long-term is to remove the 9 | need for these patches. 10 | 11 | - The `Deployments` defined upstream do not specify a `containerPort` in the `Pod` template. Consul relies on this `containerPort` 12 | when a `connect-service-port` annotation is not present. To make this work, we patch the `connect-service-port` annotation onto 13 | each `Deployment`'s template. They all use the same port. 14 | - The Consul services default to a protocol of `tcp`; however, the testing framework uses `http`. To make this work, we create 15 | a `ProxyDefaults` resource which sets the protocol to `http` globally. 16 | - GitHub Actions' default hosted runner is not powerful enough to run all pods specified upstream in kind. 17 | To cope with this, we reduce all `Deployments` to 1 replica. 18 | 19 | ## Status 20 | 21 | The conformance tests are run nightly in GitHub Actions using the workflow [here](/.github/workflows/conformance.yml). 22 | You may also run the workflow on demand from this repo's Actions tab, by following the `conformance/*` branch naming convention, or by adding the `pr/run-conformance` label to your pull request. 23 | -------------------------------------------------------------------------------- /internal/testing/conformance/consul-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | global: 5 | tls: 6 | enabled: true 7 | server: 8 | replicas: 1 9 | connectInject: 10 | enabled: true 11 | default: true 12 | # For consul-k8s >= 1.2.0, allow consul-api-gateway tests to install their own 13 | # CRDs just like they did before api-gateway was integrated into consul-k8s 14 | apiGateway: 15 | manageExternalCRDs: false 16 | controller: 17 | enabled: true 18 | apiGateway: 19 | enabled: true 20 | logLevel: info 21 | -------------------------------------------------------------------------------- /internal/testing/conformance/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This file contains the additional resources and the patches for existing resources necessary 5 | # to run the conformance tests from https://github.com/kubernetes-sigs/gateway-api against 6 | # Consul API Gateway. 7 | 8 | apiVersion: kustomize.config.k8s.io/v1beta1 9 | kind: Kustomization 10 | 11 | resources: 12 | - ./base/manifests.yaml 13 | - ./proxydefaults.yaml 14 | 15 | patches: 16 | # Add connect-inject annotation to each Deployment. This is required due to 17 | # containerPort not being defined on Deployments upstream. Though containerPort 18 | # is optional, Consul relies on it as a default value in the absence of a 19 | # connect-service-port annotation. 20 | - patch: |- 21 | - op: add 22 | path: "/spec/template/metadata/annotations" 23 | value: {'consul.hashicorp.com/connect-service-port': '3000'} 24 | target: 25 | kind: Deployment 26 | # We don't have enough resources in the GitHub-hosted Actions runner to support 2 replicas 27 | - patch: |- 28 | - op: replace 29 | path: "/spec/replicas" 30 | value: 1 31 | target: 32 | kind: Deployment 33 | -------------------------------------------------------------------------------- /internal/testing/conformance/metallb-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | namespace: metallb-system 8 | name: config 9 | data: 10 | config: | 11 | address-pools: 12 | - name: default 13 | protocol: layer2 14 | addresses: 15 | - 172.18.255.200-172.18.255.250 16 | -------------------------------------------------------------------------------- /internal/testing/conformance/proxydefaults.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: consul.hashicorp.com/v1alpha1 5 | kind: ProxyDefaults 6 | metadata: 7 | name: global 8 | namespace: consul 9 | spec: 10 | config: 11 | protocol: http 12 | -------------------------------------------------------------------------------- /internal/testing/e2e/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | // This package is meant for helper functions used in scaffolding end-to-end tests 7 | // due to the bulky nature of end-to-end tests, pretty much any test that leverages 8 | // this package should be run behind an "e2e" build tag, so that it can be skipped 9 | // easily and opted into during CI. 10 | -------------------------------------------------------------------------------- /internal/testing/e2e/docker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path" 13 | "strings" 14 | 15 | "github.com/docker/docker/api/types" 16 | "github.com/docker/docker/client" 17 | "github.com/docker/docker/pkg/archive" 18 | "github.com/google/uuid" 19 | "sigs.k8s.io/e2e-framework/pkg/envconf" 20 | ) 21 | 22 | type dockerImageContext struct{} 23 | 24 | var dockerImageContextKey = dockerImageContext{} 25 | 26 | const ( 27 | envvarExtraDockerImages = envvarPrefix + "DOCKER_IMAGES" 28 | ) 29 | 30 | func BuildDockerImage(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 31 | log.Print("Building docker image") 32 | 33 | tag := fmt.Sprintf("consul-api-gateway:%s", uuid.New().String()) 34 | dockerClient, err := client.NewClientWithOpts( 35 | client.FromEnv, 36 | ) 37 | if err != nil { 38 | return nil, err 39 | } 40 | tar, err := archive.TarWithOptions(path.Join("..", "..", ".."), &archive.TarOptions{}) 41 | if err != nil { 42 | return nil, err 43 | } 44 | r, err := dockerClient.ImageBuild(ctx, tar, types.ImageBuildOptions{ 45 | Dockerfile: "Dockerfile.local", 46 | Tags: []string{tag}, 47 | Remove: true, 48 | }) 49 | if err != nil { 50 | return nil, err 51 | } 52 | defer r.Body.Close() 53 | b, err := io.ReadAll(r.Body) 54 | if err != nil { 55 | return nil, err 56 | } 57 | log.Print(string(b)) 58 | 59 | return context.WithValue(ctx, dockerImageContextKey, tag), nil 60 | } 61 | 62 | func DockerImage(ctx context.Context) string { 63 | image := ctx.Value(dockerImageContextKey) 64 | if image == nil { 65 | panic("must run this with an integration test that has called BuildDockerImage") 66 | } 67 | return image.(string) 68 | } 69 | 70 | func ExtraDockerImages() []string { 71 | images := os.Getenv(envvarExtraDockerImages) 72 | if images != "" { 73 | return strings.Split(images, ",") 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/testing/e2e/environment.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "context" 8 | 9 | "sigs.k8s.io/e2e-framework/pkg/env" 10 | "sigs.k8s.io/e2e-framework/pkg/envconf" 11 | ) 12 | 13 | type namespaceContext struct{} 14 | type namespaceMirroringContext struct{} 15 | type clusterNameContext struct{} 16 | 17 | var namespaceContextKey = namespaceContext{} 18 | var namespaceMirroringContextKey = namespaceMirroringContext{} 19 | var clusterNameContextKey = clusterNameContext{} 20 | 21 | func SetNamespace(namespace string) env.Func { 22 | return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 23 | return context.WithValue(ctx, namespaceContextKey, namespace), nil 24 | } 25 | } 26 | 27 | func SetNamespaceMirroring(namespaceMirroring bool) env.Func { 28 | return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 29 | return context.WithValue(ctx, namespaceMirroringContextKey, namespaceMirroring), nil 30 | } 31 | } 32 | 33 | func Namespace(ctx context.Context) string { 34 | namespace := ctx.Value(namespaceContextKey) 35 | if namespace == nil { 36 | panic("must run this with an integration test that has called SetNamespace") 37 | } 38 | return namespace.(string) 39 | } 40 | 41 | func NamespaceMirroring(ctx context.Context) bool { 42 | namespaceMirroring := ctx.Value(namespaceMirroringContextKey) 43 | return namespaceMirroring.(bool) 44 | } 45 | 46 | func SetClusterName(name string) env.Func { 47 | return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 48 | return context.WithValue(ctx, clusterNameContextKey, name), nil 49 | } 50 | } 51 | 52 | func ClusterName(ctx context.Context) string { 53 | name := ctx.Value(clusterNameContextKey) 54 | if name == nil { 55 | panic("must run this with an integration test that has called SetClusterName") 56 | } 57 | return name.(string) 58 | } 59 | -------------------------------------------------------------------------------- /internal/testing/e2e/go.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "errors" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "path" 14 | "time" 15 | 16 | "sigs.k8s.io/e2e-framework/pkg/envconf" 17 | ) 18 | 19 | func CrossCompileProject(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 20 | log.Print("Cross compiling consul-api-gateway") 21 | 22 | var stdout, stderr bytes.Buffer 23 | timeoutContext, cancel := context.WithTimeout(ctx, 60*time.Second) 24 | defer cancel() 25 | 26 | rootProjectPath := path.Join("..", "..", "..") 27 | cmd := exec.CommandContext(timeoutContext, "go", "build", "-o", path.Join(rootProjectPath, "consul-api-gateway"), rootProjectPath) 28 | cmd.Env = append(os.Environ(), "GOOS=linux") 29 | cmd.Stderr = &stderr 30 | cmd.Stdout = &stdout 31 | if err := cmd.Run(); err != nil { 32 | return nil, errors.New(stderr.String()) 33 | } 34 | return ctx, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/testing/e2e/kustomize.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "os/exec" 10 | "time" 11 | 12 | api "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 13 | ) 14 | 15 | // TODO: switch this to krusty implementation after integration for conformance tests 16 | func kubectlKustomizeCRDs(ctx context.Context, path string) ([]*api.CustomResourceDefinition, error) { 17 | var stdout, stderr bytes.Buffer 18 | timeoutContext, cancel := context.WithTimeout(ctx, 10*time.Second) 19 | defer cancel() 20 | cmd := exec.CommandContext(timeoutContext, "kubectl", "kustomize", path) 21 | cmd.Stderr = &stderr 22 | cmd.Stdout = &stdout 23 | if err := cmd.Run(); err != nil { 24 | return nil, err 25 | } 26 | 27 | return readCRDs(stdout.Bytes()) 28 | } 29 | -------------------------------------------------------------------------------- /internal/testing/e2e/stack.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "path/filepath" 10 | 11 | "sigs.k8s.io/e2e-framework/pkg/env" 12 | "sigs.k8s.io/e2e-framework/pkg/envconf" 13 | "sigs.k8s.io/e2e-framework/pkg/envfuncs" 14 | ) 15 | 16 | const ( 17 | envvarPrefix = "E2E_APIGW_" 18 | ) 19 | 20 | func SetUpStack(hostRoute string) env.Func { 21 | return func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 22 | var err error 23 | kindClusterName := envconf.RandomName("consul-api-gateway-test", 30) 24 | namespace := envconf.RandomName("test", 16) 25 | 26 | // TODO: should this instead create a new context? 27 | 28 | ctx = SetHostRoute(ctx, hostRoute) 29 | 30 | for _, f := range []env.Func{ 31 | SetClusterName(kindClusterName), 32 | SetNamespace(namespace), 33 | SetNamespaceMirroring(true), 34 | CrossCompileProject, 35 | BuildDockerImage, 36 | CreateKindCluster(kindClusterName), 37 | LoadKindDockerImage(kindClusterName), 38 | envfuncs.CreateNamespace(namespace), 39 | InstallCRDs, 40 | CreateServiceAccount(namespace, "consul-server", getBasePath()+"/config/rbac/role.yaml"), 41 | CreateServiceAccount(namespace, "consul-api-gateway", getBasePath()+"/config/rbac/role.yaml"), 42 | CreateTestConsulContainer(kindClusterName, namespace), 43 | CreateConsulACLPolicy, 44 | CreateConsulAuthMethod(), 45 | SetConsulNamespace(nil), 46 | CreateTestGatewayServer(namespace), 47 | } { 48 | ctx, err = f(ctx, cfg) 49 | if err != nil { 50 | return nil, err 51 | } 52 | } 53 | 54 | return ctx, nil 55 | } 56 | } 57 | 58 | func TearDownStack(ctx context.Context, cfg *envconf.Config) (context.Context, error) { 59 | var err error 60 | namespace := Namespace(ctx) 61 | 62 | for _, f := range []env.Func{ 63 | DestroyTestGatewayServer, 64 | envfuncs.DeleteNamespace(namespace), 65 | envfuncs.DestroyKindCluster(ClusterName(ctx)), 66 | } { 67 | ctx, err = f(ctx, cfg) 68 | if err != nil { 69 | return nil, err 70 | } 71 | } 72 | return ctx, nil 73 | } 74 | 75 | type hostRouteContext struct{} 76 | 77 | var ( 78 | hostRouteContextKey = hostRouteContext{} 79 | ) 80 | 81 | func SetHostRoute(ctx context.Context, hostRoute string) context.Context { 82 | return context.WithValue(ctx, hostRouteContextKey, hostRoute) 83 | } 84 | 85 | func HostRoute(ctx context.Context) string { 86 | return ctx.Value(hostRouteContextKey).(string) 87 | } 88 | 89 | func getEnvDefault(envvar, defaultVal string) string { 90 | if val := os.Getenv(envvar); val != "" { 91 | return val 92 | } 93 | return defaultVal 94 | } 95 | 96 | func getBasePath() string { 97 | path, _ := filepath.Abs("../../.././") 98 | return path 99 | } 100 | -------------------------------------------------------------------------------- /internal/testing/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package testing 5 | 6 | import "math/rand" 7 | 8 | var letters = []rune("abcdefghijklmnopqrstuvwxyz") 9 | 10 | func RandomString() string { 11 | s := make([]rune, 10) 12 | for i := range s { 13 | s[i] = letters[rand.Intn(26)] 14 | } 15 | return string(s) 16 | } 17 | 18 | func StringPtr(val string) *string { 19 | return &val 20 | } 21 | -------------------------------------------------------------------------------- /internal/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build tools 5 | 6 | // following https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 7 | package internal 8 | 9 | import ( 10 | _ "github.com/deepmap/oapi-codegen/cmd/oapi-codegen" 11 | _ "github.com/golang/mock/mockgen" 12 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 13 | _ "github.com/hashicorp/go-changelog/cmd/changelog-build" 14 | _ "github.com/hashicorp/go-changelog/cmd/changelog-check" 15 | _ "github.com/hashicorp/go-changelog/cmd/changelog-entry" 16 | _ "golang.org/x/tools/cmd/goimports" 17 | _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" 18 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 19 | ) 20 | -------------------------------------------------------------------------------- /internal/vault/mocks/certificates.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./certificates.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | api "github.com/hashicorp/vault/api" 13 | ) 14 | 15 | // MockLogicalClient is a mock of LogicalClient interface. 16 | type MockLogicalClient struct { 17 | ctrl *gomock.Controller 18 | recorder *MockLogicalClientMockRecorder 19 | } 20 | 21 | // MockLogicalClientMockRecorder is the mock recorder for MockLogicalClient. 22 | type MockLogicalClientMockRecorder struct { 23 | mock *MockLogicalClient 24 | } 25 | 26 | // NewMockLogicalClient creates a new mock instance. 27 | func NewMockLogicalClient(ctrl *gomock.Controller) *MockLogicalClient { 28 | mock := &MockLogicalClient{ctrl: ctrl} 29 | mock.recorder = &MockLogicalClientMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockLogicalClient) EXPECT() *MockLogicalClientMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // WriteWithContext mocks base method. 39 | func (m *MockLogicalClient) WriteWithContext(arg0 context.Context, arg1 string, arg2 map[string]interface{}) (*api.Secret, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "WriteWithContext", arg0, arg1, arg2) 42 | ret0, _ := ret[0].(*api.Secret) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // WriteWithContext indicates an expected call of WriteWithContext. 48 | func (mr *MockLogicalClientMockRecorder) WriteWithContext(arg0, arg1, arg2 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteWithContext", reflect.TypeOf((*MockLogicalClient)(nil).WriteWithContext), arg0, arg1, arg2) 51 | } 52 | 53 | // MockKVClient is a mock of KVClient interface. 54 | type MockKVClient struct { 55 | ctrl *gomock.Controller 56 | recorder *MockKVClientMockRecorder 57 | } 58 | 59 | // MockKVClientMockRecorder is the mock recorder for MockKVClient. 60 | type MockKVClientMockRecorder struct { 61 | mock *MockKVClient 62 | } 63 | 64 | // NewMockKVClient creates a new mock instance. 65 | func NewMockKVClient(ctrl *gomock.Controller) *MockKVClient { 66 | mock := &MockKVClient{ctrl: ctrl} 67 | mock.recorder = &MockKVClientMockRecorder{mock} 68 | return mock 69 | } 70 | 71 | // EXPECT returns an object that allows the caller to indicate expected use. 72 | func (m *MockKVClient) EXPECT() *MockKVClientMockRecorder { 73 | return m.recorder 74 | } 75 | 76 | // Get mocks base method. 77 | func (m *MockKVClient) Get(arg0 context.Context, arg1 string) (*api.KVSecret, error) { 78 | m.ctrl.T.Helper() 79 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 80 | ret0, _ := ret[0].(*api.KVSecret) 81 | ret1, _ := ret[1].(error) 82 | return ret0, ret1 83 | } 84 | 85 | // Get indicates an expected call of Get. 86 | func (mr *MockKVClientMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 87 | mr.mock.ctrl.T.Helper() 88 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKVClient)(nil).Get), arg0, arg1) 89 | } 90 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | // The git commit that was compiled. These will be filled in by the compiler. 13 | GitCommit string 14 | GitDescribe string 15 | 16 | // The main version number that is being run at the moment. 17 | // 18 | // Version must conform to the format expected by 19 | // github.com/hashicorp/go-version for tests to work. 20 | Version = "0.6.0" 21 | 22 | // A pre-release marker for the version. If this is "" (empty string) 23 | // then it means that it is a final release. Otherwise, this is a pre-release 24 | // such as "dev" (in development), "beta", "rc1", etc. 25 | VersionPrerelease = "dev" 26 | ) 27 | 28 | // GetHumanVersion composes the parts of the version in a way that's suitable 29 | // for displaying to humans. 30 | func GetHumanVersion() string { 31 | version := Version 32 | if GitDescribe != "" { 33 | version = GitDescribe 34 | } 35 | 36 | release := VersionPrerelease 37 | if GitDescribe == "" && release == "" { 38 | release = "dev" 39 | } 40 | 41 | if release != "" { 42 | if !strings.HasSuffix(version, "-"+release) { 43 | // if we tagged a prerelease version then the release is in the version already 44 | version += fmt.Sprintf("-%s", release) 45 | } 46 | if GitCommit != "" { 47 | version += fmt.Sprintf(" (%s)", GitCommit) 48 | } 49 | } 50 | 51 | // Strip off any single quotes added by the git information. 52 | return strings.Replace(version, "'", "", -1) 53 | } 54 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package version 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestVersion(t *testing.T) { 13 | t.Parallel() 14 | 15 | require.NotEmpty(t, GetHumanVersion()) 16 | 17 | GitCommit = "1" 18 | require.Contains(t, GetHumanVersion(), "(1)") 19 | 20 | GitDescribe = "description" 21 | require.Contains(t, GetHumanVersion(), "description") 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "log" 10 | "os" 11 | 12 | "github.com/mitchellh/cli" 13 | 14 | cmdExec "github.com/hashicorp/consul-api-gateway/internal/commands/exec" 15 | cmdServer "github.com/hashicorp/consul-api-gateway/internal/commands/server" 16 | cmdVersion "github.com/hashicorp/consul-api-gateway/internal/commands/version" 17 | 18 | "github.com/hashicorp/consul-api-gateway/internal/version" 19 | ) 20 | 21 | func main() { 22 | ui := &cli.BasicUi{Writer: os.Stdout, ErrorWriter: os.Stderr} 23 | os.Exit(run(os.Args[1:], ui, os.Stdout)) 24 | } 25 | 26 | func run(args []string, ui cli.Ui, logOutput io.Writer) int { 27 | c := cli.NewCLI("consul-api-gateway", version.GetHumanVersion()) 28 | c.Args = args 29 | c.Commands = initializeCommands(ui, logOutput) 30 | c.HelpFunc = helpFunc(c.Commands) 31 | c.HelpWriter = logOutput 32 | 33 | exitStatus, err := c.Run() 34 | if err != nil { 35 | log.Println(err) 36 | } 37 | return exitStatus 38 | } 39 | 40 | func initializeCommands(ui cli.Ui, logOutput io.Writer) map[string]cli.CommandFactory { 41 | commands := map[string]cli.CommandFactory{ 42 | "server": func() (cli.Command, error) { 43 | return cmdServer.New(context.Background(), ui, logOutput), nil 44 | }, 45 | "exec": func() (cli.Command, error) { 46 | return cmdExec.New(context.Background(), ui, logOutput), nil 47 | }, 48 | "version": func() (cli.Command, error) { 49 | return &cmdVersion.Command{UI: ui, Version: version.GetHumanVersion()}, nil 50 | }, 51 | } 52 | 53 | return commands 54 | } 55 | 56 | func helpFunc(commands map[string]cli.CommandFactory) cli.HelpFunc { 57 | // This should be updated for any commands we want to hide for any reason. 58 | // Hidden commands can still be executed if you know the command, but 59 | // aren't shown in any help output. We use this for prerelease functionality 60 | // or advanced features. 61 | hidden := map[string]struct{}{ 62 | "exec": {}, 63 | "server": {}, 64 | } 65 | 66 | var include []string 67 | for k := range commands { 68 | if _, ok := hidden[k]; !ok { 69 | include = append(include, k) 70 | } 71 | } 72 | 73 | return cli.FilteredHelpFunc(include, cli.BasicHelpFunc("consul-api-gateway")) 74 | } 75 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | "github.com/mitchellh/cli" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMain(t *testing.T) { 15 | ui := cli.NewMockUi() 16 | var buffer bytes.Buffer 17 | 18 | require.Equal(t, 0, run([]string{ 19 | "server", "-h", 20 | }, ui, &buffer)) 21 | require.NotEmpty(t, buffer.String()) 22 | buffer.Reset() 23 | 24 | require.Equal(t, 0, run([]string{ 25 | "exec", "-h", 26 | }, ui, &buffer)) 27 | require.NotEmpty(t, buffer.String()) 28 | buffer.Reset() 29 | 30 | require.Equal(t, 0, run([]string{ 31 | "version", "-h", 32 | }, ui, &buffer)) 33 | require.NotEmpty(t, buffer.String()) 34 | buffer.Reset() 35 | 36 | require.Equal(t, 0, run([]string{ 37 | "-h", 38 | }, ui, &buffer)) 39 | require.NotEmpty(t, buffer.String()) 40 | buffer.Reset() 41 | } 42 | 43 | func TestHelpFilter(t *testing.T) { 44 | ui := cli.NewMockUi() 45 | var buffer bytes.Buffer 46 | 47 | commands := initializeCommands(ui, &buffer) 48 | output := helpFunc(commands)(commands) 49 | 50 | require.NotContains(t, output, "exec") 51 | } 52 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | ) 11 | 12 | // +groupName=api-gateway.consul.hashicorp.com 13 | 14 | const ( 15 | Group = "api-gateway.consul.hashicorp.com" 16 | Version = "v1alpha1" 17 | ) 18 | 19 | var ( 20 | GroupVersion = schema.GroupVersion{Group: Group, Version: Version} 21 | ) 22 | 23 | func RegisterTypes(scheme *runtime.Scheme) { 24 | scheme.AddKnownTypes(GroupVersion, &GatewayClassConfig{}, &GatewayClassConfigList{}) 25 | scheme.AddKnownTypes(GroupVersion, &MeshService{}, &MeshServiceList{}) 26 | meta.AddToGroupVersion(scheme, GroupVersion) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/doc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | func TestRegisterTypes(t *testing.T) { 14 | scheme := runtime.NewScheme() 15 | RegisterTypes(scheme) 16 | 17 | var foundGatewayClassConfig bool 18 | var foundGatewayClassConfigList bool 19 | for gvk := range scheme.AllKnownTypes() { 20 | if gvk.GroupVersion() == GroupVersion { 21 | if gvk.Kind == "GatewayClassConfig" { 22 | foundGatewayClassConfig = true 23 | } 24 | if gvk.Kind == "GatewayClassConfigList" { 25 | foundGatewayClassConfigList = true 26 | } 27 | } 28 | } 29 | require.True(t, foundGatewayClassConfig) 30 | require.True(t, foundGatewayClassConfigList) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/changelog-check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | set -e 7 | shopt -s extglob 8 | 9 | changelog_file_path=$1 10 | 11 | if [ ! -z "$2" ]; then 12 | enforce_matching_pull_request_number="matching this PR number " 13 | fi 14 | 15 | # Check if there is a diff matching the expected changelog file path 16 | changelog_files=$(git --no-pager diff --name-only HEAD "$(git merge-base HEAD "origin/main")" -- ${changelog_file_path}) 17 | 18 | # Exit with error if no changelog entry is found 19 | if [ -z "$changelog_files" ]; then 20 | echo "Did not find a changelog entry ${enforce_matching_pull_request_number}and the 'pr/no-changelog' label was not applied. Reference - https://github.com/hashicorp/consul/pull/8387" 21 | exit 1 22 | fi 23 | 24 | # Validate format with make changelog-check, exit with error if any note has an 25 | # invalid format 26 | for file in $changelog_files; do 27 | if ! cat $file | make changelog-check; then 28 | echo "Found a changelog entry ${enforce_matching_pull_request_number}but the note format in ${file} was invalid." 29 | exit 1 30 | fi 31 | done 32 | 33 | echo "Found valid changelog entry!" 34 | -------------------------------------------------------------------------------- /scripts/e2e_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | set -eEuo pipefail 7 | 8 | run_test() { 9 | E2E_APIGW_CONSUL_IMAGE="$E2E_APIGW_CONSUL_IMAGE" DOCKER_HOST_ROUTE="$DOCKER_HOST_ROUTE" go test -short -v -failfast -tags e2e ./internal/commands/server 10 | } 11 | 12 | check_env_vars() { 13 | # if enterprise image check that the license env var is set 14 | if [[ "$E2E_APIGW_CONSUL_IMAGE" == *"ent"* && "$CONSUL_LICENSE" == "" ]]; then 15 | echo "You are running the e2e tests against enterprise consul without an enterprise license env var set. Set an env var named \"CONSUL_LICENSE\" to a valid license and run again" 16 | exit 1 17 | fi 18 | 19 | # if running on linux the DOCKER_HOST_ROUTE should be set to the docker IP address 20 | if [[ "$(uname -s)" == "Linux" ]]; then 21 | DOCKER_HOST_ROUTE="172.17.0.1" 22 | fi 23 | } 24 | 25 | main() { 26 | check_env_vars 27 | run_test 28 | } 29 | 30 | main 31 | --------------------------------------------------------------------------------